Merge branch 'MDL-80220-main' of https://github.com/laurentdavid/moodle
[moodle.git] / lib / moodlelib.php
blob22b56caf6f7d2dcc90dffb05f4a203b849a0ef50
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/param.php');
79 /**
80 * PARAM_ALPHA - contains only English ascii letters [a-zA-Z].
82 define('PARAM_ALPHA', \core\param::ALPHA->value);
84 /**
85 * PARAM_ALPHAEXT the same contents as PARAM_ALPHA (English ascii letters [a-zA-Z]) plus the chars in quotes: "_-" allowed
86 * NOTE: originally this allowed "/" too, please use PARAM_SAFEPATH if "/" needed
88 define('PARAM_ALPHAEXT', \core\param::ALPHAEXT->value);
90 /**
91 * PARAM_ALPHANUM - expected numbers 0-9 and English ascii letters [a-zA-Z] only.
93 define('PARAM_ALPHANUM', \core\param::ALPHANUM->value);
95 /**
96 * PARAM_ALPHANUMEXT - expected numbers 0-9, letters (English ascii letters [a-zA-Z]) and _- only.
98 define('PARAM_ALPHANUMEXT', \core\param::ALPHANUMEXT->value);
101 * PARAM_AUTH - actually checks to make sure the string is a valid auth plugin
103 define('PARAM_AUTH', \core\param::AUTH->value);
106 * PARAM_BASE64 - Base 64 encoded format
108 define('PARAM_BASE64', \core\param::BASE64->value);
111 * PARAM_BOOL - converts input into 0 or 1, use for switches in forms and urls.
113 define('PARAM_BOOL', \core\param::BOOL->value);
116 * PARAM_CAPABILITY - A capability name, like 'moodle/role:manage'. Actually
117 * checked against the list of capabilities in the database.
119 define('PARAM_CAPABILITY', \core\param::CAPABILITY->value);
122 * PARAM_CLEANHTML - cleans submitted HTML code. Note that you almost never want
123 * to use this. The normal mode of operation is to use PARAM_RAW when receiving
124 * the input (required/optional_param or formslib) and then sanitise the HTML
125 * using format_text on output. This is for the rare cases when you want to
126 * sanitise the HTML on input. This cleaning may also fix xhtml strictness.
128 define('PARAM_CLEANHTML', \core\param::CLEANHTML->value);
131 * PARAM_EMAIL - an email address following the RFC
133 define('PARAM_EMAIL', \core\param::EMAIL->value);
136 * PARAM_FILE - safe file name, all dangerous chars are stripped, protects against XSS, SQL injections and directory traversals
138 define('PARAM_FILE', \core\param::FILE->value);
141 * PARAM_FLOAT - a real/floating point number.
143 * Note that you should not use PARAM_FLOAT for numbers typed in by the user.
144 * It does not work for languages that use , as a decimal separator.
145 * Use PARAM_LOCALISEDFLOAT instead.
147 define('PARAM_FLOAT', \core\param::FLOAT->value);
150 * PARAM_LOCALISEDFLOAT - a localised real/floating point number.
151 * This is preferred over PARAM_FLOAT for numbers typed in by the user.
152 * Cleans localised numbers to computer readable numbers; false for invalid numbers.
154 define('PARAM_LOCALISEDFLOAT', \core\param::LOCALISEDFLOAT->value);
157 * PARAM_HOST - expected fully qualified domain name (FQDN) or an IPv4 dotted quad (IP address)
159 define('PARAM_HOST', \core\param::HOST->value);
162 * PARAM_INT - integers only, use when expecting only numbers.
164 define('PARAM_INT', \core\param::INT->value);
167 * PARAM_LANG - checks to see if the string is a valid installed language in the current site.
169 define('PARAM_LANG', \core\param::LANG->value);
172 * PARAM_LOCALURL - expected properly formatted URL as well as one that refers to the local server itself. (NOT orthogonal to the
173 * others! Implies PARAM_URL!)
175 define('PARAM_LOCALURL', \core\param::LOCALURL->value);
178 * PARAM_NOTAGS - all html tags are stripped from the text. Do not abuse this type.
180 define('PARAM_NOTAGS', \core\param::NOTAGS->value);
183 * PARAM_PATH - safe relative path name, all dangerous chars are stripped, protects against XSS, SQL injections and directory
184 * traversals note: the leading slash is not removed, window drive letter is not allowed
186 define('PARAM_PATH', \core\param::PATH->value);
189 * PARAM_PEM - Privacy Enhanced Mail format
191 define('PARAM_PEM', \core\param::PEM->value);
194 * PARAM_PERMISSION - A permission, one of CAP_INHERIT, CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT.
196 define('PARAM_PERMISSION', \core\param::PERMISSION->value);
199 * PARAM_RAW specifies a parameter that is not cleaned/processed in any way except the discarding of the invalid utf-8 characters
201 define('PARAM_RAW', \core\param::RAW->value);
204 * PARAM_RAW_TRIMMED like PARAM_RAW but leading and trailing whitespace is stripped.
206 define('PARAM_RAW_TRIMMED', \core\param::RAW_TRIMMED->value);
209 * PARAM_SAFEDIR - safe directory name, suitable for include() and require()
211 define('PARAM_SAFEDIR', \core\param::SAFEDIR->value);
214 * PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths
215 * and other references to Moodle code files.
217 * This is NOT intended to be used for absolute paths or any user uploaded files.
219 define('PARAM_SAFEPATH', \core\param::SAFEPATH->value);
222 * PARAM_SEQUENCE - expects a sequence of numbers like 8 to 1,5,6,4,6,8,9. Numbers and comma only.
224 define('PARAM_SEQUENCE', \core\param::SEQUENCE->value);
227 * PARAM_TAG - one tag (interests, blogs, etc.) - mostly international characters and space, <> not supported
229 define('PARAM_TAG', \core\param::TAG->value);
232 * PARAM_TAGLIST - list of tags separated by commas (interests, blogs, etc.)
234 define('PARAM_TAGLIST', \core\param::TAGLIST->value);
237 * PARAM_TEXT - general plain text compatible with multilang filter, no other html tags. Please note '<', or '>' are allowed here.
239 define('PARAM_TEXT', \core\param::TEXT->value);
242 * PARAM_THEME - Checks to see if the string is a valid theme name in the current site
244 define('PARAM_THEME', \core\param::THEME->value);
247 * PARAM_URL - expected properly formatted URL. Please note that domain part is required, http://localhost/ is not accepted but
248 * http://localhost.localdomain/ is ok.
250 define('PARAM_URL', \core\param::URL->value);
253 * PARAM_USERNAME - Clean username to only contains allowed characters. This is to be used ONLY when manually creating user
254 * accounts, do NOT use when syncing with external systems!!
256 define('PARAM_USERNAME', \core\param::USERNAME->value);
259 * PARAM_STRINGID - used to check if the given string is valid string identifier for get_string()
261 define('PARAM_STRINGID', \core\param::STRINGID->value);
263 // DEPRECATED PARAM TYPES OR ALIASES - DO NOT USE FOR NEW CODE.
265 * PARAM_CLEAN - obsoleted, please use a more specific type of parameter.
266 * It was one of the first types, that is why it is abused so much ;-)
267 * @deprecated since 2.0
269 define('PARAM_CLEAN', \core\param::CLEAN->value);
272 * PARAM_INTEGER - deprecated alias for PARAM_INT
273 * @deprecated since 2.0
275 define('PARAM_INTEGER', \core\param::INT->value);
278 * PARAM_NUMBER - deprecated alias of PARAM_FLOAT
279 * @deprecated since 2.0
281 define('PARAM_NUMBER', \core\param::FLOAT->value);
284 * PARAM_ACTION - deprecated alias for PARAM_ALPHANUMEXT, use for various actions in forms and urls
285 * NOTE: originally alias for PARAM_APLHA
286 * @deprecated since 2.0
288 define('PARAM_ACTION', \core\param::ALPHANUMEXT->value);
291 * PARAM_FORMAT - deprecated alias for PARAM_ALPHANUMEXT, use for names of plugins, formats, etc.
292 * NOTE: originally alias for PARAM_APLHA
293 * @deprecated since 2.0
295 define('PARAM_FORMAT', \core\param::ALPHANUMEXT->value);
298 * PARAM_MULTILANG - deprecated alias of PARAM_TEXT.
299 * @deprecated since 2.0
301 define('PARAM_MULTILANG', \core\param::TEXT->value);
304 * PARAM_TIMEZONE - expected timezone. Timezone can be int +-(0-13) or float +-(0.5-12.5) or
305 * string separated by '/' and can have '-' &/ '_' (eg. America/North_Dakota/New_Salem
306 * America/Port-au-Prince)
308 define('PARAM_TIMEZONE', \core\param::TIMEZONE->value);
311 * PARAM_CLEANFILE - deprecated alias of PARAM_FILE; originally was removing regional chars too
312 * @deprecated since 2.0
314 define('PARAM_CLEANFILE', \core\param::CLEANFILE->value);
317 * PARAM_COMPONENT is used for full component names (aka frankenstyle) such as 'mod_forum', 'core_rating', 'auth_ldap'.
318 * Short legacy subsystem names and module names are accepted too ex: 'forum', 'rating', 'user'.
319 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
320 * NOTE: numbers and underscores are strongly discouraged in plugin names!
322 define('PARAM_COMPONENT', \core\param::COMPONENT->value);
325 * PARAM_AREA is a name of area used when addressing files, comments, ratings, etc.
326 * It is usually used together with context id and component.
327 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
329 define('PARAM_AREA', \core\param::AREA->value);
332 * PARAM_PLUGIN is used for plugin names such as 'forum', 'glossary', 'ldap', 'paypal', 'completionstatus'.
333 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
334 * NOTE: numbers and underscores are strongly discouraged in plugin names! Underscores are forbidden in module names.
336 define('PARAM_PLUGIN', \core\param::PLUGIN->value);
339 // Web Services.
342 * VALUE_REQUIRED - if the parameter is not supplied, there is an error
344 define('VALUE_REQUIRED', 1);
347 * VALUE_OPTIONAL - if the parameter is not supplied, then the param has no value
349 define('VALUE_OPTIONAL', 2);
352 * VALUE_DEFAULT - if the parameter is not supplied, then the default value is used
354 define('VALUE_DEFAULT', 0);
357 * NULL_NOT_ALLOWED - the parameter can not be set to null in the database
359 define('NULL_NOT_ALLOWED', false);
362 * NULL_ALLOWED - the parameter can be set to null in the database
364 define('NULL_ALLOWED', true);
366 // Page types.
369 * PAGE_COURSE_VIEW is a definition of a page type. For more information on the page class see moodle/lib/pagelib.php.
371 define('PAGE_COURSE_VIEW', 'course-view');
373 /** Get remote addr constant */
374 define('GETREMOTEADDR_SKIP_HTTP_CLIENT_IP', '1');
375 /** Get remote addr constant */
376 define('GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR', '2');
378 * GETREMOTEADDR_SKIP_DEFAULT defines the default behavior remote IP address validation.
380 define('GETREMOTEADDR_SKIP_DEFAULT', GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR|GETREMOTEADDR_SKIP_HTTP_CLIENT_IP);
382 // Blog access level constant declaration.
383 define ('BLOG_USER_LEVEL', 1);
384 define ('BLOG_GROUP_LEVEL', 2);
385 define ('BLOG_COURSE_LEVEL', 3);
386 define ('BLOG_SITE_LEVEL', 4);
387 define ('BLOG_GLOBAL_LEVEL', 5);
390 // Tag constants.
392 * To prevent problems with multibytes strings,Flag updating in nav not working on the review page. this should not exceed the
393 * length of "varchar(255) / 3 (bytes / utf-8 character) = 85".
394 * TODO: this is not correct, varchar(255) are 255 unicode chars ;-)
396 * @todo define(TAG_MAX_LENGTH) this is not correct, varchar(255) are 255 unicode chars ;-)
398 define('TAG_MAX_LENGTH', 50);
400 // Password policy constants.
401 define ('PASSWORD_LOWER', 'abcdefghijklmnopqrstuvwxyz');
402 define ('PASSWORD_UPPER', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
403 define ('PASSWORD_DIGITS', '0123456789');
404 define ('PASSWORD_NONALPHANUM', '.,;:!?_-+/*@#&$');
407 * Required password pepper entropy.
409 define ('PEPPER_ENTROPY', 112);
411 // Feature constants.
412 // Used for plugin_supports() to report features that are, or are not, supported by a module.
414 /** True if module can provide a grade */
415 define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade');
416 /** True if module supports outcomes */
417 define('FEATURE_GRADE_OUTCOMES', 'outcomes');
418 /** True if module supports advanced grading methods */
419 define('FEATURE_ADVANCED_GRADING', 'grade_advanced_grading');
420 /** True if module controls the grade visibility over the gradebook */
421 define('FEATURE_CONTROLS_GRADE_VISIBILITY', 'controlsgradevisbility');
422 /** True if module supports plagiarism plugins */
423 define('FEATURE_PLAGIARISM', 'plagiarism');
425 /** True if module has code to track whether somebody viewed it */
426 define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views');
427 /** True if module has custom completion rules */
428 define('FEATURE_COMPLETION_HAS_RULES', 'completion_has_rules');
430 /** True if module has no 'view' page (like label) */
431 define('FEATURE_NO_VIEW_LINK', 'viewlink');
432 /** True (which is default) if the module wants support for setting the ID number for grade calculation purposes. */
433 define('FEATURE_IDNUMBER', 'idnumber');
434 /** True if module supports groups */
435 define('FEATURE_GROUPS', 'groups');
436 /** True if module supports groupings */
437 define('FEATURE_GROUPINGS', 'groupings');
439 * True if module supports groupmembersonly (which no longer exists)
440 * @deprecated Since Moodle 2.8
442 define('FEATURE_GROUPMEMBERSONLY', 'groupmembersonly');
444 /** Type of module */
445 define('FEATURE_MOD_ARCHETYPE', 'mod_archetype');
446 /** True if module supports intro editor */
447 define('FEATURE_MOD_INTRO', 'mod_intro');
448 /** True if module has default completion */
449 define('FEATURE_MODEDIT_DEFAULT_COMPLETION', 'modedit_default_completion');
451 define('FEATURE_COMMENT', 'comment');
453 define('FEATURE_RATE', 'rate');
454 /** True if module supports backup/restore of moodle2 format */
455 define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2');
457 /** True if module can show description on course main page */
458 define('FEATURE_SHOW_DESCRIPTION', 'showdescription');
460 /** True if module uses the question bank */
461 define('FEATURE_USES_QUESTIONS', 'usesquestions');
464 * Maximum filename char size
466 define('MAX_FILENAME_SIZE', 100);
468 /** Unspecified module archetype */
469 define('MOD_ARCHETYPE_OTHER', 0);
470 /** Resource-like type module */
471 define('MOD_ARCHETYPE_RESOURCE', 1);
472 /** Assignment module archetype */
473 define('MOD_ARCHETYPE_ASSIGNMENT', 2);
474 /** System (not user-addable) module archetype */
475 define('MOD_ARCHETYPE_SYSTEM', 3);
477 /** Type of module */
478 define('FEATURE_MOD_PURPOSE', 'mod_purpose');
479 /** Module purpose administration */
480 define('MOD_PURPOSE_ADMINISTRATION', 'administration');
481 /** Module purpose assessment */
482 define('MOD_PURPOSE_ASSESSMENT', 'assessment');
483 /** Module purpose communication */
484 define('MOD_PURPOSE_COLLABORATION', 'collaboration');
485 /** Module purpose communication */
486 define('MOD_PURPOSE_COMMUNICATION', 'communication');
487 /** Module purpose content */
488 define('MOD_PURPOSE_CONTENT', 'content');
489 /** Module purpose interactive content */
490 define('MOD_PURPOSE_INTERACTIVECONTENT', 'interactivecontent');
491 /** Module purpose other */
492 define('MOD_PURPOSE_OTHER', 'other');
494 * Module purpose interface
495 * @deprecated since Moodle 4.4
496 * @todo MDL-80701 Remove in Moodle 4.8
498 define('MOD_PURPOSE_INTERFACE', 'interface');
501 * Security token used for allowing access
502 * from external application such as web services.
503 * Scripts do not use any session, performance is relatively
504 * low because we need to load access info in each request.
505 * Scripts are executed in parallel.
507 define('EXTERNAL_TOKEN_PERMANENT', 0);
510 * Security token used for allowing access
511 * of embedded applications, the code is executed in the
512 * active user session. Token is invalidated after user logs out.
513 * Scripts are executed serially - normal session locking is used.
515 define('EXTERNAL_TOKEN_EMBEDDED', 1);
518 * The home page should be the site home
520 define('HOMEPAGE_SITE', 0);
522 * The home page should be the users my page
524 define('HOMEPAGE_MY', 1);
526 * The home page can be chosen by the user
528 define('HOMEPAGE_USER', 2);
530 * The home page should be the users my courses page
532 define('HOMEPAGE_MYCOURSES', 3);
535 * URL of the Moodle sites registration portal.
537 defined('HUB_MOODLEORGHUBURL') || define('HUB_MOODLEORGHUBURL', 'https://stats.moodle.org');
540 * URL of main Moodle site for marketing, products and services.
542 defined('MOODLE_PRODUCTURL') || define('MOODLE_PRODUCTURL', 'https://moodle.com');
545 * URL of the statistic server public key.
547 defined('HUB_STATSPUBLICKEY') || define('HUB_STATSPUBLICKEY', 'https://moodle.org/static/statspubkey.pem');
550 * Moodle mobile app service name
552 define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app');
555 * Indicates the user has the capabilities required to ignore activity and course file size restrictions
557 define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1);
560 * Course display settings: display all sections on one page.
562 define('COURSE_DISPLAY_SINGLEPAGE', 0);
564 * Course display settings: split pages into a page per section.
566 define('COURSE_DISPLAY_MULTIPAGE', 1);
569 * Authentication constant: String used in password field when password is not stored.
571 define('AUTH_PASSWORD_NOT_CACHED', 'not cached');
574 * Email from header to never include via information.
576 define('EMAIL_VIA_NEVER', 0);
579 * Email from header to always include via information.
581 define('EMAIL_VIA_ALWAYS', 1);
584 * Email from header to only include via information if the address is no-reply.
586 define('EMAIL_VIA_NO_REPLY_ONLY', 2);
589 * Contact site support form/link disabled.
591 define('CONTACT_SUPPORT_DISABLED', 0);
594 * Contact site support form/link only available to authenticated users.
596 define('CONTACT_SUPPORT_AUTHENTICATED', 1);
599 * Contact site support form/link available to anyone visiting the site.
601 define('CONTACT_SUPPORT_ANYONE', 2);
604 * Maximum number of characters for password.
606 define('MAX_PASSWORD_CHARACTERS', 128);
609 * Toggle sensitive feature is disabled. Used for sensitive inputs (passwords, tokens, keys).
611 define('TOGGLE_SENSITIVE_DISABLED', 0);
614 * Toggle sensitive feature is enabled. Used for sensitive inputs (passwords, tokens, keys).
616 define('TOGGLE_SENSITIVE_ENABLED', 1);
619 * Toggle sensitive feature is enabled for small screens only. Used for sensitive inputs (passwords, tokens, keys).
621 define('TOGGLE_SENSITIVE_SMALL_SCREENS_ONLY', 2);
623 // PARAMETER HANDLING.
626 * Returns a particular value for the named variable, taken from
627 * POST or GET. If the parameter doesn't exist then an error is
628 * thrown because we require this variable.
630 * This function should be used to initialise all required values
631 * in a script that are based on parameters. Usually it will be
632 * used like this:
633 * $id = required_param('id', PARAM_INT);
635 * Please note the $type parameter is now required and the value can not be array.
637 * @param string $parname the name of the page parameter we want
638 * @param string $type expected type of parameter
639 * @return mixed
640 * @throws coding_exception
642 function required_param($parname, $type) {
643 return \core\param::from_type($type)->required_param($parname);
647 * Returns a particular array value for the named variable, taken from
648 * POST or GET. If the parameter doesn't exist then an error is
649 * thrown because we require this variable.
651 * This function should be used to initialise all required values
652 * in a script that are based on parameters. Usually it will be
653 * used like this:
654 * $ids = required_param_array('ids', PARAM_INT);
656 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
658 * @param string $parname the name of the page parameter we want
659 * @param string $type expected type of parameter
660 * @return array
661 * @throws coding_exception
663 function required_param_array($parname, $type) {
664 return \core\param::from_type($type)->required_param_array($parname);
668 * Returns a particular value for the named variable, taken from
669 * POST or GET, otherwise returning a given default.
671 * This function should be used to initialise all optional values
672 * in a script that are based on parameters. Usually it will be
673 * used like this:
674 * $name = optional_param('name', 'Fred', PARAM_TEXT);
676 * Please note the $type parameter is now required and the value can not be array.
678 * @param string $parname the name of the page parameter we want
679 * @param mixed $default the default value to return if nothing is found
680 * @param string $type expected type of parameter
681 * @return mixed
682 * @throws coding_exception
684 function optional_param($parname, $default, $type) {
685 return \core\param::from_type($type)->optional_param(
686 paramname: $parname,
687 default: $default,
692 * Returns a particular array value for the named variable, taken from
693 * POST or GET, otherwise returning a given default.
695 * This function should be used to initialise all optional values
696 * in a script that are based on parameters. Usually it will be
697 * used like this:
698 * $ids = optional_param('id', array(), PARAM_INT);
700 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
702 * @param string $parname the name of the page parameter we want
703 * @param mixed $default the default value to return if nothing is found
704 * @param string $type expected type of parameter
705 * @return array
706 * @throws coding_exception
708 function optional_param_array($parname, $default, $type) {
709 return \core\param::from_type($type)->optional_param_array(
710 paramname: $parname,
711 default: $default,
716 * Strict validation of parameter values, the values are only converted
717 * to requested PHP type. Internally it is using clean_param, the values
718 * before and after cleaning must be equal - otherwise
719 * an invalid_parameter_exception is thrown.
720 * Objects and classes are not accepted.
722 * @param mixed $param
723 * @param string $type PARAM_ constant
724 * @param bool $allownull are nulls valid value?
725 * @param string $debuginfo optional debug information
726 * @return mixed the $param value converted to PHP type
727 * @throws invalid_parameter_exception if $param is not of given type
729 function validate_param($param, $type, $allownull = NULL_NOT_ALLOWED, $debuginfo = '') {
730 return \core\param::from_type($type)->validate_param(
731 param: $param,
732 allownull: $allownull,
733 debuginfo: $debuginfo,
738 * Makes sure array contains only the allowed types, this function does not validate array key names!
740 * <code>
741 * $options = clean_param($options, PARAM_INT);
742 * </code>
744 * @param array|null $param the variable array we are cleaning
745 * @param string $type expected format of param after cleaning.
746 * @param bool $recursive clean recursive arrays
747 * @return array
748 * @throws coding_exception
750 function clean_param_array(?array $param, $type, $recursive = false) {
751 return \core\param::from_type($type)->clean_param_array(
752 param: $param,
753 recursive: $recursive,
758 * Used by {@link optional_param()} and {@link required_param()} to
759 * clean the variables and/or cast to specific types, based on
760 * an options field.
761 * <code>
762 * $course->format = clean_param($course->format, PARAM_ALPHA);
763 * $selectedgradeitem = clean_param($selectedgradeitem, PARAM_INT);
764 * </code>
766 * @param mixed $param the variable we are cleaning
767 * @param string $type expected format of param after cleaning.
768 * @return mixed
769 * @throws coding_exception
771 function clean_param($param, $type) {
772 return \core\param::from_type($type)->clean($param);
776 * Whether the PARAM_* type is compatible in RTL.
778 * Being compatible with RTL means that the data they contain can flow
779 * from right-to-left or left-to-right without compromising the user experience.
781 * Take URLs for example, they are not RTL compatible as they should always
782 * flow from the left to the right. This also applies to numbers, email addresses,
783 * configuration snippets, base64 strings, etc...
785 * This function tries to best guess which parameters can contain localised strings.
787 * @param string $paramtype Constant PARAM_*.
788 * @return bool
790 function is_rtl_compatible($paramtype) {
791 return $paramtype == PARAM_TEXT || $paramtype == PARAM_NOTAGS;
795 * Makes sure the data is using valid utf8, invalid characters are discarded.
797 * Note: this function is not intended for full objects with methods and private properties.
799 * @param mixed $value
800 * @return mixed with proper utf-8 encoding
802 function fix_utf8($value) {
803 if (is_null($value) or $value === '') {
804 return $value;
806 } else if (is_string($value)) {
807 if ((string)(int)$value === $value) {
808 // Shortcut.
809 return $value;
812 // Remove null bytes or invalid Unicode sequences from value.
813 $value = str_replace(["\0", "\xef\xbf\xbe", "\xef\xbf\xbf"], '', $value);
815 // Note: this duplicates min_fix_utf8() intentionally.
816 static $buggyiconv = null;
817 if ($buggyiconv === null) {
818 $buggyiconv = (!function_exists('iconv') or @iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€');
821 if ($buggyiconv) {
822 if (function_exists('mb_convert_encoding')) {
823 $subst = mb_substitute_character();
824 mb_substitute_character('none');
825 $result = mb_convert_encoding($value, 'utf-8', 'utf-8');
826 mb_substitute_character($subst);
828 } else {
829 // Warn admins on admin/index.php page.
830 $result = $value;
833 } else {
834 $result = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
837 return $result;
839 } else if (is_array($value)) {
840 foreach ($value as $k => $v) {
841 $value[$k] = fix_utf8($v);
843 return $value;
845 } else if (is_object($value)) {
846 // Do not modify original.
847 $value = clone($value);
848 foreach ($value as $k => $v) {
849 $value->$k = fix_utf8($v);
851 return $value;
853 } else {
854 // This is some other type, no utf-8 here.
855 return $value;
860 * Return true if given value is integer or string with integer value
862 * @param mixed $value String or Int
863 * @return bool true if number, false if not
865 function is_number($value) {
866 if (is_int($value)) {
867 return true;
868 } else if (is_string($value)) {
869 return ((string)(int)$value) === $value;
870 } else {
871 return false;
876 * Returns host part from url.
878 * @param string $url full url
879 * @return string host, null if not found
881 function get_host_from_url($url) {
882 preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches);
883 if ($matches) {
884 return $matches[1];
886 return null;
890 * Tests whether anything was returned by text editor
892 * This function is useful for testing whether something you got back from
893 * the HTML editor actually contains anything. Sometimes the HTML editor
894 * appear to be empty, but actually you get back a <br> tag or something.
896 * @param string $string a string containing HTML.
897 * @return boolean does the string contain any actual content - that is text,
898 * images, objects, etc.
900 function html_is_blank($string) {
901 return trim(strip_tags((string)$string, '<img><object><applet><input><select><textarea><hr>')) == '';
905 * Set a key in global configuration
907 * Set a key/value pair in both this session's {@link $CFG} global variable
908 * and in the 'config' database table for future sessions.
910 * Can also be used to update keys for plugin-scoped configs in config_plugin table.
911 * In that case it doesn't affect $CFG.
913 * A NULL value will delete the entry.
915 * NOTE: this function is called from lib/db/upgrade.php
917 * @param string $name the key to set
918 * @param string $value the value to set (without magic quotes)
919 * @param string $plugin (optional) the plugin scope, default null
920 * @return bool true or exception
922 function set_config($name, $value, $plugin = null) {
923 global $CFG, $DB;
925 // Redirect to appropriate handler when value is null.
926 if ($value === null) {
927 return unset_config($name, $plugin);
930 // Set variables determining conditions and where to store the new config.
931 // Plugin config goes to {config_plugins}, core config goes to {config}.
932 $iscore = empty($plugin);
933 if ($iscore) {
934 // If it's for core config.
935 $table = 'config';
936 $conditions = ['name' => $name];
937 $invalidatecachekey = 'core';
938 } else {
939 // If it's a plugin.
940 $table = 'config_plugins';
941 $conditions = ['name' => $name, 'plugin' => $plugin];
942 $invalidatecachekey = $plugin;
945 // DB handling - checks for existing config, updating or inserting only if necessary.
946 $invalidatecache = true;
947 $inserted = false;
948 $record = $DB->get_record($table, $conditions, 'id, value');
949 if ($record === false) {
950 // Inserts a new config record.
951 $config = new stdClass();
952 $config->name = $name;
953 $config->value = $value;
954 if (!$iscore) {
955 $config->plugin = $plugin;
957 $inserted = $DB->insert_record($table, $config, false);
958 } else if ($invalidatecache = ($record->value !== $value)) {
959 // Record exists - Check and only set new value if it has changed.
960 $DB->set_field($table, 'value', $value, ['id' => $record->id]);
963 if ($iscore && !isset($CFG->config_php_settings[$name])) {
964 // So it's defined for this invocation at least.
965 // Settings from db are always strings.
966 $CFG->$name = (string) $value;
969 // When setting config during a Behat test (in the CLI script, not in the web browser
970 // requests), remember which ones are set so that we can clear them later.
971 if ($iscore && $inserted && defined('BEHAT_TEST')) {
972 $CFG->behat_cli_added_config[$name] = true;
975 // Update siteidentifier cache, if required.
976 if ($iscore && $name === 'siteidentifier') {
977 cache_helper::update_site_identifier($value);
980 // Invalidate cache, if required.
981 if ($invalidatecache) {
982 cache_helper::invalidate_by_definition('core', 'config', [], $invalidatecachekey);
985 return true;
989 * Get configuration values from the global config table
990 * or the config_plugins table.
992 * If called with one parameter, it will load all the config
993 * variables for one plugin, and return them as an object.
995 * If called with 2 parameters it will return a string single
996 * value or false if the value is not found.
998 * NOTE: this function is called from lib/db/upgrade.php
1000 * @param string $plugin full component name
1001 * @param string $name default null
1002 * @return mixed hash-like object or single value, return false no config found
1003 * @throws dml_exception
1005 function get_config($plugin, $name = null) {
1006 global $CFG, $DB;
1008 if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) {
1009 $forced =& $CFG->config_php_settings;
1010 $iscore = true;
1011 $plugin = 'core';
1012 } else {
1013 if (array_key_exists($plugin, $CFG->forced_plugin_settings)) {
1014 $forced =& $CFG->forced_plugin_settings[$plugin];
1015 } else {
1016 $forced = array();
1018 $iscore = false;
1021 if (!isset($CFG->siteidentifier)) {
1022 try {
1023 // This may throw an exception during installation, which is how we detect the
1024 // need to install the database. For more details see {@see initialise_cfg()}.
1025 $CFG->siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
1026 } catch (dml_exception $ex) {
1027 // Set siteidentifier to false. We don't want to trip this continually.
1028 $siteidentifier = false;
1029 throw $ex;
1033 if (!empty($name)) {
1034 if (array_key_exists($name, $forced)) {
1035 return (string)$forced[$name];
1036 } else if ($name === 'siteidentifier' && $plugin == 'core') {
1037 return $CFG->siteidentifier;
1041 $cache = cache::make('core', 'config');
1042 $result = $cache->get($plugin);
1043 if ($result === false) {
1044 // The user is after a recordset.
1045 if (!$iscore) {
1046 $result = $DB->get_records_menu('config_plugins', array('plugin' => $plugin), '', 'name,value');
1047 } else {
1048 // This part is not really used any more, but anyway...
1049 $result = $DB->get_records_menu('config', array(), '', 'name,value');;
1051 $cache->set($plugin, $result);
1054 if (!empty($name)) {
1055 if (array_key_exists($name, $result)) {
1056 return $result[$name];
1058 return false;
1061 if ($plugin === 'core') {
1062 $result['siteidentifier'] = $CFG->siteidentifier;
1065 foreach ($forced as $key => $value) {
1066 if (is_null($value) or is_array($value) or is_object($value)) {
1067 // We do not want any extra mess here, just real settings that could be saved in db.
1068 unset($result[$key]);
1069 } else {
1070 // Convert to string as if it went through the DB.
1071 $result[$key] = (string)$value;
1075 return (object)$result;
1079 * Removes a key from global configuration.
1081 * NOTE: this function is called from lib/db/upgrade.php
1083 * @param string $name the key to set
1084 * @param string $plugin (optional) the plugin scope
1085 * @return boolean whether the operation succeeded.
1087 function unset_config($name, $plugin=null) {
1088 global $CFG, $DB;
1090 if (empty($plugin)) {
1091 unset($CFG->$name);
1092 $DB->delete_records('config', array('name' => $name));
1093 cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1094 } else {
1095 $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
1096 cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1099 return true;
1103 * Remove all the config variables for a given plugin.
1105 * NOTE: this function is called from lib/db/upgrade.php
1107 * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice';
1108 * @return boolean whether the operation succeeded.
1110 function unset_all_config_for_plugin($plugin) {
1111 global $DB;
1112 // Delete from the obvious config_plugins first.
1113 $DB->delete_records('config_plugins', array('plugin' => $plugin));
1114 // Next delete any suspect settings from config.
1115 $like = $DB->sql_like('name', '?', true, true, false, '|');
1116 $params = array($DB->sql_like_escape($plugin.'_', '|') . '%');
1117 $DB->delete_records_select('config', $like, $params);
1118 // Finally clear both the plugin cache and the core cache (suspect settings now removed from core).
1119 cache_helper::invalidate_by_definition('core', 'config', array(), array('core', $plugin));
1121 return true;
1125 * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability.
1127 * All users are verified if they still have the necessary capability.
1129 * @param string $value the value of the config setting.
1130 * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor.
1131 * @param bool $includeadmins include administrators.
1132 * @return array of user objects.
1134 function get_users_from_config($value, $capability, $includeadmins = true) {
1135 if (empty($value) or $value === '$@NONE@$') {
1136 return array();
1139 // We have to make sure that users still have the necessary capability,
1140 // it should be faster to fetch them all first and then test if they are present
1141 // instead of validating them one-by-one.
1142 $users = get_users_by_capability(context_system::instance(), $capability);
1143 if ($includeadmins) {
1144 $admins = get_admins();
1145 foreach ($admins as $admin) {
1146 $users[$admin->id] = $admin;
1150 if ($value === '$@ALL@$') {
1151 return $users;
1154 $result = array(); // Result in correct order.
1155 $allowed = explode(',', $value);
1156 foreach ($allowed as $uid) {
1157 if (isset($users[$uid])) {
1158 $user = $users[$uid];
1159 $result[$user->id] = $user;
1163 return $result;
1168 * Invalidates browser caches and cached data in temp.
1170 * @return void
1172 function purge_all_caches() {
1173 purge_caches();
1177 * Selectively invalidate different types of cache.
1179 * Purges the cache areas specified. By default, this will purge all caches but can selectively purge specific
1180 * areas alone or in combination.
1182 * @param bool[] $options Specific parts of the cache to purge. Valid options are:
1183 * 'muc' Purge MUC caches?
1184 * 'theme' Purge theme cache?
1185 * 'lang' Purge language string cache?
1186 * 'js' Purge javascript cache?
1187 * 'filter' Purge text filter cache?
1188 * 'other' Purge all other caches?
1190 function purge_caches($options = []) {
1191 $defaults = array_fill_keys(['muc', 'theme', 'lang', 'js', 'template', 'filter', 'other'], false);
1192 if (empty(array_filter($options))) {
1193 $options = array_fill_keys(array_keys($defaults), true); // Set all options to true.
1194 } else {
1195 $options = array_merge($defaults, array_intersect_key($options, $defaults)); // Override defaults with specified options.
1197 if ($options['muc']) {
1198 cache_helper::purge_all();
1200 if ($options['theme']) {
1201 theme_reset_all_caches();
1203 if ($options['lang']) {
1204 get_string_manager()->reset_caches();
1206 if ($options['js']) {
1207 js_reset_all_caches();
1209 if ($options['template']) {
1210 template_reset_all_caches();
1212 if ($options['filter']) {
1213 reset_text_filters_cache();
1215 if ($options['other']) {
1216 purge_other_caches();
1221 * Purge all non-MUC caches not otherwise purged in purge_caches.
1223 * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
1224 * {@link phpunit_util::reset_dataroot()}
1226 function purge_other_caches() {
1227 global $DB, $CFG;
1228 if (class_exists('core_plugin_manager')) {
1229 core_plugin_manager::reset_caches();
1232 // Bump up cacherev field for all courses.
1233 try {
1234 increment_revision_number('course', 'cacherev', '');
1235 } catch (moodle_exception $e) {
1236 // Ignore exception since this function is also called before upgrade script when field course.cacherev does not exist yet.
1239 $DB->reset_caches();
1241 // Purge all other caches: rss, simplepie, etc.
1242 clearstatcache();
1243 remove_dir($CFG->cachedir.'', true);
1245 // Make sure cache dir is writable, throws exception if not.
1246 make_cache_directory('');
1248 // This is the only place where we purge local caches, we are only adding files there.
1249 // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes.
1250 remove_dir($CFG->localcachedir, true);
1251 set_config('localcachedirpurged', time());
1252 make_localcache_directory('', true);
1253 \core\task\manager::clear_static_caches();
1257 * Get volatile flags
1259 * @param string $type
1260 * @param int $changedsince default null
1261 * @return array records array
1263 function get_cache_flags($type, $changedsince = null) {
1264 global $DB;
1266 $params = array('type' => $type, 'expiry' => time());
1267 $sqlwhere = "flagtype = :type AND expiry >= :expiry";
1268 if ($changedsince !== null) {
1269 $params['changedsince'] = $changedsince;
1270 $sqlwhere .= " AND timemodified > :changedsince";
1272 $cf = array();
1273 if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) {
1274 foreach ($flags as $flag) {
1275 $cf[$flag->name] = $flag->value;
1278 return $cf;
1282 * Get volatile flags
1284 * @param string $type
1285 * @param string $name
1286 * @param int $changedsince default null
1287 * @return string|false The cache flag value or false
1289 function get_cache_flag($type, $name, $changedsince=null) {
1290 global $DB;
1292 $params = array('type' => $type, 'name' => $name, 'expiry' => time());
1294 $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry";
1295 if ($changedsince !== null) {
1296 $params['changedsince'] = $changedsince;
1297 $sqlwhere .= " AND timemodified > :changedsince";
1300 return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params);
1304 * Set a volatile flag
1306 * @param string $type the "type" namespace for the key
1307 * @param string $name the key to set
1308 * @param string $value the value to set (without magic quotes) - null will remove the flag
1309 * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs
1310 * @return bool Always returns true
1312 function set_cache_flag($type, $name, $value, $expiry = null) {
1313 global $DB;
1315 $timemodified = time();
1316 if ($expiry === null || $expiry < $timemodified) {
1317 $expiry = $timemodified + 24 * 60 * 60;
1318 } else {
1319 $expiry = (int)$expiry;
1322 if ($value === null) {
1323 unset_cache_flag($type, $name);
1324 return true;
1327 if ($f = $DB->get_record('cache_flags', array('name' => $name, 'flagtype' => $type), '*', IGNORE_MULTIPLE)) {
1328 // This is a potential problem in DEBUG_DEVELOPER.
1329 if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) {
1330 return true; // No need to update.
1332 $f->value = $value;
1333 $f->expiry = $expiry;
1334 $f->timemodified = $timemodified;
1335 $DB->update_record('cache_flags', $f);
1336 } else {
1337 $f = new stdClass();
1338 $f->flagtype = $type;
1339 $f->name = $name;
1340 $f->value = $value;
1341 $f->expiry = $expiry;
1342 $f->timemodified = $timemodified;
1343 $DB->insert_record('cache_flags', $f);
1345 return true;
1349 * Removes a single volatile flag
1351 * @param string $type the "type" namespace for the key
1352 * @param string $name the key to set
1353 * @return bool
1355 function unset_cache_flag($type, $name) {
1356 global $DB;
1357 $DB->delete_records('cache_flags', array('name' => $name, 'flagtype' => $type));
1358 return true;
1362 * Garbage-collect volatile flags
1364 * @return bool Always returns true
1366 function gc_cache_flags() {
1367 global $DB;
1368 $DB->delete_records_select('cache_flags', 'expiry < ?', array(time()));
1369 return true;
1372 // USER PREFERENCE API.
1375 * Refresh user preference cache. This is used most often for $USER
1376 * object that is stored in session, but it also helps with performance in cron script.
1378 * Preferences for each user are loaded on first use on every page, then again after the timeout expires.
1380 * @package core
1381 * @category preference
1382 * @access public
1383 * @param stdClass $user User object. Preferences are preloaded into 'preference' property
1384 * @param int $cachelifetime Cache life time on the current page (in seconds)
1385 * @throws coding_exception
1386 * @return null
1388 function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120) {
1389 global $DB;
1390 // Static cache, we need to check on each page load, not only every 2 minutes.
1391 static $loadedusers = array();
1393 if (!isset($user->id)) {
1394 throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field');
1397 if (empty($user->id) or isguestuser($user->id)) {
1398 // No permanent storage for not-logged-in users and guest.
1399 if (!isset($user->preference)) {
1400 $user->preference = array();
1402 return;
1405 $timenow = time();
1407 if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) {
1408 // Already loaded at least once on this page. Are we up to date?
1409 if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) {
1410 // No need to reload - we are on the same page and we loaded prefs just a moment ago.
1411 return;
1413 } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) {
1414 // No change since the lastcheck on this page.
1415 $user->preference['_lastloaded'] = $timenow;
1416 return;
1420 // OK, so we have to reload all preferences.
1421 $loadedusers[$user->id] = true;
1422 $user->preference = $DB->get_records_menu('user_preferences', array('userid' => $user->id), '', 'name,value'); // All values.
1423 $user->preference['_lastloaded'] = $timenow;
1427 * Called from set/unset_user_preferences, so that the prefs can be correctly reloaded in different sessions.
1429 * NOTE: internal function, do not call from other code.
1431 * @package core
1432 * @access private
1433 * @param integer $userid the user whose prefs were changed.
1435 function mark_user_preferences_changed($userid) {
1436 global $CFG;
1438 if (empty($userid) or isguestuser($userid)) {
1439 // No cache flags for guest and not-logged-in users.
1440 return;
1443 set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout);
1447 * Sets a preference for the specified user.
1449 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1451 * When additional validation/permission check is needed it is better to use {@see useredit_update_user_preference()}
1453 * @package core
1454 * @category preference
1455 * @access public
1456 * @param string $name The key to set as preference for the specified user
1457 * @param string $value The value to set for the $name key in the specified user's
1458 * record, null means delete current value.
1459 * @param stdClass|int|null $user A moodle user object or id, null means current user
1460 * @throws coding_exception
1461 * @return bool Always true or exception
1463 function set_user_preference($name, $value, $user = null) {
1464 global $USER, $DB;
1466 if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1467 throw new coding_exception('Invalid preference name in set_user_preference() call');
1470 if (is_null($value)) {
1471 // Null means delete current.
1472 return unset_user_preference($name, $user);
1473 } else if (is_object($value)) {
1474 throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed');
1475 } else if (is_array($value)) {
1476 throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed');
1478 // Value column maximum length is 1333 characters.
1479 $value = (string)$value;
1480 if (core_text::strlen($value) > 1333) {
1481 throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column');
1484 if (is_null($user)) {
1485 $user = $USER;
1486 } else if (isset($user->id)) {
1487 // It is a valid object.
1488 } else if (is_numeric($user)) {
1489 $user = (object)array('id' => (int)$user);
1490 } else {
1491 throw new coding_exception('Invalid $user parameter in set_user_preference() call');
1494 check_user_preferences_loaded($user);
1496 if (empty($user->id) or isguestuser($user->id)) {
1497 // No permanent storage for not-logged-in users and guest.
1498 $user->preference[$name] = $value;
1499 return true;
1502 if ($preference = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => $name))) {
1503 if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) {
1504 // Preference already set to this value.
1505 return true;
1507 $DB->set_field('user_preferences', 'value', $value, array('id' => $preference->id));
1509 } else {
1510 $preference = new stdClass();
1511 $preference->userid = $user->id;
1512 $preference->name = $name;
1513 $preference->value = $value;
1514 $DB->insert_record('user_preferences', $preference);
1517 // Update value in cache.
1518 $user->preference[$name] = $value;
1519 // Update the $USER in case where we've not a direct reference to $USER.
1520 if ($user !== $USER && $user->id == $USER->id) {
1521 $USER->preference[$name] = $value;
1524 // Set reload flag for other sessions.
1525 mark_user_preferences_changed($user->id);
1527 return true;
1531 * Sets a whole array of preferences for the current user
1533 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1535 * @package core
1536 * @category preference
1537 * @access public
1538 * @param array $prefarray An array of key/value pairs to be set
1539 * @param stdClass|int|null $user A moodle user object or id, null means current user
1540 * @return bool Always true or exception
1542 function set_user_preferences(array $prefarray, $user = null) {
1543 foreach ($prefarray as $name => $value) {
1544 set_user_preference($name, $value, $user);
1546 return true;
1550 * Unsets a preference completely by deleting it from the database
1552 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1554 * @package core
1555 * @category preference
1556 * @access public
1557 * @param string $name The key to unset as preference for the specified user
1558 * @param stdClass|int|null $user A moodle user object or id, null means current user
1559 * @throws coding_exception
1560 * @return bool Always true or exception
1562 function unset_user_preference($name, $user = null) {
1563 global $USER, $DB;
1565 if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1566 throw new coding_exception('Invalid preference name in unset_user_preference() call');
1569 if (is_null($user)) {
1570 $user = $USER;
1571 } else if (isset($user->id)) {
1572 // It is a valid object.
1573 } else if (is_numeric($user)) {
1574 $user = (object)array('id' => (int)$user);
1575 } else {
1576 throw new coding_exception('Invalid $user parameter in unset_user_preference() call');
1579 check_user_preferences_loaded($user);
1581 if (empty($user->id) or isguestuser($user->id)) {
1582 // No permanent storage for not-logged-in user and guest.
1583 unset($user->preference[$name]);
1584 return true;
1587 // Delete from DB.
1588 $DB->delete_records('user_preferences', array('userid' => $user->id, 'name' => $name));
1590 // Delete the preference from cache.
1591 unset($user->preference[$name]);
1592 // Update the $USER in case where we've not a direct reference to $USER.
1593 if ($user !== $USER && $user->id == $USER->id) {
1594 unset($USER->preference[$name]);
1597 // Set reload flag for other sessions.
1598 mark_user_preferences_changed($user->id);
1600 return true;
1604 * Used to fetch user preference(s)
1606 * If no arguments are supplied this function will return
1607 * all of the current user preferences as an array.
1609 * If a name is specified then this function
1610 * attempts to return that particular preference value. If
1611 * none is found, then the optional value $default is returned,
1612 * otherwise null.
1614 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1616 * @package core
1617 * @category preference
1618 * @access public
1619 * @param string $name Name of the key to use in finding a preference value
1620 * @param mixed|null $default Value to be returned if the $name key is not set in the user preferences
1621 * @param stdClass|int|null $user A moodle user object or id, null means current user
1622 * @throws coding_exception
1623 * @return string|mixed|null A string containing the value of a single preference. An
1624 * array with all of the preferences or null
1626 function get_user_preferences($name = null, $default = null, $user = null) {
1627 global $USER;
1629 if (is_null($name)) {
1630 // All prefs.
1631 } else if (is_numeric($name) or $name === '_lastloaded') {
1632 throw new coding_exception('Invalid preference name in get_user_preferences() call');
1635 if (is_null($user)) {
1636 $user = $USER;
1637 } else if (isset($user->id)) {
1638 // Is a valid object.
1639 } else if (is_numeric($user)) {
1640 if ($USER->id == $user) {
1641 $user = $USER;
1642 } else {
1643 $user = (object)array('id' => (int)$user);
1645 } else {
1646 throw new coding_exception('Invalid $user parameter in get_user_preferences() call');
1649 check_user_preferences_loaded($user);
1651 if (empty($name)) {
1652 // All values.
1653 return $user->preference;
1654 } else if (isset($user->preference[$name])) {
1655 // The single string value.
1656 return $user->preference[$name];
1657 } else {
1658 // Default value (null if not specified).
1659 return $default;
1663 // FUNCTIONS FOR HANDLING TIME.
1666 * Given Gregorian date parts in user time produce a GMT timestamp.
1668 * @package core
1669 * @category time
1670 * @param int $year The year part to create timestamp of
1671 * @param int $month The month part to create timestamp of
1672 * @param int $day The day part to create timestamp of
1673 * @param int $hour The hour part to create timestamp of
1674 * @param int $minute The minute part to create timestamp of
1675 * @param int $second The second part to create timestamp of
1676 * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset.
1677 * if 99 then default user's timezone is used {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
1678 * @param bool $applydst Toggle Daylight Saving Time, default true, will be
1679 * applied only if timezone is 99 or string.
1680 * @return int GMT timestamp
1682 function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) {
1683 $date = new DateTime('now', core_date::get_user_timezone_object($timezone));
1684 $date->setDate((int)$year, (int)$month, (int)$day);
1685 $date->setTime((int)$hour, (int)$minute, (int)$second);
1687 $time = $date->getTimestamp();
1689 if ($time === false) {
1690 throw new coding_exception('getTimestamp() returned false, please ensure you have passed correct values.'.
1691 ' This can fail if year is more than 2038 and OS is 32 bit windows');
1694 // Moodle BC DST stuff.
1695 if (!$applydst) {
1696 $time += dst_offset_on($time, $timezone);
1699 return $time;
1704 * Format a date/time (seconds) as weeks, days, hours etc as needed
1706 * Given an amount of time in seconds, returns string
1707 * formatted nicely as years, days, hours etc as needed
1709 * @package core
1710 * @category time
1711 * @uses MINSECS
1712 * @uses HOURSECS
1713 * @uses DAYSECS
1714 * @uses YEARSECS
1715 * @param int $totalsecs Time in seconds
1716 * @param stdClass $str Should be a time object
1717 * @return string A nicely formatted date/time string
1719 function format_time($totalsecs, $str = null) {
1721 $totalsecs = abs($totalsecs);
1723 if (!$str) {
1724 // Create the str structure the slow way.
1725 $str = new stdClass();
1726 $str->day = get_string('day');
1727 $str->days = get_string('days');
1728 $str->hour = get_string('hour');
1729 $str->hours = get_string('hours');
1730 $str->min = get_string('min');
1731 $str->mins = get_string('mins');
1732 $str->sec = get_string('sec');
1733 $str->secs = get_string('secs');
1734 $str->year = get_string('year');
1735 $str->years = get_string('years');
1738 $years = floor($totalsecs/YEARSECS);
1739 $remainder = $totalsecs - ($years*YEARSECS);
1740 $days = floor($remainder/DAYSECS);
1741 $remainder = $totalsecs - ($days*DAYSECS);
1742 $hours = floor($remainder/HOURSECS);
1743 $remainder = $remainder - ($hours*HOURSECS);
1744 $mins = floor($remainder/MINSECS);
1745 $secs = $remainder - ($mins*MINSECS);
1747 $ss = ($secs == 1) ? $str->sec : $str->secs;
1748 $sm = ($mins == 1) ? $str->min : $str->mins;
1749 $sh = ($hours == 1) ? $str->hour : $str->hours;
1750 $sd = ($days == 1) ? $str->day : $str->days;
1751 $sy = ($years == 1) ? $str->year : $str->years;
1753 $oyears = '';
1754 $odays = '';
1755 $ohours = '';
1756 $omins = '';
1757 $osecs = '';
1759 if ($years) {
1760 $oyears = $years .' '. $sy;
1762 if ($days) {
1763 $odays = $days .' '. $sd;
1765 if ($hours) {
1766 $ohours = $hours .' '. $sh;
1768 if ($mins) {
1769 $omins = $mins .' '. $sm;
1771 if ($secs) {
1772 $osecs = $secs .' '. $ss;
1775 if ($years) {
1776 return trim($oyears .' '. $odays);
1778 if ($days) {
1779 return trim($odays .' '. $ohours);
1781 if ($hours) {
1782 return trim($ohours .' '. $omins);
1784 if ($mins) {
1785 return trim($omins .' '. $osecs);
1787 if ($secs) {
1788 return $osecs;
1790 return get_string('now');
1794 * Returns a formatted string that represents a date in user time.
1796 * @package core
1797 * @category time
1798 * @param int $date the timestamp in UTC, as obtained from the database.
1799 * @param string $format strftime format. You should probably get this using
1800 * get_string('strftime...', 'langconfig');
1801 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
1802 * not 99 then daylight saving will not be added.
1803 * {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
1804 * @param bool $fixday If true (default) then the leading zero from %d is removed.
1805 * If false then the leading zero is maintained.
1806 * @param bool $fixhour If true (default) then the leading zero from %I is removed.
1807 * @return string the formatted date/time.
1809 function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
1810 $calendartype = \core_calendar\type_factory::get_calendar_instance();
1811 return $calendartype->timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour);
1815 * Returns a html "time" tag with both the exact user date with timezone information
1816 * as a datetime attribute in the W3C format, and the user readable date and time as text.
1818 * @package core
1819 * @category time
1820 * @param int $date the timestamp in UTC, as obtained from the database.
1821 * @param string $format strftime format. You should probably get this using
1822 * get_string('strftime...', 'langconfig');
1823 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
1824 * not 99 then daylight saving will not be added.
1825 * {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
1826 * @param bool $fixday If true (default) then the leading zero from %d is removed.
1827 * If false then the leading zero is maintained.
1828 * @param bool $fixhour If true (default) then the leading zero from %I is removed.
1829 * @return string the formatted date/time.
1831 function userdate_htmltime($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
1832 $userdatestr = userdate($date, $format, $timezone, $fixday, $fixhour);
1833 if (CLI_SCRIPT && !PHPUNIT_TEST) {
1834 return $userdatestr;
1836 $machinedate = new DateTime();
1837 $machinedate->setTimestamp(intval($date));
1838 $machinedate->setTimezone(core_date::get_user_timezone_object());
1840 return html_writer::tag('time', $userdatestr, ['datetime' => $machinedate->format(DateTime::W3C)]);
1844 * Returns a formatted date ensuring it is UTF-8.
1846 * If we are running under Windows convert to Windows encoding and then back to UTF-8
1847 * (because it's impossible to specify UTF-8 to fetch locale info in Win32).
1849 * @param int $date the timestamp - since Moodle 2.9 this is a real UTC timestamp
1850 * @param string $format strftime format.
1851 * @param int|float|string $tz the user timezone
1852 * @return string the formatted date/time.
1853 * @since Moodle 2.3.3
1855 function date_format_string($date, $format, $tz = 99) {
1857 date_default_timezone_set(core_date::get_user_timezone($tz));
1859 if (date('A', 0) === date('A', HOURSECS * 18)) {
1860 $datearray = getdate($date);
1861 $format = str_replace([
1862 '%P',
1863 '%p',
1864 ], [
1865 $datearray['hours'] < 12 ? get_string('am', 'langconfig') : get_string('pm', 'langconfig'),
1866 $datearray['hours'] < 12 ? get_string('amcaps', 'langconfig') : get_string('pmcaps', 'langconfig'),
1867 ], $format);
1870 $datestring = core_date::strftime($format, $date);
1871 core_date::set_default_server_timezone();
1873 return $datestring;
1877 * Given a $time timestamp in GMT (seconds since epoch),
1878 * returns an array that represents the Gregorian date in user time
1880 * @package core
1881 * @category time
1882 * @param int $time Timestamp in GMT
1883 * @param float|int|string $timezone user timezone
1884 * @return array An array that represents the date in user time
1886 function usergetdate($time, $timezone=99) {
1887 if ($time === null) {
1888 // PHP8 and PHP7 return different results when getdate(null) is called.
1889 // Display warning and cast to 0 to make sure the usergetdate() behaves consistently on all versions of PHP.
1890 // In the future versions of Moodle we may consider adding a strict typehint.
1891 debugging('usergetdate() expects parameter $time to be int, null given', DEBUG_DEVELOPER);
1892 $time = 0;
1895 date_default_timezone_set(core_date::get_user_timezone($timezone));
1896 $result = getdate($time);
1897 core_date::set_default_server_timezone();
1899 return $result;
1903 * Given a GMT timestamp (seconds since epoch), offsets it by
1904 * the timezone. eg 3pm in India is 3pm GMT - 7 * 3600 seconds
1906 * NOTE: this function does not include DST properly,
1907 * you should use the PHP date stuff instead!
1909 * @package core
1910 * @category time
1911 * @param int $date Timestamp in GMT
1912 * @param float|int|string $timezone user timezone
1913 * @return int
1915 function usertime($date, $timezone=99) {
1916 $userdate = new DateTime('@' . $date);
1917 $userdate->setTimezone(core_date::get_user_timezone_object($timezone));
1918 $dst = dst_offset_on($date, $timezone);
1920 return $date - $userdate->getOffset() + $dst;
1924 * Get a formatted string representation of an interval between two unix timestamps.
1926 * E.g.
1927 * $intervalstring = get_time_interval_string(12345600, 12345660);
1928 * Will produce the string:
1929 * '0d 0h 1m'
1931 * @param int $time1 unix timestamp
1932 * @param int $time2 unix timestamp
1933 * @param string $format string (can be lang string) containing format chars: https://www.php.net/manual/en/dateinterval.format.php.
1934 * @param bool $dropzeroes If format is not provided and this is set to true, do not include zero time units.
1935 * e.g. a duration of 3 days and 2 hours will be displayed as '3d 2h' instead of '3d 2h 0s'
1936 * @param bool $fullformat If format is not provided and this is set to true, display time units in full format.
1937 * e.g. instead of showing "3d", "3 days" will be returned.
1938 * @return string the formatted string describing the time difference, e.g. '10d 11h 45m'.
1940 function get_time_interval_string(int $time1, int $time2, string $format = '',
1941 bool $dropzeroes = false, bool $fullformat = false): string {
1942 $dtdate = new DateTime();
1943 $dtdate->setTimeStamp($time1);
1944 $dtdate2 = new DateTime();
1945 $dtdate2->setTimeStamp($time2);
1946 $interval = $dtdate2->diff($dtdate);
1948 if (empty(trim($format))) {
1949 // Default to this key.
1950 $formatkey = 'dateintervaldayhrmin';
1952 if ($dropzeroes) {
1953 $units = [
1954 'y' => 'yr',
1955 'm' => 'mo',
1956 'd' => 'day',
1957 'h' => 'hr',
1958 'i' => 'min',
1959 's' => 'sec',
1961 $formatunits = [];
1962 foreach ($units as $key => $unit) {
1963 if (empty($interval->$key)) {
1964 continue;
1966 $formatunits[] = $unit;
1968 if (!empty($formatunits)) {
1969 $formatkey = 'dateinterval' . implode("", $formatunits);
1973 if ($fullformat) {
1974 $formatkey .= 'full';
1976 $format = get_string($formatkey, 'langconfig');
1978 return $interval->format($format);
1982 * Given a time, return the GMT timestamp of the most recent midnight
1983 * for the current user.
1985 * @package core
1986 * @category time
1987 * @param int $date Timestamp in GMT
1988 * @param float|int|string $timezone user timezone
1989 * @return int Returns a GMT timestamp
1991 function usergetmidnight($date, $timezone=99) {
1993 $userdate = usergetdate($date, $timezone);
1995 // Time of midnight of this user's day, in GMT.
1996 return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone);
2001 * Returns a string that prints the user's timezone
2003 * @package core
2004 * @category time
2005 * @param float|int|string $timezone user timezone
2006 * @return string
2008 function usertimezone($timezone=99) {
2009 $tz = core_date::get_user_timezone($timezone);
2010 return core_date::get_localised_timezone($tz);
2014 * Returns a float or a string which denotes the user's timezone
2015 * 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)
2016 * means that for this timezone there are also DST rules to be taken into account
2017 * Checks various settings and picks the most dominant of those which have a value
2019 * @package core
2020 * @category time
2021 * @param float|int|string $tz timezone to calculate GMT time offset before
2022 * calculating user timezone, 99 is default user timezone
2023 * {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
2024 * @return float|string
2026 function get_user_timezone($tz = 99) {
2027 global $USER, $CFG;
2029 $timezones = array(
2030 $tz,
2031 isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99,
2032 isset($USER->timezone) ? $USER->timezone : 99,
2033 isset($CFG->timezone) ? $CFG->timezone : 99,
2036 $tz = 99;
2038 // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array.
2039 foreach ($timezones as $nextvalue) {
2040 if ((empty($tz) && !is_numeric($tz)) || $tz == 99) {
2041 $tz = $nextvalue;
2044 return is_numeric($tz) ? (float) $tz : $tz;
2048 * Calculates the Daylight Saving Offset for a given date/time (timestamp)
2049 * - Note: Daylight saving only works for string timezones and not for float.
2051 * @package core
2052 * @category time
2053 * @param int $time must NOT be compensated at all, it has to be a pure timestamp
2054 * @param int|float|string $strtimezone user timezone
2055 * @return int
2057 function dst_offset_on($time, $strtimezone = null) {
2058 $tz = core_date::get_user_timezone($strtimezone);
2059 $date = new DateTime('@' . $time);
2060 $date->setTimezone(new DateTimeZone($tz));
2061 if ($date->format('I') == '1') {
2062 if ($tz === 'Australia/Lord_Howe') {
2063 return 1800;
2065 return 3600;
2067 return 0;
2071 * Calculates when the day appears in specific month
2073 * @package core
2074 * @category time
2075 * @param int $startday starting day of the month
2076 * @param int $weekday The day when week starts (normally taken from user preferences)
2077 * @param int $month The month whose day is sought
2078 * @param int $year The year of the month whose day is sought
2079 * @return int
2081 function find_day_in_month($startday, $weekday, $month, $year) {
2082 $calendartype = \core_calendar\type_factory::get_calendar_instance();
2084 $daysinmonth = days_in_month($month, $year);
2085 $daysinweek = count($calendartype->get_weekdays());
2087 if ($weekday == -1) {
2088 // Don't care about weekday, so return:
2089 // abs($startday) if $startday != -1
2090 // $daysinmonth otherwise.
2091 return ($startday == -1) ? $daysinmonth : abs($startday);
2094 // From now on we 're looking for a specific weekday.
2095 // Give "end of month" its actual value, since we know it.
2096 if ($startday == -1) {
2097 $startday = -1 * $daysinmonth;
2100 // Starting from day $startday, the sign is the direction.
2101 if ($startday < 1) {
2102 $startday = abs($startday);
2103 $lastmonthweekday = dayofweek($daysinmonth, $month, $year);
2105 // This is the last such weekday of the month.
2106 $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday;
2107 if ($lastinmonth > $daysinmonth) {
2108 $lastinmonth -= $daysinweek;
2111 // Find the first such weekday <= $startday.
2112 while ($lastinmonth > $startday) {
2113 $lastinmonth -= $daysinweek;
2116 return $lastinmonth;
2117 } else {
2118 $indexweekday = dayofweek($startday, $month, $year);
2120 $diff = $weekday - $indexweekday;
2121 if ($diff < 0) {
2122 $diff += $daysinweek;
2125 // This is the first such weekday of the month equal to or after $startday.
2126 $firstfromindex = $startday + $diff;
2128 return $firstfromindex;
2133 * Calculate the number of days in a given month
2135 * @package core
2136 * @category time
2137 * @param int $month The month whose day count is sought
2138 * @param int $year The year of the month whose day count is sought
2139 * @return int
2141 function days_in_month($month, $year) {
2142 $calendartype = \core_calendar\type_factory::get_calendar_instance();
2143 return $calendartype->get_num_days_in_month($year, $month);
2147 * Calculate the position in the week of a specific calendar day
2149 * @package core
2150 * @category time
2151 * @param int $day The day of the date whose position in the week is sought
2152 * @param int $month The month of the date whose position in the week is sought
2153 * @param int $year The year of the date whose position in the week is sought
2154 * @return int
2156 function dayofweek($day, $month, $year) {
2157 $calendartype = \core_calendar\type_factory::get_calendar_instance();
2158 return $calendartype->get_weekday($year, $month, $day);
2161 // USER AUTHENTICATION AND LOGIN.
2164 * Returns full login url.
2166 * Any form submissions for authentication to this URL must include username,
2167 * password as well as a logintoken generated by \core\session\manager::get_login_token().
2169 * @return string login url
2171 function get_login_url() {
2172 global $CFG;
2174 return "$CFG->wwwroot/login/index.php";
2178 * This function checks that the current user is logged in and has the
2179 * required privileges
2181 * This function checks that the current user is logged in, and optionally
2182 * whether they are allowed to be in a particular course and view a particular
2183 * course module.
2184 * If they are not logged in, then it redirects them to the site login unless
2185 * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which
2186 * case they are automatically logged in as guests.
2187 * If $courseid is given and the user is not enrolled in that course then the
2188 * user is redirected to the course enrolment page.
2189 * If $cm is given and the course module is hidden and the user is not a teacher
2190 * in the course then the user is redirected to the course home page.
2192 * When $cm parameter specified, this function sets page layout to 'module'.
2193 * You need to change it manually later if some other layout needed.
2195 * @package core_access
2196 * @category access
2198 * @param mixed $courseorid id of the course or course object
2199 * @param bool $autologinguest default true
2200 * @param object $cm course module object
2201 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2202 * true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2203 * in order to keep redirects working properly. MDL-14495
2204 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2205 * @return mixed Void, exit, and die depending on path
2206 * @throws coding_exception
2207 * @throws require_login_exception
2208 * @throws moodle_exception
2210 function require_login($courseorid = null, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
2211 global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT;
2213 // Must not redirect when byteserving already started.
2214 if (!empty($_SERVER['HTTP_RANGE'])) {
2215 $preventredirect = true;
2218 if (AJAX_SCRIPT) {
2219 // We cannot redirect for AJAX scripts either.
2220 $preventredirect = true;
2223 // Setup global $COURSE, themes, language and locale.
2224 if (!empty($courseorid)) {
2225 if (is_object($courseorid)) {
2226 $course = $courseorid;
2227 } else if ($courseorid == SITEID) {
2228 $course = clone($SITE);
2229 } else {
2230 $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST);
2232 if ($cm) {
2233 if ($cm->course != $course->id) {
2234 throw new coding_exception('course and cm parameters in require_login() call do not match!!');
2236 // Make sure we have a $cm from get_fast_modinfo as this contains activity access details.
2237 if (!($cm instanceof cm_info)) {
2238 // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
2239 // db queries so this is not really a performance concern, however it is obviously
2240 // better if you use get_fast_modinfo to get the cm before calling this.
2241 $modinfo = get_fast_modinfo($course);
2242 $cm = $modinfo->get_cm($cm->id);
2245 } else {
2246 // Do not touch global $COURSE via $PAGE->set_course(),
2247 // the reasons is we need to be able to call require_login() at any time!!
2248 $course = $SITE;
2249 if ($cm) {
2250 throw new coding_exception('cm parameter in require_login() requires valid course parameter!');
2254 // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false.
2255 // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future
2256 // risk leading the user back to the AJAX request URL.
2257 if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) {
2258 $setwantsurltome = false;
2261 // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour.
2262 if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !empty($CFG->dbsessions)) {
2263 if ($preventredirect) {
2264 throw new require_login_session_timeout_exception();
2265 } else {
2266 if ($setwantsurltome) {
2267 $SESSION->wantsurl = qualified_me();
2269 redirect(get_login_url());
2273 // If the user is not even logged in yet then make sure they are.
2274 if (!isloggedin()) {
2275 if ($autologinguest && !empty($CFG->autologinguests)) {
2276 if (!$guest = get_complete_user_data('id', $CFG->siteguest)) {
2277 // Misconfigured site guest, just redirect to login page.
2278 redirect(get_login_url());
2279 exit; // Never reached.
2281 $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang;
2282 complete_user_login($guest);
2283 $USER->autologinguest = true;
2284 $SESSION->lang = $lang;
2285 } else {
2286 // NOTE: $USER->site check was obsoleted by session test cookie, $USER->confirmed test is in login/index.php.
2287 if ($preventredirect) {
2288 throw new require_login_exception('You are not logged in');
2291 if ($setwantsurltome) {
2292 $SESSION->wantsurl = qualified_me();
2295 // Give auth plugins an opportunity to authenticate or redirect to an external login page
2296 $authsequence = get_enabled_auth_plugins(); // Auths, in sequence.
2297 foreach($authsequence as $authname) {
2298 $authplugin = get_auth_plugin($authname);
2299 $authplugin->pre_loginpage_hook();
2300 if (isloggedin()) {
2301 if ($cm) {
2302 $modinfo = get_fast_modinfo($course);
2303 $cm = $modinfo->get_cm($cm->id);
2305 set_access_log_user();
2306 break;
2310 // If we're still not logged in then go to the login page
2311 if (!isloggedin()) {
2312 redirect(get_login_url());
2313 exit; // Never reached.
2318 // Loginas as redirection if needed.
2319 if ($course->id != SITEID and \core\session\manager::is_loggedinas()) {
2320 if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) {
2321 if ($USER->loginascontext->instanceid != $course->id) {
2322 throw new \moodle_exception('loginasonecourse', '',
2323 $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid);
2328 // Check whether the user should be changing password (but only if it is REALLY them).
2329 if (get_user_preferences('auth_forcepasswordchange') && !\core\session\manager::is_loggedinas()) {
2330 $userauth = get_auth_plugin($USER->auth);
2331 if ($userauth->can_change_password() and !$preventredirect) {
2332 if ($setwantsurltome) {
2333 $SESSION->wantsurl = qualified_me();
2335 if ($changeurl = $userauth->change_password_url()) {
2336 // Use plugin custom url.
2337 redirect($changeurl);
2338 } else {
2339 // Use moodle internal method.
2340 redirect($CFG->wwwroot .'/login/change_password.php');
2342 } else if ($userauth->can_change_password()) {
2343 throw new moodle_exception('forcepasswordchangenotice');
2344 } else {
2345 throw new moodle_exception('nopasswordchangeforced', 'auth');
2349 // Check that the user account is properly set up. If we can't redirect to
2350 // edit their profile and this is not a WS request, perform just the lax check.
2351 // It will allow them to use filepicker on the profile edit page.
2353 if ($preventredirect && !WS_SERVER) {
2354 $usernotfullysetup = user_not_fully_set_up($USER, false);
2355 } else {
2356 $usernotfullysetup = user_not_fully_set_up($USER, true);
2359 if ($usernotfullysetup) {
2360 if ($preventredirect) {
2361 throw new moodle_exception('usernotfullysetup');
2363 if ($setwantsurltome) {
2364 $SESSION->wantsurl = qualified_me();
2366 redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&amp;course='. SITEID);
2369 // Make sure the USER has a sesskey set up. Used for CSRF protection.
2370 sesskey();
2372 if (\core\session\manager::is_loggedinas()) {
2373 // During a "logged in as" session we should force all content to be cleaned because the
2374 // logged in user will be viewing potentially malicious user generated content.
2375 // See MDL-63786 for more details.
2376 $CFG->forceclean = true;
2379 $afterlogins = get_plugins_with_function('after_require_login', 'lib.php');
2381 // Do not bother admins with any formalities, except for activities pending deletion.
2382 if (is_siteadmin() && !($cm && $cm->deletioninprogress)) {
2383 // Set the global $COURSE.
2384 if ($cm) {
2385 $PAGE->set_cm($cm, $course);
2386 $PAGE->set_pagelayout('incourse');
2387 } else if (!empty($courseorid)) {
2388 $PAGE->set_course($course);
2390 // Set accesstime or the user will appear offline which messes up messaging.
2391 // Do not update access time for webservice or ajax requests.
2392 if (!WS_SERVER && !AJAX_SCRIPT) {
2393 user_accesstime_log($course->id);
2396 foreach ($afterlogins as $plugintype => $plugins) {
2397 foreach ($plugins as $pluginfunction) {
2398 $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2401 return;
2404 // Scripts have a chance to declare that $USER->policyagreed should not be checked.
2405 // This is mostly for places where users are actually accepting the policies, to avoid the redirect loop.
2406 if (!defined('NO_SITEPOLICY_CHECK')) {
2407 define('NO_SITEPOLICY_CHECK', false);
2410 // Check that the user has agreed to a site policy if there is one - do not test in case of admins.
2411 // Do not test if the script explicitly asked for skipping the site policies check.
2412 // Or if the user auth type is webservice.
2413 if (!$USER->policyagreed && !is_siteadmin() && !NO_SITEPOLICY_CHECK && $USER->auth !== 'webservice') {
2414 $manager = new \core_privacy\local\sitepolicy\manager();
2415 if ($policyurl = $manager->get_redirect_url(isguestuser())) {
2416 if ($preventredirect) {
2417 throw new moodle_exception('sitepolicynotagreed', 'error', '', $policyurl->out());
2419 if ($setwantsurltome) {
2420 $SESSION->wantsurl = qualified_me();
2422 redirect($policyurl);
2426 // Fetch the system context, the course context, and prefetch its child contexts.
2427 $sysctx = context_system::instance();
2428 $coursecontext = context_course::instance($course->id, MUST_EXIST);
2429 if ($cm) {
2430 $cmcontext = context_module::instance($cm->id, MUST_EXIST);
2431 } else {
2432 $cmcontext = null;
2435 // If the site is currently under maintenance, then print a message.
2436 if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:maintenanceaccess', $sysctx)) {
2437 if ($preventredirect) {
2438 throw new require_login_exception('Maintenance in progress');
2440 $PAGE->set_context(null);
2441 print_maintenance_message();
2444 // Make sure the course itself is not hidden.
2445 if ($course->id == SITEID) {
2446 // Frontpage can not be hidden.
2447 } else {
2448 if (is_role_switched($course->id)) {
2449 // When switching roles ignore the hidden flag - user had to be in course to do the switch.
2450 } else {
2451 if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
2452 // Originally there was also test of parent category visibility, BUT is was very slow in complex queries
2453 // involving "my courses" now it is also possible to simply hide all courses user is not enrolled in :-).
2454 if ($preventredirect) {
2455 throw new require_login_exception('Course is hidden');
2457 $PAGE->set_context(null);
2458 // We need to override the navigation URL as the course won't have been added to the navigation and thus
2459 // the navigation will mess up when trying to find it.
2460 navigation_node::override_active_url(new moodle_url('/'));
2461 notice(get_string('coursehidden'), $CFG->wwwroot .'/');
2466 // Is the user enrolled?
2467 if ($course->id == SITEID) {
2468 // Everybody is enrolled on the frontpage.
2469 } else {
2470 if (\core\session\manager::is_loggedinas()) {
2471 // Make sure the REAL person can access this course first.
2472 $realuser = \core\session\manager::get_realuser();
2473 if (!is_enrolled($coursecontext, $realuser->id, '', true) and
2474 !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) {
2475 if ($preventredirect) {
2476 throw new require_login_exception('Invalid course login-as access');
2478 $PAGE->set_context(null);
2479 echo $OUTPUT->header();
2480 notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/');
2484 $access = false;
2486 if (is_role_switched($course->id)) {
2487 // Ok, user had to be inside this course before the switch.
2488 $access = true;
2490 } else if (is_viewing($coursecontext, $USER)) {
2491 // Ok, no need to mess with enrol.
2492 $access = true;
2494 } else {
2495 if (isset($USER->enrol['enrolled'][$course->id])) {
2496 if ($USER->enrol['enrolled'][$course->id] > time()) {
2497 $access = true;
2498 if (isset($USER->enrol['tempguest'][$course->id])) {
2499 unset($USER->enrol['tempguest'][$course->id]);
2500 remove_temp_course_roles($coursecontext);
2502 } else {
2503 // Expired.
2504 unset($USER->enrol['enrolled'][$course->id]);
2507 if (isset($USER->enrol['tempguest'][$course->id])) {
2508 if ($USER->enrol['tempguest'][$course->id] == 0) {
2509 $access = true;
2510 } else if ($USER->enrol['tempguest'][$course->id] > time()) {
2511 $access = true;
2512 } else {
2513 // Expired.
2514 unset($USER->enrol['tempguest'][$course->id]);
2515 remove_temp_course_roles($coursecontext);
2519 if (!$access) {
2520 // Cache not ok.
2521 $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id);
2522 if ($until !== false) {
2523 // Active participants may always access, a timestamp in the future, 0 (always) or false.
2524 if ($until == 0) {
2525 $until = ENROL_MAX_TIMESTAMP;
2527 $USER->enrol['enrolled'][$course->id] = $until;
2528 $access = true;
2530 } else if (core_course_category::can_view_course_info($course)) {
2531 $params = array('courseid' => $course->id, 'status' => ENROL_INSTANCE_ENABLED);
2532 $instances = $DB->get_records('enrol', $params, 'sortorder, id ASC');
2533 $enrols = enrol_get_plugins(true);
2534 // First ask all enabled enrol instances in course if they want to auto enrol user.
2535 foreach ($instances as $instance) {
2536 if (!isset($enrols[$instance->enrol])) {
2537 continue;
2539 // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false.
2540 $until = $enrols[$instance->enrol]->try_autoenrol($instance);
2541 if ($until !== false) {
2542 if ($until == 0) {
2543 $until = ENROL_MAX_TIMESTAMP;
2545 $USER->enrol['enrolled'][$course->id] = $until;
2546 $access = true;
2547 break;
2550 // If not enrolled yet try to gain temporary guest access.
2551 if (!$access) {
2552 foreach ($instances as $instance) {
2553 if (!isset($enrols[$instance->enrol])) {
2554 continue;
2556 // Get a duration for the guest access, a timestamp in the future or false.
2557 $until = $enrols[$instance->enrol]->try_guestaccess($instance);
2558 if ($until !== false and $until > time()) {
2559 $USER->enrol['tempguest'][$course->id] = $until;
2560 $access = true;
2561 break;
2565 } else {
2566 // User is not enrolled and is not allowed to browse courses here.
2567 if ($preventredirect) {
2568 throw new require_login_exception('Course is not available');
2570 $PAGE->set_context(null);
2571 // We need to override the navigation URL as the course won't have been added to the navigation and thus
2572 // the navigation will mess up when trying to find it.
2573 navigation_node::override_active_url(new moodle_url('/'));
2574 notice(get_string('coursehidden'), $CFG->wwwroot .'/');
2579 if (!$access) {
2580 if ($preventredirect) {
2581 throw new require_login_exception('Not enrolled');
2583 if ($setwantsurltome) {
2584 $SESSION->wantsurl = qualified_me();
2586 redirect($CFG->wwwroot .'/enrol/index.php?id='. $course->id);
2590 // Check whether the activity has been scheduled for deletion. If so, then deny access, even for admins.
2591 if ($cm && $cm->deletioninprogress) {
2592 if ($preventredirect) {
2593 throw new moodle_exception('activityisscheduledfordeletion');
2595 require_once($CFG->dirroot . '/course/lib.php');
2596 redirect(course_get_url($course), get_string('activityisscheduledfordeletion', 'error'));
2599 // Check visibility of activity to current user; includes visible flag, conditional availability, etc.
2600 if ($cm && !$cm->uservisible) {
2601 if ($preventredirect) {
2602 throw new require_login_exception('Activity is hidden');
2604 // Get the error message that activity is not available and why (if explanation can be shown to the user).
2605 $PAGE->set_course($course);
2606 $renderer = $PAGE->get_renderer('course');
2607 $message = $renderer->course_section_cm_unavailable_error_message($cm);
2608 redirect(course_get_url($course), $message, null, \core\output\notification::NOTIFY_ERROR);
2611 // Set the global $COURSE.
2612 if ($cm) {
2613 $PAGE->set_cm($cm, $course);
2614 $PAGE->set_pagelayout('incourse');
2615 } else if (!empty($courseorid)) {
2616 $PAGE->set_course($course);
2619 foreach ($afterlogins as $plugintype => $plugins) {
2620 foreach ($plugins as $pluginfunction) {
2621 $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2625 // Finally access granted, update lastaccess times.
2626 // Do not update access time for webservice or ajax requests.
2627 if (!WS_SERVER && !AJAX_SCRIPT) {
2628 user_accesstime_log($course->id);
2633 * A convenience function for where we must be logged in as admin
2634 * @return void
2636 function require_admin() {
2637 require_login(null, false);
2638 require_capability('moodle/site:config', context_system::instance());
2642 * This function just makes sure a user is logged out.
2644 * @package core_access
2645 * @category access
2647 function require_logout() {
2648 global $USER, $DB;
2650 if (!isloggedin()) {
2651 // This should not happen often, no need for hooks or events here.
2652 \core\session\manager::terminate_current();
2653 return;
2656 // Execute hooks before action.
2657 $authplugins = array();
2658 $authsequence = get_enabled_auth_plugins();
2659 foreach ($authsequence as $authname) {
2660 $authplugins[$authname] = get_auth_plugin($authname);
2661 $authplugins[$authname]->prelogout_hook();
2664 // Store info that gets removed during logout.
2665 $sid = session_id();
2666 $event = \core\event\user_loggedout::create(
2667 array(
2668 'userid' => $USER->id,
2669 'objectid' => $USER->id,
2670 'other' => array('sessionid' => $sid),
2673 if ($session = $DB->get_record('sessions', array('sid'=>$sid))) {
2674 $event->add_record_snapshot('sessions', $session);
2677 // Clone of $USER object to be used by auth plugins.
2678 $user = fullclone($USER);
2680 // Delete session record and drop $_SESSION content.
2681 \core\session\manager::terminate_current();
2683 // Trigger event AFTER action.
2684 $event->trigger();
2686 // Hook to execute auth plugins redirection after event trigger.
2687 foreach ($authplugins as $authplugin) {
2688 $authplugin->postlogout_hook($user);
2693 * Weaker version of require_login()
2695 * This is a weaker version of {@link require_login()} which only requires login
2696 * when called from within a course rather than the site page, unless
2697 * the forcelogin option is turned on.
2698 * @see require_login()
2700 * @package core_access
2701 * @category access
2703 * @param mixed $courseorid The course object or id in question
2704 * @param bool $autologinguest Allow autologin guests if that is wanted
2705 * @param object $cm Course activity module if known
2706 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2707 * true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2708 * in order to keep redirects working properly. MDL-14495
2709 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2710 * @return void
2711 * @throws coding_exception
2713 function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
2714 global $CFG, $PAGE, $SITE;
2715 $issite = ((is_object($courseorid) and $courseorid->id == SITEID)
2716 or (!is_object($courseorid) and $courseorid == SITEID));
2717 if ($issite && !empty($cm) && !($cm instanceof cm_info)) {
2718 // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
2719 // db queries so this is not really a performance concern, however it is obviously
2720 // better if you use get_fast_modinfo to get the cm before calling this.
2721 if (is_object($courseorid)) {
2722 $course = $courseorid;
2723 } else {
2724 $course = clone($SITE);
2726 $modinfo = get_fast_modinfo($course);
2727 $cm = $modinfo->get_cm($cm->id);
2729 if (!empty($CFG->forcelogin)) {
2730 // Login required for both SITE and courses.
2731 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2733 } else if ($issite && !empty($cm) and !$cm->uservisible) {
2734 // Always login for hidden activities.
2735 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2737 } else if (isloggedin() && !isguestuser()) {
2738 // User is already logged in. Make sure the login is complete (user is fully setup, policies agreed).
2739 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2741 } else if ($issite) {
2742 // Login for SITE not required.
2743 // We still need to instatiate PAGE vars properly so that things that rely on it like navigation function correctly.
2744 if (!empty($courseorid)) {
2745 if (is_object($courseorid)) {
2746 $course = $courseorid;
2747 } else {
2748 $course = clone $SITE;
2750 if ($cm) {
2751 if ($cm->course != $course->id) {
2752 throw new coding_exception('course and cm parameters in require_course_login() call do not match!!');
2754 $PAGE->set_cm($cm, $course);
2755 $PAGE->set_pagelayout('incourse');
2756 } else {
2757 $PAGE->set_course($course);
2759 } else {
2760 // If $PAGE->course, and hence $PAGE->context, have not already been set up properly, set them up now.
2761 $PAGE->set_course($PAGE->course);
2763 // Do not update access time for webservice or ajax requests.
2764 if (!WS_SERVER && !AJAX_SCRIPT) {
2765 user_accesstime_log(SITEID);
2767 return;
2769 } else {
2770 // Course login always required.
2771 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2776 * Validates a user key, checking if the key exists, is not expired and the remote ip is correct.
2778 * @param string $keyvalue the key value
2779 * @param string $script unique script identifier
2780 * @param int $instance instance id
2781 * @return stdClass the key entry in the user_private_key table
2782 * @since Moodle 3.2
2783 * @throws moodle_exception
2785 function validate_user_key($keyvalue, $script, $instance) {
2786 global $DB;
2788 if (!$key = $DB->get_record('user_private_key', array('script' => $script, 'value' => $keyvalue, 'instance' => $instance))) {
2789 throw new \moodle_exception('invalidkey');
2792 if (!empty($key->validuntil) and $key->validuntil < time()) {
2793 throw new \moodle_exception('expiredkey');
2796 if ($key->iprestriction) {
2797 $remoteaddr = getremoteaddr(null);
2798 if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) {
2799 throw new \moodle_exception('ipmismatch');
2802 return $key;
2806 * Require key login. Function terminates with error if key not found or incorrect.
2808 * @uses NO_MOODLE_COOKIES
2809 * @uses PARAM_ALPHANUM
2810 * @param string $script unique script identifier
2811 * @param int $instance optional instance id
2812 * @param string $keyvalue The key. If not supplied, this will be fetched from the current session.
2813 * @return int Instance ID
2815 function require_user_key_login($script, $instance = null, $keyvalue = null) {
2816 global $DB;
2818 if (!NO_MOODLE_COOKIES) {
2819 throw new \moodle_exception('sessioncookiesdisable');
2822 // Extra safety.
2823 \core\session\manager::write_close();
2825 if (null === $keyvalue) {
2826 $keyvalue = required_param('key', PARAM_ALPHANUM);
2829 $key = validate_user_key($keyvalue, $script, $instance);
2831 if (!$user = $DB->get_record('user', array('id' => $key->userid))) {
2832 throw new \moodle_exception('invaliduserid');
2835 core_user::require_active_user($user, true, true);
2837 // Emulate normal session.
2838 enrol_check_plugins($user, false);
2839 \core\session\manager::set_user($user);
2841 // Note we are not using normal login.
2842 if (!defined('USER_KEY_LOGIN')) {
2843 define('USER_KEY_LOGIN', true);
2846 // Return instance id - it might be empty.
2847 return $key->instance;
2851 * Creates a new private user access key.
2853 * @param string $script unique target identifier
2854 * @param int $userid
2855 * @param int $instance optional instance id
2856 * @param string $iprestriction optional ip restricted access
2857 * @param int $validuntil key valid only until given data
2858 * @return string access key value
2860 function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
2861 global $DB;
2863 $key = new stdClass();
2864 $key->script = $script;
2865 $key->userid = $userid;
2866 $key->instance = $instance;
2867 $key->iprestriction = $iprestriction;
2868 $key->validuntil = $validuntil;
2869 $key->timecreated = time();
2871 // Something long and unique.
2872 $key->value = md5($userid.'_'.time().random_string(40));
2873 while ($DB->record_exists('user_private_key', array('value' => $key->value))) {
2874 // Must be unique.
2875 $key->value = md5($userid.'_'.time().random_string(40));
2877 $DB->insert_record('user_private_key', $key);
2878 return $key->value;
2882 * Delete the user's new private user access keys for a particular script.
2884 * @param string $script unique target identifier
2885 * @param int $userid
2886 * @return void
2888 function delete_user_key($script, $userid) {
2889 global $DB;
2890 $DB->delete_records('user_private_key', array('script' => $script, 'userid' => $userid));
2894 * Gets a private user access key (and creates one if one doesn't exist).
2896 * @param string $script unique target identifier
2897 * @param int $userid
2898 * @param int $instance optional instance id
2899 * @param string $iprestriction optional ip restricted access
2900 * @param int $validuntil key valid only until given date
2901 * @return string access key value
2903 function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
2904 global $DB;
2906 if ($key = $DB->get_record('user_private_key', array('script' => $script, 'userid' => $userid,
2907 'instance' => $instance, 'iprestriction' => $iprestriction,
2908 'validuntil' => $validuntil))) {
2909 return $key->value;
2910 } else {
2911 return create_user_key($script, $userid, $instance, $iprestriction, $validuntil);
2917 * Modify the user table by setting the currently logged in user's last login to now.
2919 * @return bool Always returns true
2921 function update_user_login_times() {
2922 global $USER, $DB, $SESSION;
2924 if (isguestuser()) {
2925 // Do not update guest access times/ips for performance.
2926 return true;
2929 if (defined('USER_KEY_LOGIN') && USER_KEY_LOGIN === true) {
2930 // Do not update user login time when using user key login.
2931 return true;
2934 $now = time();
2936 $user = new stdClass();
2937 $user->id = $USER->id;
2939 // Make sure all users that logged in have some firstaccess.
2940 if ($USER->firstaccess == 0) {
2941 $USER->firstaccess = $user->firstaccess = $now;
2944 // Store the previous current as lastlogin.
2945 $USER->lastlogin = $user->lastlogin = $USER->currentlogin;
2947 $USER->currentlogin = $user->currentlogin = $now;
2949 // Function user_accesstime_log() may not update immediately, better do it here.
2950 $USER->lastaccess = $user->lastaccess = $now;
2951 $SESSION->userpreviousip = $USER->lastip;
2952 $USER->lastip = $user->lastip = getremoteaddr();
2954 // Note: do not call user_update_user() here because this is part of the login process,
2955 // the login event means that these fields were updated.
2956 $DB->update_record('user', $user);
2957 return true;
2961 * Determines if a user has completed setting up their account.
2963 * The lax mode (with $strict = false) has been introduced for special cases
2964 * only where we want to skip certain checks intentionally. This is valid in
2965 * certain mnet or ajax scenarios when the user cannot / should not be
2966 * redirected to edit their profile. In most cases, you should perform the
2967 * strict check.
2969 * @param stdClass $user A {@link $USER} object to test for the existence of a valid name and email
2970 * @param bool $strict Be more strict and assert id and custom profile fields set, too
2971 * @return bool
2973 function user_not_fully_set_up($user, $strict = true) {
2974 global $CFG, $SESSION, $USER;
2975 require_once($CFG->dirroot.'/user/profile/lib.php');
2977 // If the user is setup then store this in the session to avoid re-checking.
2978 // Some edge cases are when the users email starts to bounce or the
2979 // configuration for custom fields has changed while they are logged in so
2980 // we re-check this fully every hour for the rare cases it has changed.
2981 if (isset($USER->id) && isset($user->id) && $USER->id === $user->id &&
2982 isset($SESSION->fullysetupstrict) && (time() - $SESSION->fullysetupstrict) < HOURSECS) {
2983 return false;
2986 if (isguestuser($user)) {
2987 return false;
2990 if (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user)) {
2991 return true;
2994 if ($strict) {
2995 if (empty($user->id)) {
2996 // Strict mode can be used with existing accounts only.
2997 return true;
2999 if (!profile_has_required_custom_fields_set($user->id)) {
3000 return true;
3002 if (isset($USER->id) && isset($user->id) && $USER->id === $user->id) {
3003 $SESSION->fullysetupstrict = time();
3007 return false;
3011 * Check whether the user has exceeded the bounce threshold
3013 * @param stdClass $user A {@link $USER} object
3014 * @return bool true => User has exceeded bounce threshold
3016 function over_bounce_threshold($user) {
3017 global $CFG, $DB;
3019 if (empty($CFG->handlebounces)) {
3020 return false;
3023 if (empty($user->id)) {
3024 // No real (DB) user, nothing to do here.
3025 return false;
3028 // Set sensible defaults.
3029 if (empty($CFG->minbounces)) {
3030 $CFG->minbounces = 10;
3032 if (empty($CFG->bounceratio)) {
3033 $CFG->bounceratio = .20;
3035 $bouncecount = 0;
3036 $sendcount = 0;
3037 if ($bounce = $DB->get_record('user_preferences', array ('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3038 $bouncecount = $bounce->value;
3040 if ($send = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3041 $sendcount = $send->value;
3043 return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio);
3047 * Used to increment or reset email sent count
3049 * @param stdClass $user object containing an id
3050 * @param bool $reset will reset the count to 0
3051 * @return void
3053 function set_send_count($user, $reset=false) {
3054 global $DB;
3056 if (empty($user->id)) {
3057 // No real (DB) user, nothing to do here.
3058 return;
3061 if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3062 $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3063 $DB->update_record('user_preferences', $pref);
3064 } else if (!empty($reset)) {
3065 // If it's not there and we're resetting, don't bother. Make a new one.
3066 $pref = new stdClass();
3067 $pref->name = 'email_send_count';
3068 $pref->value = 1;
3069 $pref->userid = $user->id;
3070 $DB->insert_record('user_preferences', $pref, false);
3075 * Increment or reset user's email bounce count
3077 * @param stdClass $user object containing an id
3078 * @param bool $reset will reset the count to 0
3080 function set_bounce_count($user, $reset=false) {
3081 global $DB;
3083 if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3084 $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3085 $DB->update_record('user_preferences', $pref);
3086 } else if (!empty($reset)) {
3087 // If it's not there and we're resetting, don't bother. Make a new one.
3088 $pref = new stdClass();
3089 $pref->name = 'email_bounce_count';
3090 $pref->value = 1;
3091 $pref->userid = $user->id;
3092 $DB->insert_record('user_preferences', $pref, false);
3097 * Determines if the logged in user is currently moving an activity
3099 * @param int $courseid The id of the course being tested
3100 * @return bool
3102 function ismoving($courseid) {
3103 global $USER;
3105 if (!empty($USER->activitycopy)) {
3106 return ($USER->activitycopycourse == $courseid);
3108 return false;
3112 * Returns a persons full name
3114 * Given an object containing all of the users name values, this function returns a string with the full name of the person.
3115 * The result may depend on system settings or language. 'override' will force the alternativefullnameformat to be used. In
3116 * English, fullname as well as alternativefullnameformat is set to 'firstname lastname' by default. But you could have
3117 * fullname set to 'firstname lastname' and alternativefullnameformat set to 'firstname middlename alternatename lastname'.
3119 * @param stdClass $user A {@link $USER} object to get full name of.
3120 * @param bool $override If true then the alternativefullnameformat format rather than fullnamedisplay format will be used.
3121 * @return string
3123 function fullname($user, $override=false) {
3124 // Note: We do not intend to deprecate this function any time soon as it is too widely used at this time.
3125 // Uses of it should be updated to use the new API and pass updated arguments.
3127 // Return an empty string if there is no user.
3128 if (empty($user)) {
3129 return '';
3132 $options = ['override' => $override];
3133 return core_user::get_fullname($user, null, $options);
3137 * Reduces lines of duplicated code for getting user name fields.
3139 * See also {@link user_picture::unalias()}
3141 * @param object $addtoobject Object to add user name fields to.
3142 * @param object $secondobject Object that contains user name field information.
3143 * @param string $prefix prefix to be added to all fields (including $additionalfields) e.g. authorfirstname.
3144 * @param array $additionalfields Additional fields to be matched with data in the second object.
3145 * The key can be set to the user table field name.
3146 * @return object User name fields.
3148 function username_load_fields_from_object($addtoobject, $secondobject, $prefix = null, $additionalfields = null) {
3149 $fields = [];
3150 foreach (\core_user\fields::get_name_fields() as $field) {
3151 $fields[$field] = $prefix . $field;
3153 if ($additionalfields) {
3154 // Additional fields can specify their own 'alias' such as 'id' => 'userid'. This checks to see if
3155 // the key is a number and then sets the key to the array value.
3156 foreach ($additionalfields as $key => $value) {
3157 if (is_numeric($key)) {
3158 $additionalfields[$value] = $prefix . $value;
3159 unset($additionalfields[$key]);
3160 } else {
3161 $additionalfields[$key] = $prefix . $value;
3164 $fields = array_merge($fields, $additionalfields);
3166 foreach ($fields as $key => $field) {
3167 // Important that we have all of the user name fields present in the object that we are sending back.
3168 $addtoobject->$key = '';
3169 if (isset($secondobject->$field)) {
3170 $addtoobject->$key = $secondobject->$field;
3173 return $addtoobject;
3177 * Returns an array of values in order of occurance in a provided string.
3178 * The key in the result is the character postion in the string.
3180 * @param array $values Values to be found in the string format
3181 * @param string $stringformat The string which may contain values being searched for.
3182 * @return array An array of values in order according to placement in the string format.
3184 function order_in_string($values, $stringformat) {
3185 $valuearray = array();
3186 foreach ($values as $value) {
3187 $pattern = "/$value\b/";
3188 // Using preg_match as strpos() may match values that are similar e.g. firstname and firstnamephonetic.
3189 if (preg_match($pattern, $stringformat)) {
3190 $replacement = "thing";
3191 // Replace the value with something more unique to ensure we get the right position when using strpos().
3192 $newformat = preg_replace($pattern, $replacement, $stringformat);
3193 $position = strpos($newformat, $replacement);
3194 $valuearray[$position] = $value;
3197 ksort($valuearray);
3198 return $valuearray;
3202 * Returns whether a given authentication plugin exists.
3204 * @param string $auth Form of authentication to check for. Defaults to the global setting in {@link $CFG}.
3205 * @return boolean Whether the plugin is available.
3207 function exists_auth_plugin($auth) {
3208 global $CFG;
3210 if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) {
3211 return is_readable("{$CFG->dirroot}/auth/$auth/auth.php");
3213 return false;
3217 * Checks if a given plugin is in the list of enabled authentication plugins.
3219 * @param string $auth Authentication plugin.
3220 * @return boolean Whether the plugin is enabled.
3222 function is_enabled_auth($auth) {
3223 if (empty($auth)) {
3224 return false;
3227 $enabled = get_enabled_auth_plugins();
3229 return in_array($auth, $enabled);
3233 * Returns an authentication plugin instance.
3235 * @param string $auth name of authentication plugin
3236 * @return auth_plugin_base An instance of the required authentication plugin.
3238 function get_auth_plugin($auth) {
3239 global $CFG;
3241 // Check the plugin exists first.
3242 if (! exists_auth_plugin($auth)) {
3243 throw new \moodle_exception('authpluginnotfound', 'debug', '', $auth);
3246 // Return auth plugin instance.
3247 require_once("{$CFG->dirroot}/auth/$auth/auth.php");
3248 $class = "auth_plugin_$auth";
3249 return new $class;
3253 * Returns array of active auth plugins.
3255 * @param bool $fix fix $CFG->auth if needed. Only set if logged in as admin.
3256 * @return array
3258 function get_enabled_auth_plugins($fix=false) {
3259 global $CFG;
3261 $default = array('manual', 'nologin');
3263 if (empty($CFG->auth)) {
3264 $auths = array();
3265 } else {
3266 $auths = explode(',', $CFG->auth);
3269 $auths = array_unique($auths);
3270 $oldauthconfig = implode(',', $auths);
3271 foreach ($auths as $k => $authname) {
3272 if (in_array($authname, $default)) {
3273 // The manual and nologin plugin never need to be stored.
3274 unset($auths[$k]);
3275 } else if (!exists_auth_plugin($authname)) {
3276 debugging(get_string('authpluginnotfound', 'debug', $authname));
3277 unset($auths[$k]);
3281 // Ideally only explicit interaction from a human admin should trigger a
3282 // change in auth config, see MDL-70424 for details.
3283 if ($fix) {
3284 $newconfig = implode(',', $auths);
3285 if (!isset($CFG->auth) or $newconfig != $CFG->auth) {
3286 add_to_config_log('auth', $oldauthconfig, $newconfig, 'core');
3287 set_config('auth', $newconfig);
3291 return (array_merge($default, $auths));
3295 * Returns true if an internal authentication method is being used.
3296 * if method not specified then, global default is assumed
3298 * @param string $auth Form of authentication required
3299 * @return bool
3301 function is_internal_auth($auth) {
3302 // Throws error if bad $auth.
3303 $authplugin = get_auth_plugin($auth);
3304 return $authplugin->is_internal();
3308 * Returns true if the user is a 'restored' one.
3310 * Used in the login process to inform the user and allow him/her to reset the password
3312 * @param string $username username to be checked
3313 * @return bool
3315 function is_restored_user($username) {
3316 global $CFG, $DB;
3318 return $DB->record_exists('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'password' => 'restored'));
3322 * Returns an array of user fields
3324 * @return array User field/column names
3326 function get_user_fieldnames() {
3327 global $DB;
3329 $fieldarray = $DB->get_columns('user');
3330 unset($fieldarray['id']);
3331 $fieldarray = array_keys($fieldarray);
3333 return $fieldarray;
3337 * Returns the string of the language for the new user.
3339 * @return string language for the new user
3341 function get_newuser_language() {
3342 global $CFG, $SESSION;
3343 return (!empty($CFG->autolangusercreation) && !empty($SESSION->lang)) ? $SESSION->lang : $CFG->lang;
3347 * Creates a bare-bones user record
3349 * @todo Outline auth types and provide code example
3351 * @param string $username New user's username to add to record
3352 * @param string $password New user's password to add to record
3353 * @param string $auth Form of authentication required
3354 * @return stdClass A complete user object
3356 function create_user_record($username, $password, $auth = 'manual') {
3357 global $CFG, $DB, $SESSION;
3358 require_once($CFG->dirroot.'/user/profile/lib.php');
3359 require_once($CFG->dirroot.'/user/lib.php');
3361 // Just in case check text case.
3362 $username = trim(core_text::strtolower($username));
3364 $authplugin = get_auth_plugin($auth);
3365 $customfields = $authplugin->get_custom_user_profile_fields();
3366 $newuser = new stdClass();
3367 if ($newinfo = $authplugin->get_userinfo($username)) {
3368 $newinfo = truncate_userinfo($newinfo);
3369 foreach ($newinfo as $key => $value) {
3370 if (in_array($key, $authplugin->userfields) || (in_array($key, $customfields))) {
3371 $newuser->$key = $value;
3376 if (!empty($newuser->email)) {
3377 if (email_is_not_allowed($newuser->email)) {
3378 unset($newuser->email);
3382 $newuser->auth = $auth;
3383 $newuser->username = $username;
3385 // Fix for MDL-8480
3386 // user CFG lang for user if $newuser->lang is empty
3387 // or $user->lang is not an installed language.
3388 if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) {
3389 $newuser->lang = get_newuser_language();
3391 $newuser->confirmed = 1;
3392 $newuser->lastip = getremoteaddr();
3393 $newuser->timecreated = time();
3394 $newuser->timemodified = $newuser->timecreated;
3395 $newuser->mnethostid = $CFG->mnet_localhost_id;
3397 $newuser->id = user_create_user($newuser, false, false);
3399 // Save user profile data.
3400 profile_save_data($newuser);
3402 $user = get_complete_user_data('id', $newuser->id);
3403 if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})) {
3404 set_user_preference('auth_forcepasswordchange', 1, $user);
3406 // Set the password.
3407 update_internal_user_password($user, $password);
3409 // Trigger event.
3410 \core\event\user_created::create_from_userid($newuser->id)->trigger();
3412 return $user;
3416 * Will update a local user record from an external source (MNET users can not be updated using this method!).
3418 * @param string $username user's username to update the record
3419 * @return stdClass A complete user object
3421 function update_user_record($username) {
3422 global $DB, $CFG;
3423 // Just in case check text case.
3424 $username = trim(core_text::strtolower($username));
3426 $oldinfo = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id), '*', MUST_EXIST);
3427 return update_user_record_by_id($oldinfo->id);
3431 * Will update a local user record from an external source (MNET users can not be updated using this method!).
3433 * @param int $id user id
3434 * @return stdClass A complete user object
3436 function update_user_record_by_id($id) {
3437 global $DB, $CFG;
3438 require_once($CFG->dirroot."/user/profile/lib.php");
3439 require_once($CFG->dirroot.'/user/lib.php');
3441 $params = array('mnethostid' => $CFG->mnet_localhost_id, 'id' => $id, 'deleted' => 0);
3442 $oldinfo = $DB->get_record('user', $params, '*', MUST_EXIST);
3444 $newuser = array();
3445 $userauth = get_auth_plugin($oldinfo->auth);
3447 if ($newinfo = $userauth->get_userinfo($oldinfo->username)) {
3448 $newinfo = truncate_userinfo($newinfo);
3449 $customfields = $userauth->get_custom_user_profile_fields();
3451 foreach ($newinfo as $key => $value) {
3452 $iscustom = in_array($key, $customfields);
3453 if (!$iscustom) {
3454 $key = strtolower($key);
3456 if ((!property_exists($oldinfo, $key) && !$iscustom) or $key === 'username' or $key === 'id'
3457 or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') {
3458 // Unknown or must not be changed.
3459 continue;
3461 if (empty($userauth->config->{'field_updatelocal_' . $key}) || empty($userauth->config->{'field_lock_' . $key})) {
3462 continue;
3464 $confval = $userauth->config->{'field_updatelocal_' . $key};
3465 $lockval = $userauth->config->{'field_lock_' . $key};
3466 if ($confval === 'onlogin') {
3467 // MDL-4207 Don't overwrite modified user profile values with
3468 // empty LDAP values when 'unlocked if empty' is set. The purpose
3469 // of the setting 'unlocked if empty' is to allow the user to fill
3470 // in a value for the selected field _if LDAP is giving
3471 // nothing_ for this field. Thus it makes sense to let this value
3472 // stand in until LDAP is giving a value for this field.
3473 if (!(empty($value) && $lockval === 'unlockedifempty')) {
3474 if ($iscustom || (in_array($key, $userauth->userfields) &&
3475 ((string)$oldinfo->$key !== (string)$value))) {
3476 $newuser[$key] = (string)$value;
3481 if ($newuser) {
3482 $newuser['id'] = $oldinfo->id;
3483 $newuser['timemodified'] = time();
3484 user_update_user((object) $newuser, false, false);
3486 // Save user profile data.
3487 profile_save_data((object) $newuser);
3489 // Trigger event.
3490 \core\event\user_updated::create_from_userid($newuser['id'])->trigger();
3494 return get_complete_user_data('id', $oldinfo->id);
3498 * Will truncate userinfo as it comes from auth_get_userinfo (from external auth) which may have large fields.
3500 * @param array $info Array of user properties to truncate if needed
3501 * @return array The now truncated information that was passed in
3503 function truncate_userinfo(array $info) {
3504 // Define the limits.
3505 $limit = array(
3506 'username' => 100,
3507 'idnumber' => 255,
3508 'firstname' => 100,
3509 'lastname' => 100,
3510 'email' => 100,
3511 'phone1' => 20,
3512 'phone2' => 20,
3513 'institution' => 255,
3514 'department' => 255,
3515 'address' => 255,
3516 'city' => 120,
3517 'country' => 2,
3520 // Apply where needed.
3521 foreach (array_keys($info) as $key) {
3522 if (!empty($limit[$key])) {
3523 $info[$key] = trim(core_text::substr($info[$key], 0, $limit[$key]));
3527 return $info;
3531 * Marks user deleted in internal user database and notifies the auth plugin.
3532 * Also unenrols user from all roles and does other cleanup.
3534 * Any plugin that needs to purge user data should register the 'user_deleted' event.
3536 * @param stdClass $user full user object before delete
3537 * @return boolean success
3538 * @throws coding_exception if invalid $user parameter detected
3540 function delete_user(stdClass $user) {
3541 global $CFG, $DB, $SESSION;
3542 require_once($CFG->libdir.'/grouplib.php');
3543 require_once($CFG->libdir.'/gradelib.php');
3544 require_once($CFG->dirroot.'/message/lib.php');
3545 require_once($CFG->dirroot.'/user/lib.php');
3547 // Make sure nobody sends bogus record type as parameter.
3548 if (!property_exists($user, 'id') or !property_exists($user, 'username')) {
3549 throw new coding_exception('Invalid $user parameter in delete_user() detected');
3552 // Better not trust the parameter and fetch the latest info this will be very expensive anyway.
3553 if (!$user = $DB->get_record('user', array('id' => $user->id))) {
3554 debugging('Attempt to delete unknown user account.');
3555 return false;
3558 // There must be always exactly one guest record, originally the guest account was identified by username only,
3559 // now we use $CFG->siteguest for performance reasons.
3560 if ($user->username === 'guest' or isguestuser($user)) {
3561 debugging('Guest user account can not be deleted.');
3562 return false;
3565 // Admin can be theoretically from different auth plugin, but we want to prevent deletion of internal accoutns only,
3566 // if anything goes wrong ppl may force somebody to be admin via config.php setting $CFG->siteadmins.
3567 if ($user->auth === 'manual' and is_siteadmin($user)) {
3568 debugging('Local administrator accounts can not be deleted.');
3569 return false;
3571 // Allow plugins to use this user object before we completely delete it.
3572 if ($pluginsfunction = get_plugins_with_function('pre_user_delete')) {
3573 foreach ($pluginsfunction as $plugintype => $plugins) {
3574 foreach ($plugins as $pluginfunction) {
3575 $pluginfunction($user);
3580 // Dispatch the hook for pre user update actions.
3581 $hook = new \core_user\hook\before_user_deleted(
3582 user: $user,
3584 \core\di::get(\core\hook\manager::class)->dispatch($hook);
3586 // Keep user record before updating it, as we have to pass this to user_deleted event.
3587 $olduser = clone $user;
3589 // Keep a copy of user context, we need it for event.
3590 $usercontext = context_user::instance($user->id);
3592 // Delete all grades - backup is kept in grade_grades_history table.
3593 grade_user_delete($user->id);
3595 // TODO: remove from cohorts using standard API here.
3597 // Remove user tags.
3598 core_tag_tag::remove_all_item_tags('core', 'user', $user->id);
3600 // Unconditionally unenrol from all courses.
3601 enrol_user_delete($user);
3603 // Unenrol from all roles in all contexts.
3604 // This might be slow but it is really needed - modules might do some extra cleanup!
3605 role_unassign_all(array('userid' => $user->id));
3607 // Notify the competency subsystem.
3608 \core_competency\api::hook_user_deleted($user->id);
3610 // Now do a brute force cleanup.
3612 // Delete all user events and subscription events.
3613 $DB->delete_records_select('event', 'userid = :userid AND subscriptionid IS NOT NULL', ['userid' => $user->id]);
3615 // Now, delete all calendar subscription from the user.
3616 $DB->delete_records('event_subscriptions', ['userid' => $user->id]);
3618 // Remove from all cohorts.
3619 $DB->delete_records('cohort_members', array('userid' => $user->id));
3621 // Remove from all groups.
3622 $DB->delete_records('groups_members', array('userid' => $user->id));
3624 // Brute force unenrol from all courses.
3625 $DB->delete_records('user_enrolments', array('userid' => $user->id));
3627 // Purge user preferences.
3628 $DB->delete_records('user_preferences', array('userid' => $user->id));
3630 // Purge user extra profile info.
3631 $DB->delete_records('user_info_data', array('userid' => $user->id));
3633 // Purge log of previous password hashes.
3634 $DB->delete_records('user_password_history', array('userid' => $user->id));
3636 // Last course access not necessary either.
3637 $DB->delete_records('user_lastaccess', array('userid' => $user->id));
3638 // Remove all user tokens.
3639 $DB->delete_records('external_tokens', array('userid' => $user->id));
3641 // Unauthorise the user for all services.
3642 $DB->delete_records('external_services_users', array('userid' => $user->id));
3644 // Remove users private keys.
3645 $DB->delete_records('user_private_key', array('userid' => $user->id));
3647 // Remove users customised pages.
3648 $DB->delete_records('my_pages', array('userid' => $user->id, 'private' => 1));
3650 // Remove user's oauth2 refresh tokens, if present.
3651 $DB->delete_records('oauth2_refresh_token', array('userid' => $user->id));
3653 // Delete user from $SESSION->bulk_users.
3654 if (isset($SESSION->bulk_users[$user->id])) {
3655 unset($SESSION->bulk_users[$user->id]);
3658 // Force logout - may fail if file based sessions used, sorry.
3659 \core\session\manager::kill_user_sessions($user->id);
3661 // Generate username from email address, or a fake email.
3662 $delemail = !empty($user->email) ? $user->email : $user->username . '.' . $user->id . '@unknownemail.invalid';
3664 $deltime = time();
3666 // Max username length is 100 chars. Select up to limit - (length of current time + 1 [period character]) from users email.
3667 $delnameprefix = clean_param($delemail, PARAM_USERNAME);
3668 $delnamesuffix = $deltime;
3669 $delnamesuffixlength = 10;
3670 do {
3671 // Workaround for bulk deletes of users with the same email address.
3672 $delname = sprintf(
3673 "%s.%10d",
3674 core_text::substr(
3675 $delnameprefix,
3677 // 100 Character maximum, with a '.' character, and a 10-digit timestamp.
3678 100 - 1 - $delnamesuffixlength,
3680 $delnamesuffix,
3682 $delnamesuffix++;
3684 // No need to use mnethostid here.
3685 } while ($DB->record_exists('user', ['username' => $delname]));
3687 // Mark internal user record as "deleted".
3688 $updateuser = new stdClass();
3689 $updateuser->id = $user->id;
3690 $updateuser->deleted = 1;
3691 $updateuser->username = $delname; // Remember it just in case.
3692 $updateuser->email = md5($user->username);// Store hash of username, useful importing/restoring users.
3693 $updateuser->idnumber = ''; // Clear this field to free it up.
3694 $updateuser->picture = 0;
3695 $updateuser->timemodified = $deltime;
3697 // Don't trigger update event, as user is being deleted.
3698 user_update_user($updateuser, false, false);
3700 // Delete all content associated with the user context, but not the context itself.
3701 $usercontext->delete_content();
3703 // Delete any search data.
3704 \core_search\manager::context_deleted($usercontext);
3706 // Any plugin that needs to cleanup should register this event.
3707 // Trigger event.
3708 $event = \core\event\user_deleted::create(
3709 array(
3710 'objectid' => $user->id,
3711 'relateduserid' => $user->id,
3712 'context' => $usercontext,
3713 'other' => array(
3714 'username' => $user->username,
3715 'email' => $user->email,
3716 'idnumber' => $user->idnumber,
3717 'picture' => $user->picture,
3718 'mnethostid' => $user->mnethostid
3722 $event->add_record_snapshot('user', $olduser);
3723 $event->trigger();
3725 // We will update the user's timemodified, as it will be passed to the user_deleted event, which
3726 // should know about this updated property persisted to the user's table.
3727 $user->timemodified = $updateuser->timemodified;
3729 // Notify auth plugin - do not block the delete even when plugin fails.
3730 $authplugin = get_auth_plugin($user->auth);
3731 $authplugin->user_delete($user);
3733 return true;
3737 * Retrieve the guest user object.
3739 * @return stdClass A {@link $USER} object
3741 function guest_user() {
3742 global $CFG, $DB;
3744 if ($newuser = $DB->get_record('user', array('id' => $CFG->siteguest))) {
3745 $newuser->confirmed = 1;
3746 $newuser->lang = get_newuser_language();
3747 $newuser->lastip = getremoteaddr();
3750 return $newuser;
3754 * Authenticates a user against the chosen authentication mechanism
3756 * Given a username and password, this function looks them
3757 * up using the currently selected authentication mechanism,
3758 * and if the authentication is successful, it returns a
3759 * valid $user object from the 'user' table.
3761 * Uses auth_ functions from the currently active auth module
3763 * After authenticate_user_login() returns success, you will need to
3764 * log that the user has logged in, and call complete_user_login() to set
3765 * the session up.
3767 * Note: this function works only with non-mnet accounts!
3769 * @param string $username User's username (or also email if $CFG->authloginviaemail enabled)
3770 * @param string $password User's password
3771 * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO
3772 * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists)
3773 * @param string|bool $logintoken If this is set to a string it is validated against the login token for the session.
3774 * @param string|bool $loginrecaptcha If this is set to a string it is validated against Google reCaptcha.
3775 * @return stdClass|false A {@link $USER} object or false if error
3777 function authenticate_user_login(
3778 $username,
3779 $password,
3780 $ignorelockout = false,
3781 &$failurereason = null,
3782 $logintoken = false,
3783 string|bool $loginrecaptcha = false,
3785 global $CFG, $DB, $PAGE, $SESSION;
3786 require_once("$CFG->libdir/authlib.php");
3788 if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) {
3789 // we have found the user
3791 } else if (!empty($CFG->authloginviaemail)) {
3792 if ($email = clean_param($username, PARAM_EMAIL)) {
3793 $select = "mnethostid = :mnethostid AND LOWER(email) = LOWER(:email) AND deleted = 0";
3794 $params = array('mnethostid' => $CFG->mnet_localhost_id, 'email' => $email);
3795 $users = $DB->get_records_select('user', $select, $params, 'id', 'id', 0, 2);
3796 if (count($users) === 1) {
3797 // Use email for login only if unique.
3798 $user = reset($users);
3799 $user = get_complete_user_data('id', $user->id);
3800 $username = $user->username;
3802 unset($users);
3806 // Make sure this request came from the login form.
3807 if (!\core\session\manager::validate_login_token($logintoken)) {
3808 $failurereason = AUTH_LOGIN_FAILED;
3810 // Trigger login failed event (specifying the ID of the found user, if available).
3811 \core\event\user_login_failed::create([
3812 'userid' => ($user->id ?? 0),
3813 'other' => [
3814 'username' => $username,
3815 'reason' => $failurereason,
3817 ])->trigger();
3819 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Invalid Login Token: $username ".$_SERVER['HTTP_USER_AGENT']);
3820 return false;
3823 // Login reCaptcha.
3824 if (login_captcha_enabled() && !validate_login_captcha($loginrecaptcha)) {
3825 $failurereason = AUTH_LOGIN_FAILED_RECAPTCHA;
3826 // Trigger login failed event (specifying the ID of the found user, if available).
3827 \core\event\user_login_failed::create([
3828 'userid' => ($user->id ?? 0),
3829 'other' => [
3830 'username' => $username,
3831 'reason' => $failurereason,
3833 ])->trigger();
3834 return false;
3837 $authsenabled = get_enabled_auth_plugins();
3839 if ($user) {
3840 // Use manual if auth not set.
3841 $auth = empty($user->auth) ? 'manual' : $user->auth;
3843 if (in_array($user->auth, $authsenabled)) {
3844 $authplugin = get_auth_plugin($user->auth);
3845 $authplugin->pre_user_login_hook($user);
3848 if (!empty($user->suspended)) {
3849 $failurereason = AUTH_LOGIN_SUSPENDED;
3851 // Trigger login failed event.
3852 $event = \core\event\user_login_failed::create(array('userid' => $user->id,
3853 'other' => array('username' => $username, 'reason' => $failurereason)));
3854 $event->trigger();
3855 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']);
3856 return false;
3858 if ($auth=='nologin' or !is_enabled_auth($auth)) {
3859 // Legacy way to suspend user.
3860 $failurereason = AUTH_LOGIN_SUSPENDED;
3862 // Trigger login failed event.
3863 $event = \core\event\user_login_failed::create(array('userid' => $user->id,
3864 'other' => array('username' => $username, 'reason' => $failurereason)));
3865 $event->trigger();
3866 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Disabled Login: $username ".$_SERVER['HTTP_USER_AGENT']);
3867 return false;
3869 $auths = array($auth);
3871 } else {
3872 // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user().
3873 if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'deleted' => 1))) {
3874 $failurereason = AUTH_LOGIN_NOUSER;
3876 // Trigger login failed event.
3877 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
3878 'reason' => $failurereason)));
3879 $event->trigger();
3880 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Deleted Login: $username ".$_SERVER['HTTP_USER_AGENT']);
3881 return false;
3884 // User does not exist.
3885 $auths = $authsenabled;
3886 $user = new stdClass();
3887 $user->id = 0;
3890 if ($ignorelockout) {
3891 // Some other mechanism protects against brute force password guessing, for example login form might include reCAPTCHA
3892 // or this function is called from a SSO script.
3893 } else if ($user->id) {
3894 // Verify login lockout after other ways that may prevent user login.
3895 if (login_is_lockedout($user)) {
3896 $failurereason = AUTH_LOGIN_LOCKOUT;
3898 // Trigger login failed event.
3899 $event = \core\event\user_login_failed::create(array('userid' => $user->id,
3900 'other' => array('username' => $username, 'reason' => $failurereason)));
3901 $event->trigger();
3903 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Login lockout: $username ".$_SERVER['HTTP_USER_AGENT']);
3904 $SESSION->loginerrormsg = get_string('accountlocked', 'admin');
3906 return false;
3908 } else {
3909 // We can not lockout non-existing accounts.
3912 foreach ($auths as $auth) {
3913 $authplugin = get_auth_plugin($auth);
3915 // On auth fail fall through to the next plugin.
3916 if (!$authplugin->user_login($username, $password)) {
3917 continue;
3920 // Before performing login actions, check if user still passes password policy, if admin setting is enabled.
3921 if (!empty($CFG->passwordpolicycheckonlogin)) {
3922 $errmsg = '';
3923 $passed = check_password_policy($password, $errmsg, $user);
3924 if (!$passed) {
3925 // First trigger event for failure.
3926 $failedevent = \core\event\user_password_policy_failed::create_from_user($user);
3927 $failedevent->trigger();
3929 // If able to change password, set flag and move on.
3930 if ($authplugin->can_change_password()) {
3931 // Check if we are on internal change password page, or service is external, don't show notification.
3932 $internalchangeurl = new moodle_url('/login/change_password.php');
3933 if (!($PAGE->has_set_url() && $internalchangeurl->compare($PAGE->url)) && $authplugin->is_internal()) {
3934 \core\notification::error(get_string('passwordpolicynomatch', '', $errmsg));
3936 set_user_preference('auth_forcepasswordchange', 1, $user);
3937 } else if ($authplugin->can_reset_password()) {
3938 // Else force a reset if possible.
3939 \core\notification::error(get_string('forcepasswordresetnotice', '', $errmsg));
3940 redirect(new moodle_url('/login/forgot_password.php'));
3941 } else {
3942 $notifymsg = get_string('forcepasswordresetfailurenotice', '', $errmsg);
3943 // If support page is set, add link for help.
3944 if (!empty($CFG->supportpage)) {
3945 $link = \html_writer::link($CFG->supportpage, $CFG->supportpage);
3946 $link = \html_writer::tag('p', $link);
3947 $notifymsg .= $link;
3950 // If no change or reset is possible, add a notification for user.
3951 \core\notification::error($notifymsg);
3956 // Successful authentication.
3957 if ($user->id) {
3958 // User already exists in database.
3959 if (empty($user->auth)) {
3960 // For some reason auth isn't set yet.
3961 $DB->set_field('user', 'auth', $auth, array('id' => $user->id));
3962 $user->auth = $auth;
3965 // If the existing hash is using an out-of-date algorithm (or the legacy md5 algorithm), then we should update to
3966 // the current hash algorithm while we have access to the user's password.
3967 update_internal_user_password($user, $password);
3969 if ($authplugin->is_synchronised_with_external()) {
3970 // Update user record from external DB.
3971 $user = update_user_record_by_id($user->id);
3973 } else {
3974 // The user is authenticated but user creation may be disabled.
3975 if (!empty($CFG->authpreventaccountcreation)) {
3976 $failurereason = AUTH_LOGIN_UNAUTHORISED;
3978 // Trigger login failed event.
3979 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
3980 'reason' => $failurereason)));
3981 $event->trigger();
3983 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Unknown user, can not create new accounts: $username ".
3984 $_SERVER['HTTP_USER_AGENT']);
3985 return false;
3986 } else {
3987 $user = create_user_record($username, $password, $auth);
3991 $authplugin->sync_roles($user);
3993 foreach ($authsenabled as $hau) {
3994 $hauth = get_auth_plugin($hau);
3995 $hauth->user_authenticated_hook($user, $username, $password);
3998 if (empty($user->id)) {
3999 $failurereason = AUTH_LOGIN_NOUSER;
4000 // Trigger login failed event.
4001 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4002 'reason' => $failurereason)));
4003 $event->trigger();
4004 return false;
4007 if (!empty($user->suspended)) {
4008 // Just in case some auth plugin suspended account.
4009 $failurereason = AUTH_LOGIN_SUSPENDED;
4010 // Trigger login failed event.
4011 $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4012 'other' => array('username' => $username, 'reason' => $failurereason)));
4013 $event->trigger();
4014 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4015 return false;
4018 login_attempt_valid($user);
4019 $failurereason = AUTH_LOGIN_OK;
4020 return $user;
4023 // Failed if all the plugins have failed.
4024 if (debugging('', DEBUG_ALL)) {
4025 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Failed Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4028 if ($user->id) {
4029 login_attempt_failed($user);
4030 $failurereason = AUTH_LOGIN_FAILED;
4031 // Trigger login failed event.
4032 $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4033 'other' => array('username' => $username, 'reason' => $failurereason)));
4034 $event->trigger();
4035 } else {
4036 $failurereason = AUTH_LOGIN_NOUSER;
4037 // Trigger login failed event.
4038 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4039 'reason' => $failurereason)));
4040 $event->trigger();
4043 return false;
4047 * Call to complete the user login process after authenticate_user_login()
4048 * has succeeded. It will setup the $USER variable and other required bits
4049 * and pieces.
4051 * NOTE:
4052 * - It will NOT log anything -- up to the caller to decide what to log.
4053 * - this function does not set any cookies any more!
4055 * @param stdClass $user
4056 * @param array $extrauserinfo
4057 * @return stdClass A {@link $USER} object - BC only, do not use
4059 function complete_user_login($user, array $extrauserinfo = []) {
4060 global $CFG, $DB, $USER, $SESSION;
4062 \core\session\manager::login_user($user);
4064 // Reload preferences from DB.
4065 unset($USER->preference);
4066 check_user_preferences_loaded($USER);
4068 // Update login times.
4069 update_user_login_times();
4071 // Extra session prefs init.
4072 set_login_session_preferences();
4074 // Trigger login event.
4075 $event = \core\event\user_loggedin::create(
4076 array(
4077 'userid' => $USER->id,
4078 'objectid' => $USER->id,
4079 'other' => [
4080 'username' => $USER->username,
4081 'extrauserinfo' => $extrauserinfo
4085 $event->trigger();
4087 // Allow plugins to callback as soon possible after user has completed login.
4088 $hook = new \core\hook\user\after_complete_login();
4089 \core\hook\manager::get_instance()->dispatch($hook);
4091 // Check if the user is using a new browser or session (a new MoodleSession cookie is set in that case).
4092 // If the user is accessing from the same IP, ignore everything (most of the time will be a new session in the same browser).
4093 // Skip Web Service requests, CLI scripts, AJAX scripts, and request from the mobile app itself.
4094 $loginip = getremoteaddr();
4095 $isnewip = isset($SESSION->userpreviousip) && $SESSION->userpreviousip != $loginip;
4096 $isvalidenv = (!WS_SERVER && !CLI_SCRIPT && !NO_MOODLE_COOKIES) || PHPUNIT_TEST;
4098 if (!empty($SESSION->isnewsessioncookie) && $isnewip && $isvalidenv && !\core_useragent::is_moodle_app()) {
4100 $logintime = time();
4101 $ismoodleapp = false;
4102 $useragent = \core_useragent::get_user_agent_string();
4104 $sitepreferences = get_message_output_default_preferences();
4105 // Check if new login notification is disabled at system level.
4106 $newlogindisabled = $sitepreferences->moodle_newlogin_disable ?? 0;
4107 // Check if message providers (web, email, mobile) are enabled at system level.
4108 $msgproviderenabled = isset($sitepreferences->message_provider_moodle_newlogin_enabled);
4109 // Get message providers enabled for a user.
4110 $userpreferences = get_user_preferences('message_provider_moodle_newlogin_enabled');
4111 // Check if notification processor plugins (web, email, mobile) are enabled at system level.
4112 $msgprocessorsready = !empty(get_message_processors(true));
4113 // If new login notification is enabled at system level then go for other conditions check.
4114 $newloginenabled = $newlogindisabled ? 0 : ($userpreferences != 'none' && $msgproviderenabled);
4116 if ($newloginenabled && $msgprocessorsready) {
4117 // Schedule adhoc task to send a login notification to the user.
4118 $task = new \core\task\send_login_notifications();
4119 $task->set_userid($USER->id);
4120 $task->set_custom_data(compact('ismoodleapp', 'useragent', 'loginip', 'logintime'));
4121 $task->set_component('core');
4122 \core\task\manager::queue_adhoc_task($task);
4126 // Queue migrating the messaging data, if we need to.
4127 if (!get_user_preferences('core_message_migrate_data', false, $USER->id)) {
4128 // Check if there are any legacy messages to migrate.
4129 if (\core_message\helper::legacy_messages_exist($USER->id)) {
4130 \core_message\task\migrate_message_data::queue_task($USER->id);
4131 } else {
4132 set_user_preference('core_message_migrate_data', true, $USER->id);
4136 if (isguestuser()) {
4137 // No need to continue when user is THE guest.
4138 return $USER;
4141 if (CLI_SCRIPT) {
4142 // We can redirect to password change URL only in browser.
4143 return $USER;
4146 // Select password change url.
4147 $userauth = get_auth_plugin($USER->auth);
4149 // Check whether the user should be changing password.
4150 if (get_user_preferences('auth_forcepasswordchange', false)) {
4151 if ($userauth->can_change_password()) {
4152 if ($changeurl = $userauth->change_password_url()) {
4153 redirect($changeurl);
4154 } else {
4155 require_once($CFG->dirroot . '/login/lib.php');
4156 $SESSION->wantsurl = core_login_get_return_url();
4157 redirect($CFG->wwwroot.'/login/change_password.php');
4159 } else {
4160 throw new \moodle_exception('nopasswordchangeforced', 'auth');
4163 return $USER;
4167 * Check a password hash to see if it was hashed using the legacy hash algorithm (bcrypt).
4169 * @param string $password String to check.
4170 * @return bool True if the $password matches the format of a bcrypt hash.
4172 function password_is_legacy_hash(#[\SensitiveParameter] string $password): bool {
4173 return (bool) preg_match('/^\$2y\$[\d]{2}\$[A-Za-z0-9\.\/]{53}$/', $password);
4177 * Calculate the Shannon entropy of a string.
4179 * @param string $pepper The pepper to calculate the entropy of.
4180 * @return float The Shannon entropy of the string.
4182 function calculate_entropy(#[\SensitiveParameter] string $pepper): float {
4183 // Initialize entropy.
4184 $h = 0;
4186 // Calculate the length of the string.
4187 $size = strlen($pepper);
4189 // For each unique character in the string.
4190 foreach (count_chars($pepper, 1) as $v) {
4191 // Calculate the probability of the character.
4192 $p = $v / $size;
4194 // Add the character's contribution to the total entropy.
4195 // This uses the formula for the entropy of a discrete random variable.
4196 $h -= $p * log($p) / log(2);
4199 // Instead of returning the average entropy per symbol (Shannon entropy),
4200 // we multiply by the length of the string to get total entropy.
4201 return $h * $size;
4205 * Get the available password peppers.
4206 * The latest pepper is checked for minimum entropy as part of this function.
4207 * We only calculate the entropy of the most recent pepper,
4208 * because passwords are always updated to the latest pepper,
4209 * and in the past we may have enforced a lower minimum entropy.
4210 * Also, we allow the latest pepper to be empty, to allow admins to migrate off peppers.
4212 * @return array The password peppers.
4213 * @throws coding_exception If the entropy of the password pepper is less than the recommended minimum.
4215 function get_password_peppers(): array {
4216 global $CFG;
4218 // Get all available peppers.
4219 if (isset($CFG->passwordpeppers) && is_array($CFG->passwordpeppers)) {
4220 // Sort the array in descending order of keys (numerical).
4221 $peppers = $CFG->passwordpeppers;
4222 krsort($peppers, SORT_NUMERIC);
4223 } else {
4224 $peppers = []; // Set an empty array if no peppers are found.
4227 // Check if the entropy of the most recent pepper is less than the minimum.
4228 // Also, we allow the most recent pepper to be empty, to allow admins to migrate off peppers.
4229 $lastpepper = reset($peppers);
4230 if (!empty($peppers) && $lastpepper !== '' && calculate_entropy($lastpepper) < PEPPER_ENTROPY) {
4231 throw new coding_exception(
4232 'password pepper below minimum',
4233 'The entropy of the password pepper is less than the recommended minimum.');
4235 return $peppers;
4239 * Compare password against hash stored in user object to determine if it is valid.
4241 * If necessary it also updates the stored hash to the current format.
4243 * @param stdClass $user (Password property may be updated).
4244 * @param string $password Plain text password.
4245 * @return bool True if password is valid.
4247 function validate_internal_user_password(stdClass $user, #[\SensitiveParameter] string $password): bool {
4249 if (exceeds_password_length($password)) {
4250 // Password cannot be more than MAX_PASSWORD_CHARACTERS characters.
4251 return false;
4254 if ($user->password === AUTH_PASSWORD_NOT_CACHED) {
4255 // Internal password is not used at all, it can not validate.
4256 return false;
4259 $peppers = get_password_peppers(); // Get the array of available peppers.
4260 $islegacy = password_is_legacy_hash($user->password); // Check if the password is a legacy bcrypt hash.
4262 // If the password is a legacy hash, no peppers were used, so verify and update directly.
4263 if ($islegacy && password_verify($password, $user->password)) {
4264 update_internal_user_password($user, $password);
4265 return true;
4268 // If the password is not a legacy hash, iterate through the peppers.
4269 $latestpepper = reset($peppers);
4270 // Add an empty pepper to the beginning of the array. To make it easier to check if the password matches without any pepper.
4271 $peppers = [-1 => ''] + $peppers;
4272 foreach ($peppers as $pepper) {
4273 $pepperedpassword = $password . $pepper;
4275 // If the peppered password is correct, update (if necessary) and return true.
4276 if (password_verify($pepperedpassword, $user->password)) {
4277 // If the pepper used is not the latest one, update the password.
4278 if ($pepper !== $latestpepper) {
4279 update_internal_user_password($user, $password);
4281 return true;
4285 // If no peppered password was correct, the password is wrong.
4286 return false;
4290 * Calculate hash for a plain text password.
4292 * @param string $password Plain text password to be hashed.
4293 * @param bool $fasthash If true, use a low number of rounds when generating the hash
4294 * This is faster to generate but makes the hash less secure.
4295 * It is used when lots of hashes need to be generated quickly.
4296 * @param int $pepperlength Lenght of the peppers
4297 * @return string The hashed password.
4299 * @throws moodle_exception If a problem occurs while generating the hash.
4301 function hash_internal_user_password(#[\SensitiveParameter] string $password, $fasthash = false, $pepperlength = 0): string {
4302 if (exceeds_password_length($password, $pepperlength)) {
4303 // Password cannot be more than MAX_PASSWORD_CHARACTERS.
4304 throw new \moodle_exception(get_string("passwordexceeded", 'error', MAX_PASSWORD_CHARACTERS));
4307 // Set the cost factor to 5000 for fast hashing, otherwise use default cost.
4308 $rounds = $fasthash ? 5000 : 10000;
4310 // First generate a cryptographically suitable salt.
4311 $randombytes = random_bytes(16);
4312 $salt = substr(strtr(base64_encode($randombytes), '+', '.'), 0, 16);
4314 // Now construct the password string with the salt and number of rounds.
4315 // The password string is in the format $algorithm$rounds$salt$hash. ($6 is the SHA512 algorithm).
4316 $generatedhash = crypt($password, implode('$', [
4318 // The SHA512 Algorithm
4319 '6',
4320 "rounds={$rounds}",
4321 $salt,
4323 ]));
4325 if ($generatedhash === false || $generatedhash === null) {
4326 throw new moodle_exception('Failed to generate password hash.');
4329 return $generatedhash;
4333 * Update password hash in user object (if necessary).
4335 * The password is updated if:
4336 * 1. The password has changed (the hash of $user->password is different
4337 * to the hash of $password).
4338 * 2. The existing hash is using an out-of-date algorithm (or the legacy
4339 * md5 algorithm).
4341 * The password is peppered with the latest pepper before hashing,
4342 * if peppers are available.
4343 * Updating the password will modify the $user object and the database
4344 * record to use the current hashing algorithm.
4345 * It will remove Web Services user tokens too.
4347 * @param stdClass $user User object (password property may be updated).
4348 * @param string $password Plain text password.
4349 * @param bool $fasthash If true, use a low cost factor when generating the hash
4350 * This is much faster to generate but makes the hash
4351 * less secure. It is used when lots of hashes need to
4352 * be generated quickly.
4353 * @return bool Always returns true.
4355 function update_internal_user_password(
4356 stdClass $user,
4357 #[\SensitiveParameter] string $password,
4358 bool $fasthash = false
4359 ): bool {
4360 global $CFG, $DB;
4362 // Add the latest password pepper to the password before further processing.
4363 $peppers = get_password_peppers();
4364 if (!empty($peppers)) {
4365 $password = $password . reset($peppers);
4368 // Figure out what the hashed password should be.
4369 if (!isset($user->auth)) {
4370 debugging('User record in update_internal_user_password() must include field auth',
4371 DEBUG_DEVELOPER);
4372 $user->auth = $DB->get_field('user', 'auth', array('id' => $user->id));
4374 $authplugin = get_auth_plugin($user->auth);
4375 if ($authplugin->prevent_local_passwords()) {
4376 $hashedpassword = AUTH_PASSWORD_NOT_CACHED;
4377 } else {
4378 $hashedpassword = hash_internal_user_password($password, $fasthash);
4381 $algorithmchanged = false;
4383 if ($hashedpassword === AUTH_PASSWORD_NOT_CACHED) {
4384 // Password is not cached, update it if not set to AUTH_PASSWORD_NOT_CACHED.
4385 $passwordchanged = ($user->password !== $hashedpassword);
4387 } else if (isset($user->password)) {
4388 // If verification fails then it means the password has changed.
4389 $passwordchanged = !password_verify($password, $user->password);
4390 $algorithmchanged = password_is_legacy_hash($user->password);
4391 } else {
4392 // While creating new user, password in unset in $user object, to avoid
4393 // saving it with user_create()
4394 $passwordchanged = true;
4397 if ($passwordchanged || $algorithmchanged) {
4398 $DB->set_field('user', 'password', $hashedpassword, array('id' => $user->id));
4399 $user->password = $hashedpassword;
4401 // Trigger event.
4402 $user = $DB->get_record('user', array('id' => $user->id));
4403 \core\event\user_password_updated::create_from_user($user)->trigger();
4405 // Remove WS user tokens.
4406 if (!empty($CFG->passwordchangetokendeletion)) {
4407 require_once($CFG->dirroot.'/webservice/lib.php');
4408 webservice::delete_user_ws_tokens($user->id);
4412 return true;
4416 * Get a complete user record, which includes all the info in the user record.
4418 * Intended for setting as $USER session variable
4420 * @param string $field The user field to be checked for a given value.
4421 * @param string $value The value to match for $field.
4422 * @param int $mnethostid
4423 * @param bool $throwexception If true, it will throw an exception when there's no record found or when there are multiple records
4424 * found. Otherwise, it will just return false.
4425 * @return mixed False, or A {@link $USER} object.
4427 function get_complete_user_data($field, $value, $mnethostid = null, $throwexception = false) {
4428 global $CFG, $DB;
4430 if (!$field || !$value) {
4431 return false;
4434 // Change the field to lowercase.
4435 $field = core_text::strtolower($field);
4437 // List of case insensitive fields.
4438 $caseinsensitivefields = ['email'];
4440 // Username input is forced to lowercase and should be case sensitive.
4441 if ($field == 'username') {
4442 $value = core_text::strtolower($value);
4445 // Build the WHERE clause for an SQL query.
4446 $params = array('fieldval' => $value);
4448 // Do a case-insensitive query, if necessary. These are generally very expensive. The performance can be improved on some DBs
4449 // such as MySQL by pre-filtering users with accent-insensitive subselect.
4450 if (in_array($field, $caseinsensitivefields)) {
4451 $fieldselect = $DB->sql_equal($field, ':fieldval', false);
4452 $idsubselect = $DB->sql_equal($field, ':fieldval2', false, false);
4453 $params['fieldval2'] = $value;
4454 } else {
4455 $fieldselect = "$field = :fieldval";
4456 $idsubselect = '';
4458 $constraints = "$fieldselect AND deleted <> 1";
4460 // If we are loading user data based on anything other than id,
4461 // we must also restrict our search based on mnet host.
4462 if ($field != 'id') {
4463 if (empty($mnethostid)) {
4464 // If empty, we restrict to local users.
4465 $mnethostid = $CFG->mnet_localhost_id;
4468 if (!empty($mnethostid)) {
4469 $params['mnethostid'] = $mnethostid;
4470 $constraints .= " AND mnethostid = :mnethostid";
4473 if ($idsubselect) {
4474 $constraints .= " AND id IN (SELECT id FROM {user} WHERE {$idsubselect})";
4477 // Get all the basic user data.
4478 try {
4479 // Make sure that there's only a single record that matches our query.
4480 // For example, when fetching by email, multiple records might match the query as there's no guarantee that email addresses
4481 // are unique. Therefore we can't reliably tell whether the user profile data that we're fetching is the correct one.
4482 $user = $DB->get_record_select('user', $constraints, $params, '*', MUST_EXIST);
4483 } catch (dml_exception $exception) {
4484 if ($throwexception) {
4485 throw $exception;
4486 } else {
4487 // Return false when no records or multiple records were found.
4488 return false;
4492 // Get various settings and preferences.
4494 // Preload preference cache.
4495 check_user_preferences_loaded($user);
4497 // Load course enrolment related stuff.
4498 $user->lastcourseaccess = array(); // During last session.
4499 $user->currentcourseaccess = array(); // During current session.
4500 if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid' => $user->id))) {
4501 foreach ($lastaccesses as $lastaccess) {
4502 $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess;
4506 // Add cohort theme.
4507 if (!empty($CFG->allowcohortthemes)) {
4508 require_once($CFG->dirroot . '/cohort/lib.php');
4509 if ($cohorttheme = cohort_get_user_cohort_theme($user->id)) {
4510 $user->cohorttheme = $cohorttheme;
4514 // Add the custom profile fields to the user record.
4515 $user->profile = array();
4516 if (!isguestuser($user)) {
4517 require_once($CFG->dirroot.'/user/profile/lib.php');
4518 profile_load_custom_fields($user);
4521 // Rewrite some variables if necessary.
4522 if (!empty($user->description)) {
4523 // No need to cart all of it around.
4524 $user->description = true;
4526 if (isguestuser($user)) {
4527 // Guest language always same as site.
4528 $user->lang = get_newuser_language();
4529 // Name always in current language.
4530 $user->firstname = get_string('guestuser');
4531 $user->lastname = ' ';
4534 return $user;
4538 * Validate a password against the configured password policy
4540 * @param string $password the password to be checked against the password policy
4541 * @param string $errmsg the error message to display when the password doesn't comply with the policy.
4542 * @param stdClass $user the user object to perform password validation against. Defaults to null if not provided.
4544 * @return bool true if the password is valid according to the policy. false otherwise.
4546 function check_password_policy($password, &$errmsg, $user = null) {
4547 global $CFG;
4549 if (!empty($CFG->passwordpolicy) && !isguestuser($user)) {
4550 $errmsg = '';
4551 if (core_text::strlen($password) < $CFG->minpasswordlength) {
4552 $errmsg .= '<div>'. get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength) .'</div>';
4554 if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) {
4555 $errmsg .= '<div>'. get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits) .'</div>';
4557 if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) {
4558 $errmsg .= '<div>'. get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower) .'</div>';
4560 if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) {
4561 $errmsg .= '<div>'. get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper) .'</div>';
4563 if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) {
4564 $errmsg .= '<div>'. get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum) .'</div>';
4566 if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) {
4567 $errmsg .= '<div>'. get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars) .'</div>';
4570 // Fire any additional password policy functions from plugins.
4571 // Plugin functions should output an error message string or empty string for success.
4572 $pluginsfunction = get_plugins_with_function('check_password_policy');
4573 foreach ($pluginsfunction as $plugintype => $plugins) {
4574 foreach ($plugins as $pluginfunction) {
4575 $pluginerr = $pluginfunction($password, $user);
4576 if ($pluginerr) {
4577 $errmsg .= '<div>'. $pluginerr .'</div>';
4583 if ($errmsg == '') {
4584 return true;
4585 } else {
4586 return false;
4592 * When logging in, this function is run to set certain preferences for the current SESSION.
4594 function set_login_session_preferences() {
4595 global $SESSION;
4597 $SESSION->justloggedin = true;
4599 unset($SESSION->lang);
4600 unset($SESSION->forcelang);
4601 unset($SESSION->load_navigation_admin);
4606 * Delete a course, including all related data from the database, and any associated files.
4608 * @param mixed $courseorid The id of the course or course object to delete.
4609 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4610 * @return bool true if all the removals succeeded. false if there were any failures. If this
4611 * method returns false, some of the removals will probably have succeeded, and others
4612 * failed, but you have no way of knowing which.
4614 function delete_course($courseorid, $showfeedback = true) {
4615 global $DB, $CFG;
4617 if (is_object($courseorid)) {
4618 $courseid = $courseorid->id;
4619 $course = $courseorid;
4620 } else {
4621 $courseid = $courseorid;
4622 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
4623 return false;
4626 $context = context_course::instance($courseid);
4628 // Frontpage course can not be deleted!!
4629 if ($courseid == SITEID) {
4630 return false;
4633 // Allow plugins to use this course before we completely delete it.
4634 if ($pluginsfunction = get_plugins_with_function('pre_course_delete')) {
4635 foreach ($pluginsfunction as $plugintype => $plugins) {
4636 foreach ($plugins as $pluginfunction) {
4637 $pluginfunction($course);
4642 // Dispatch the hook for pre course delete actions.
4643 $hook = new \core_course\hook\before_course_delete(
4644 course: $course,
4646 \core\di::get(\core\hook\manager::class)->dispatch($hook);
4648 // Tell the search manager we are about to delete a course. This prevents us sending updates
4649 // for each individual context being deleted.
4650 \core_search\manager::course_deleting_start($courseid);
4652 $handler = core_course\customfield\course_handler::create();
4653 $handler->delete_instance($courseid);
4655 // Make the course completely empty.
4656 remove_course_contents($courseid, $showfeedback);
4658 // Delete the course and related context instance.
4659 context_helper::delete_instance(CONTEXT_COURSE, $courseid);
4661 $DB->delete_records("course", array("id" => $courseid));
4662 $DB->delete_records("course_format_options", array("courseid" => $courseid));
4664 // Reset all course related caches here.
4665 core_courseformat\base::reset_course_cache($courseid);
4667 // Tell search that we have deleted the course so it can delete course data from the index.
4668 \core_search\manager::course_deleting_finish($courseid);
4670 // Trigger a course deleted event.
4671 $event = \core\event\course_deleted::create(array(
4672 'objectid' => $course->id,
4673 'context' => $context,
4674 'other' => array(
4675 'shortname' => $course->shortname,
4676 'fullname' => $course->fullname,
4677 'idnumber' => $course->idnumber
4680 $event->add_record_snapshot('course', $course);
4681 $event->trigger();
4683 return true;
4687 * Clear a course out completely, deleting all content but don't delete the course itself.
4689 * This function does not verify any permissions.
4691 * Please note this function also deletes all user enrolments,
4692 * enrolment instances and role assignments by default.
4694 * $options:
4695 * - 'keep_roles_and_enrolments' - false by default
4696 * - 'keep_groups_and_groupings' - false by default
4698 * @param int $courseid The id of the course that is being deleted
4699 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4700 * @param array $options extra options
4701 * @return bool true if all the removals succeeded. false if there were any failures. If this
4702 * method returns false, some of the removals will probably have succeeded, and others
4703 * failed, but you have no way of knowing which.
4705 function remove_course_contents($courseid, $showfeedback = true, array $options = null) {
4706 global $CFG, $DB, $OUTPUT;
4708 require_once($CFG->libdir.'/badgeslib.php');
4709 require_once($CFG->libdir.'/completionlib.php');
4710 require_once($CFG->libdir.'/questionlib.php');
4711 require_once($CFG->libdir.'/gradelib.php');
4712 require_once($CFG->dirroot.'/group/lib.php');
4713 require_once($CFG->dirroot.'/comment/lib.php');
4714 require_once($CFG->dirroot.'/rating/lib.php');
4715 require_once($CFG->dirroot.'/notes/lib.php');
4717 // Handle course badges.
4718 badges_handle_course_deletion($courseid);
4720 // NOTE: these concatenated strings are suboptimal, but it is just extra info...
4721 $strdeleted = get_string('deleted').' - ';
4723 // Some crazy wishlist of stuff we should skip during purging of course content.
4724 $options = (array)$options;
4726 $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
4727 $coursecontext = context_course::instance($courseid);
4728 $fs = get_file_storage();
4730 // Delete course completion information, this has to be done before grades and enrols.
4731 $cc = new completion_info($course);
4732 $cc->clear_criteria();
4733 if ($showfeedback) {
4734 echo $OUTPUT->notification($strdeleted.get_string('completion', 'completion'), 'notifysuccess');
4737 // Remove all data from gradebook - this needs to be done before course modules
4738 // because while deleting this information, the system may need to reference
4739 // the course modules that own the grades.
4740 remove_course_grades($courseid, $showfeedback);
4741 remove_grade_letters($coursecontext, $showfeedback);
4743 // Delete course blocks in any all child contexts,
4744 // they may depend on modules so delete them first.
4745 $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
4746 foreach ($childcontexts as $childcontext) {
4747 blocks_delete_all_for_context($childcontext->id);
4749 unset($childcontexts);
4750 blocks_delete_all_for_context($coursecontext->id);
4751 if ($showfeedback) {
4752 echo $OUTPUT->notification($strdeleted.get_string('type_block_plural', 'plugin'), 'notifysuccess');
4755 $DB->set_field('course_modules', 'deletioninprogress', '1', ['course' => $courseid]);
4756 rebuild_course_cache($courseid, true);
4758 // Get the list of all modules that are properly installed.
4759 $allmodules = $DB->get_records_menu('modules', array(), '', 'name, id');
4761 // Delete every instance of every module,
4762 // this has to be done before deleting of course level stuff.
4763 $locations = core_component::get_plugin_list('mod');
4764 foreach ($locations as $modname => $moddir) {
4765 if ($modname === 'NEWMODULE') {
4766 continue;
4768 if (array_key_exists($modname, $allmodules)) {
4769 $sql = "SELECT cm.*, m.id AS modinstance, m.name, '$modname' AS modname
4770 FROM {".$modname."} m
4771 LEFT JOIN {course_modules} cm ON cm.instance = m.id AND cm.module = :moduleid
4772 WHERE m.course = :courseid";
4773 $instances = $DB->get_records_sql($sql, array('courseid' => $course->id,
4774 'modulename' => $modname, 'moduleid' => $allmodules[$modname]));
4776 include_once("$moddir/lib.php"); // Shows php warning only if plugin defective.
4777 $moddelete = $modname .'_delete_instance'; // Delete everything connected to an instance.
4779 if ($instances) {
4780 foreach ($instances as $cm) {
4781 if ($cm->id) {
4782 // Delete activity context questions and question categories.
4783 question_delete_activity($cm);
4784 // Notify the competency subsystem.
4785 \core_competency\api::hook_course_module_deleted($cm);
4787 // Delete all tag instances associated with the instance of this module.
4788 core_tag_tag::delete_instances("mod_{$modname}", null, context_module::instance($cm->id)->id);
4789 core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id);
4791 if (function_exists($moddelete)) {
4792 // This purges all module data in related tables, extra user prefs, settings, etc.
4793 $moddelete($cm->modinstance);
4794 } else {
4795 // NOTE: we should not allow installation of modules with missing delete support!
4796 debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!");
4797 $DB->delete_records($modname, array('id' => $cm->modinstance));
4800 if ($cm->id) {
4801 // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition.
4802 context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4803 $DB->delete_records('course_modules_completion', ['coursemoduleid' => $cm->id]);
4804 $DB->delete_records('course_modules_viewed', ['coursemoduleid' => $cm->id]);
4805 $DB->delete_records('course_modules', array('id' => $cm->id));
4806 rebuild_course_cache($cm->course, true);
4810 if ($instances and $showfeedback) {
4811 echo $OUTPUT->notification($strdeleted.get_string('pluginname', $modname), 'notifysuccess');
4813 } else {
4814 // Ooops, this module is not properly installed, force-delete it in the next block.
4818 // We have tried to delete everything the nice way - now let's force-delete any remaining module data.
4820 // Delete completion defaults.
4821 $DB->delete_records("course_completion_defaults", array("course" => $courseid));
4823 // Remove all data from availability and completion tables that is associated
4824 // with course-modules belonging to this course. Note this is done even if the
4825 // features are not enabled now, in case they were enabled previously.
4826 $DB->delete_records_subquery('course_modules_completion', 'coursemoduleid', 'id',
4827 'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
4828 $DB->delete_records_subquery('course_modules_viewed', 'coursemoduleid', 'id',
4829 'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
4831 // Remove course-module data that has not been removed in modules' _delete_instance callbacks.
4832 $cms = $DB->get_records('course_modules', array('course' => $course->id));
4833 $allmodulesbyid = array_flip($allmodules);
4834 foreach ($cms as $cm) {
4835 if (array_key_exists($cm->module, $allmodulesbyid)) {
4836 try {
4837 $DB->delete_records($allmodulesbyid[$cm->module], array('id' => $cm->instance));
4838 } catch (Exception $e) {
4839 // Ignore weird or missing table problems.
4842 context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4843 $DB->delete_records('course_modules', array('id' => $cm->id));
4844 rebuild_course_cache($cm->course, true);
4847 if ($showfeedback) {
4848 echo $OUTPUT->notification($strdeleted.get_string('type_mod_plural', 'plugin'), 'notifysuccess');
4851 // Delete questions and question categories.
4852 question_delete_course($course);
4853 if ($showfeedback) {
4854 echo $OUTPUT->notification($strdeleted.get_string('questions', 'question'), 'notifysuccess');
4857 // Delete content bank contents.
4858 $cb = new \core_contentbank\contentbank();
4859 $cbdeleted = $cb->delete_contents($coursecontext);
4860 if ($showfeedback && $cbdeleted) {
4861 echo $OUTPUT->notification($strdeleted.get_string('contentbank', 'contentbank'), 'notifysuccess');
4864 // Make sure there are no subcontexts left - all valid blocks and modules should be already gone.
4865 $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
4866 foreach ($childcontexts as $childcontext) {
4867 $childcontext->delete();
4869 unset($childcontexts);
4871 // Remove roles and enrolments by default.
4872 if (empty($options['keep_roles_and_enrolments'])) {
4873 // This hack is used in restore when deleting contents of existing course.
4874 // During restore, we should remove only enrolment related data that the user performing the restore has a
4875 // permission to remove.
4876 $userid = $options['userid'] ?? null;
4877 enrol_course_delete($course, $userid);
4878 role_unassign_all(array('contextid' => $coursecontext->id, 'component' => ''), true);
4879 if ($showfeedback) {
4880 echo $OUTPUT->notification($strdeleted.get_string('type_enrol_plural', 'plugin'), 'notifysuccess');
4884 // Delete any groups, removing members and grouping/course links first.
4885 if (empty($options['keep_groups_and_groupings'])) {
4886 groups_delete_groupings($course->id, $showfeedback);
4887 groups_delete_groups($course->id, $showfeedback);
4890 // Filters be gone!
4891 filter_delete_all_for_context($coursecontext->id);
4893 // Notes, you shall not pass!
4894 note_delete_all($course->id);
4896 // Die comments!
4897 comment::delete_comments($coursecontext->id);
4899 // Ratings are history too.
4900 $delopt = new stdclass();
4901 $delopt->contextid = $coursecontext->id;
4902 $rm = new rating_manager();
4903 $rm->delete_ratings($delopt);
4905 // Delete course tags.
4906 core_tag_tag::remove_all_item_tags('core', 'course', $course->id);
4908 // Give the course format the opportunity to remove its obscure data.
4909 $format = course_get_format($course);
4910 $format->delete_format_data();
4912 // Notify the competency subsystem.
4913 \core_competency\api::hook_course_deleted($course);
4915 // Delete calendar events.
4916 $DB->delete_records('event', array('courseid' => $course->id));
4917 $fs->delete_area_files($coursecontext->id, 'calendar');
4919 // Delete all related records in other core tables that may have a courseid
4920 // This array stores the tables that need to be cleared, as
4921 // table_name => column_name that contains the course id.
4922 $tablestoclear = array(
4923 'backup_courses' => 'courseid', // Scheduled backup stuff.
4924 'user_lastaccess' => 'courseid', // User access info.
4926 foreach ($tablestoclear as $table => $col) {
4927 $DB->delete_records($table, array($col => $course->id));
4930 // Delete all course backup files.
4931 $fs->delete_area_files($coursecontext->id, 'backup');
4933 // Cleanup course record - remove links to deleted stuff.
4934 // Do not wipe cacherev, as this course might be reused and we need to ensure that it keeps
4935 // increasing.
4936 $oldcourse = new stdClass();
4937 $oldcourse->id = $course->id;
4938 $oldcourse->summary = '';
4939 $oldcourse->legacyfiles = 0;
4940 if (!empty($options['keep_groups_and_groupings'])) {
4941 $oldcourse->defaultgroupingid = 0;
4943 $DB->update_record('course', $oldcourse);
4945 // Delete course sections.
4946 $DB->delete_records('course_sections', array('course' => $course->id));
4948 // Delete legacy, section and any other course files.
4949 $fs->delete_area_files($coursecontext->id, 'course'); // Files from summary and section.
4951 // Delete all remaining stuff linked to context such as files, comments, ratings, etc.
4952 if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) {
4953 // Easy, do not delete the context itself...
4954 $coursecontext->delete_content();
4955 } else {
4956 // Hack alert!!!!
4957 // We can not drop all context stuff because it would bork enrolments and roles,
4958 // there might be also files used by enrol plugins...
4961 // Delete legacy files - just in case some files are still left there after conversion to new file api,
4962 // also some non-standard unsupported plugins may try to store something there.
4963 fulldelete($CFG->dataroot.'/'.$course->id);
4965 // Delete from cache to reduce the cache size especially makes sense in case of bulk course deletion.
4966 course_modinfo::purge_course_cache($courseid);
4968 // Trigger a course content deleted event.
4969 $event = \core\event\course_content_deleted::create(array(
4970 'objectid' => $course->id,
4971 'context' => $coursecontext,
4972 'other' => array('shortname' => $course->shortname,
4973 'fullname' => $course->fullname,
4974 'options' => $options) // Passing this for legacy reasons.
4976 $event->add_record_snapshot('course', $course);
4977 $event->trigger();
4979 return true;
4983 * Change dates in module - used from course reset.
4985 * @param string $modname forum, assignment, etc
4986 * @param array $fields array of date fields from mod table
4987 * @param int $timeshift time difference
4988 * @param int $courseid
4989 * @param int $modid (Optional) passed if specific mod instance in course needs to be updated.
4990 * @return bool success
4992 function shift_course_mod_dates($modname, $fields, $timeshift, $courseid, $modid = 0) {
4993 global $CFG, $DB;
4994 include_once($CFG->dirroot.'/mod/'.$modname.'/lib.php');
4996 $return = true;
4997 $params = array($timeshift, $courseid);
4998 foreach ($fields as $field) {
4999 $updatesql = "UPDATE {".$modname."}
5000 SET $field = $field + ?
5001 WHERE course=? AND $field<>0";
5002 if ($modid) {
5003 $updatesql .= ' AND id=?';
5004 $params[] = $modid;
5006 $return = $DB->execute($updatesql, $params) && $return;
5009 return $return;
5013 * This function will empty a course of user data.
5014 * It will retain the activities and the structure of the course.
5016 * @param object $data an object containing all the settings including courseid (without magic quotes)
5017 * @return array status array of array component, item, error
5019 function reset_course_userdata($data) {
5020 global $CFG, $DB;
5021 require_once($CFG->libdir.'/gradelib.php');
5022 require_once($CFG->libdir.'/completionlib.php');
5023 require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php');
5024 require_once($CFG->dirroot.'/group/lib.php');
5026 $data->courseid = $data->id;
5027 $context = context_course::instance($data->courseid);
5029 $eventparams = array(
5030 'context' => $context,
5031 'courseid' => $data->id,
5032 'other' => array(
5033 'reset_options' => (array) $data
5036 $event = \core\event\course_reset_started::create($eventparams);
5037 $event->trigger();
5039 // Calculate the time shift of dates.
5040 if (!empty($data->reset_start_date)) {
5041 // Time part of course startdate should be zero.
5042 $data->timeshift = $data->reset_start_date - usergetmidnight($data->reset_start_date_old);
5043 } else {
5044 $data->timeshift = 0;
5047 // Result array: component, item, error.
5048 $status = array();
5050 // Start the resetting.
5051 $componentstr = get_string('general');
5053 // Move the course start time.
5054 if (!empty($data->reset_start_date) and $data->timeshift) {
5055 // Change course start data.
5056 $DB->set_field('course', 'startdate', $data->reset_start_date, array('id' => $data->courseid));
5057 // Update all course and group events - do not move activity events.
5058 $updatesql = "UPDATE {event}
5059 SET timestart = timestart + ?
5060 WHERE courseid=? AND instance=0";
5061 $DB->execute($updatesql, array($data->timeshift, $data->courseid));
5063 // Update any date activity restrictions.
5064 if ($CFG->enableavailability) {
5065 \availability_date\condition::update_all_dates($data->courseid, $data->timeshift);
5068 // Update completion expected dates.
5069 if ($CFG->enablecompletion) {
5070 $modinfo = get_fast_modinfo($data->courseid);
5071 $changed = false;
5072 foreach ($modinfo->get_cms() as $cm) {
5073 if ($cm->completion && !empty($cm->completionexpected)) {
5074 $DB->set_field('course_modules', 'completionexpected', $cm->completionexpected + $data->timeshift,
5075 array('id' => $cm->id));
5076 $changed = true;
5080 // Clear course cache if changes made.
5081 if ($changed) {
5082 rebuild_course_cache($data->courseid, true);
5085 // Update course date completion criteria.
5086 \completion_criteria_date::update_date($data->courseid, $data->timeshift);
5089 $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false);
5092 if (!empty($data->reset_end_date)) {
5093 // If the user set a end date value respect it.
5094 $DB->set_field('course', 'enddate', $data->reset_end_date, array('id' => $data->courseid));
5095 } else if ($data->timeshift > 0 && $data->reset_end_date_old) {
5096 // If there is a time shift apply it to the end date as well.
5097 $enddate = $data->reset_end_date_old + $data->timeshift;
5098 $DB->set_field('course', 'enddate', $enddate, array('id' => $data->courseid));
5101 if (!empty($data->reset_events)) {
5102 $DB->delete_records('event', array('courseid' => $data->courseid));
5103 $status[] = array('component' => $componentstr, 'item' => get_string('deleteevents', 'calendar'), 'error' => false);
5106 if (!empty($data->reset_notes)) {
5107 require_once($CFG->dirroot.'/notes/lib.php');
5108 note_delete_all($data->courseid);
5109 $status[] = array('component' => $componentstr, 'item' => get_string('deletenotes', 'notes'), 'error' => false);
5112 if (!empty($data->delete_blog_associations)) {
5113 require_once($CFG->dirroot.'/blog/lib.php');
5114 blog_remove_associations_for_course($data->courseid);
5115 $status[] = array('component' => $componentstr, 'item' => get_string('deleteblogassociations', 'blog'), 'error' => false);
5118 if (!empty($data->reset_completion)) {
5119 // Delete course and activity completion information.
5120 $course = $DB->get_record('course', array('id' => $data->courseid));
5121 $cc = new completion_info($course);
5122 $cc->delete_all_completion_data();
5123 $status[] = array('component' => $componentstr,
5124 'item' => get_string('deletecompletiondata', 'completion'), 'error' => false);
5127 if (!empty($data->reset_competency_ratings)) {
5128 \core_competency\api::hook_course_reset_competency_ratings($data->courseid);
5129 $status[] = array('component' => $componentstr,
5130 'item' => get_string('deletecompetencyratings', 'core_competency'), 'error' => false);
5133 $componentstr = get_string('roles');
5135 if (!empty($data->reset_roles_overrides)) {
5136 $children = $context->get_child_contexts();
5137 foreach ($children as $child) {
5138 $child->delete_capabilities();
5140 $context->delete_capabilities();
5141 $status[] = array('component' => $componentstr, 'item' => get_string('deletecourseoverrides', 'role'), 'error' => false);
5144 if (!empty($data->reset_roles_local)) {
5145 $children = $context->get_child_contexts();
5146 foreach ($children as $child) {
5147 role_unassign_all(array('contextid' => $child->id));
5149 $status[] = array('component' => $componentstr, 'item' => get_string('deletelocalroles', 'role'), 'error' => false);
5152 // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc.
5153 $data->unenrolled = array();
5154 if (!empty($data->unenrol_users)) {
5155 $plugins = enrol_get_plugins(true);
5156 $instances = enrol_get_instances($data->courseid, true);
5157 foreach ($instances as $key => $instance) {
5158 if (!isset($plugins[$instance->enrol])) {
5159 unset($instances[$key]);
5160 continue;
5164 $usersroles = enrol_get_course_users_roles($data->courseid);
5165 foreach ($data->unenrol_users as $withroleid) {
5166 if ($withroleid) {
5167 $sql = "SELECT ue.*
5168 FROM {user_enrolments} ue
5169 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5170 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5171 JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)";
5172 $params = array('courseid' => $data->courseid, 'roleid' => $withroleid, 'courselevel' => CONTEXT_COURSE);
5174 } else {
5175 // Without any role assigned at course context.
5176 $sql = "SELECT ue.*
5177 FROM {user_enrolments} ue
5178 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5179 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5180 LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid)
5181 WHERE ra.id IS null";
5182 $params = array('courseid' => $data->courseid, 'courselevel' => CONTEXT_COURSE);
5185 $rs = $DB->get_recordset_sql($sql, $params);
5186 foreach ($rs as $ue) {
5187 if (!isset($instances[$ue->enrolid])) {
5188 continue;
5190 $instance = $instances[$ue->enrolid];
5191 $plugin = $plugins[$instance->enrol];
5192 if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) {
5193 continue;
5196 if ($withroleid && count($usersroles[$ue->userid]) > 1) {
5197 // If we don't remove all roles and user has more than one role, just remove this role.
5198 role_unassign($withroleid, $ue->userid, $context->id);
5200 unset($usersroles[$ue->userid][$withroleid]);
5201 } else {
5202 // If we remove all roles or user has only one role, unenrol user from course.
5203 $plugin->unenrol_user($instance, $ue->userid);
5205 $data->unenrolled[$ue->userid] = $ue->userid;
5207 $rs->close();
5210 if (!empty($data->unenrolled)) {
5211 $status[] = array(
5212 'component' => $componentstr,
5213 'item' => get_string('unenrol', 'enrol').' ('.count($data->unenrolled).')',
5214 'error' => false
5218 $componentstr = get_string('groups');
5220 // Remove all group members.
5221 if (!empty($data->reset_groups_members)) {
5222 groups_delete_group_members($data->courseid);
5223 $status[] = array('component' => $componentstr, 'item' => get_string('removegroupsmembers', 'group'), 'error' => false);
5226 // Remove all groups.
5227 if (!empty($data->reset_groups_remove)) {
5228 groups_delete_groups($data->courseid, false);
5229 $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroups', 'group'), 'error' => false);
5232 // Remove all grouping members.
5233 if (!empty($data->reset_groupings_members)) {
5234 groups_delete_groupings_groups($data->courseid, false);
5235 $status[] = array('component' => $componentstr, 'item' => get_string('removegroupingsmembers', 'group'), 'error' => false);
5238 // Remove all groupings.
5239 if (!empty($data->reset_groupings_remove)) {
5240 groups_delete_groupings($data->courseid, false);
5241 $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroupings', 'group'), 'error' => false);
5244 // Look in every instance of every module for data to delete.
5245 $unsupportedmods = array();
5246 if ($allmods = $DB->get_records('modules') ) {
5247 foreach ($allmods as $mod) {
5248 $modname = $mod->name;
5249 $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php';
5250 $moddeleteuserdata = $modname.'_reset_userdata'; // Function to delete user data.
5251 if (file_exists($modfile)) {
5252 if (!$DB->count_records($modname, array('course' => $data->courseid))) {
5253 continue; // Skip mods with no instances.
5255 include_once($modfile);
5256 if (function_exists($moddeleteuserdata)) {
5257 $modstatus = $moddeleteuserdata($data);
5258 if (is_array($modstatus)) {
5259 $status = array_merge($status, $modstatus);
5260 } else {
5261 debugging('Module '.$modname.' returned incorrect staus - must be an array!');
5263 } else {
5264 $unsupportedmods[] = $mod;
5266 } else {
5267 debugging('Missing lib.php in '.$modname.' module!');
5269 // Update calendar events for all modules.
5270 course_module_bulk_update_calendar_events($modname, $data->courseid);
5272 // Purge the course cache after resetting course start date. MDL-76936
5273 if ($data->timeshift) {
5274 course_modinfo::purge_course_cache($data->courseid);
5278 // Mention unsupported mods.
5279 if (!empty($unsupportedmods)) {
5280 foreach ($unsupportedmods as $mod) {
5281 $status[] = array(
5282 'component' => get_string('modulenameplural', $mod->name),
5283 'item' => '',
5284 'error' => get_string('resetnotimplemented')
5289 $componentstr = get_string('gradebook', 'grades');
5290 // Reset gradebook,.
5291 if (!empty($data->reset_gradebook_items)) {
5292 remove_course_grades($data->courseid, false);
5293 grade_grab_course_grades($data->courseid);
5294 grade_regrade_final_grades($data->courseid);
5295 $status[] = array('component' => $componentstr, 'item' => get_string('removeallcourseitems', 'grades'), 'error' => false);
5297 } else if (!empty($data->reset_gradebook_grades)) {
5298 grade_course_reset($data->courseid);
5299 $status[] = array('component' => $componentstr, 'item' => get_string('removeallcoursegrades', 'grades'), 'error' => false);
5301 // Reset comments.
5302 if (!empty($data->reset_comments)) {
5303 require_once($CFG->dirroot.'/comment/lib.php');
5304 comment::reset_course_page_comments($context);
5307 $event = \core\event\course_reset_ended::create($eventparams);
5308 $event->trigger();
5310 return $status;
5314 * Generate an email processing address.
5316 * @param int $modid
5317 * @param string $modargs
5318 * @return string Returns email processing address
5320 function generate_email_processing_address($modid, $modargs) {
5321 global $CFG;
5323 $header = $CFG->mailprefix . substr(base64_encode(pack('C', $modid)), 0, 2).$modargs;
5324 return $header . substr(md5($header.get_site_identifier()), 0, 16).'@'.$CFG->maildomain;
5330 * @todo Finish documenting this function
5332 * @param string $modargs
5333 * @param string $body Currently unused
5335 function moodle_process_email($modargs, $body) {
5336 global $DB;
5338 // The first char should be an unencoded letter. We'll take this as an action.
5339 switch ($modargs[0]) {
5340 case 'B': { // Bounce.
5341 list(, $userid) = unpack('V', base64_decode(substr($modargs, 1, 8)));
5342 if ($user = $DB->get_record("user", array('id' => $userid), "id,email")) {
5343 // Check the half md5 of their email.
5344 $md5check = substr(md5($user->email), 0, 16);
5345 if ($md5check == substr($modargs, -16)) {
5346 set_bounce_count($user);
5348 // Else maybe they've already changed it?
5351 break;
5352 // Maybe more later?
5356 // CORRESPONDENCE.
5359 * Get mailer instance, enable buffering, flush buffer or disable buffering.
5361 * @param string $action 'get', 'buffer', 'close' or 'flush'
5362 * @return moodle_phpmailer|null mailer instance if 'get' used or nothing
5364 function get_mailer($action='get') {
5365 global $CFG;
5367 /** @var moodle_phpmailer $mailer */
5368 static $mailer = null;
5369 static $counter = 0;
5371 if (!isset($CFG->smtpmaxbulk)) {
5372 $CFG->smtpmaxbulk = 1;
5375 if ($action == 'get') {
5376 $prevkeepalive = false;
5378 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5379 if ($counter < $CFG->smtpmaxbulk and !$mailer->isError()) {
5380 $counter++;
5381 // Reset the mailer.
5382 $mailer->Priority = 3;
5383 $mailer->CharSet = 'UTF-8'; // Our default.
5384 $mailer->ContentType = "text/plain";
5385 $mailer->Encoding = "8bit";
5386 $mailer->From = "root@localhost";
5387 $mailer->FromName = "Root User";
5388 $mailer->Sender = "";
5389 $mailer->Subject = "";
5390 $mailer->Body = "";
5391 $mailer->AltBody = "";
5392 $mailer->ConfirmReadingTo = "";
5394 $mailer->clearAllRecipients();
5395 $mailer->clearReplyTos();
5396 $mailer->clearAttachments();
5397 $mailer->clearCustomHeaders();
5398 return $mailer;
5401 $prevkeepalive = $mailer->SMTPKeepAlive;
5402 get_mailer('flush');
5405 require_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php');
5406 $mailer = new moodle_phpmailer();
5408 $counter = 1;
5410 if ($CFG->smtphosts == 'qmail') {
5411 // Use Qmail system.
5412 $mailer->isQmail();
5414 } else if (empty($CFG->smtphosts)) {
5415 // Use PHP mail() = sendmail.
5416 $mailer->isMail();
5418 } else {
5419 // Use SMTP directly.
5420 $mailer->isSMTP();
5421 if (!empty($CFG->debugsmtp) && (!empty($CFG->debugdeveloper))) {
5422 $mailer->SMTPDebug = 3;
5424 // Specify main and backup servers.
5425 $mailer->Host = $CFG->smtphosts;
5426 // Specify secure connection protocol.
5427 $mailer->SMTPSecure = $CFG->smtpsecure;
5428 // Use previous keepalive.
5429 $mailer->SMTPKeepAlive = $prevkeepalive;
5431 if ($CFG->smtpuser) {
5432 // Use SMTP authentication.
5433 $mailer->SMTPAuth = true;
5434 $mailer->Username = $CFG->smtpuser;
5435 $mailer->Password = $CFG->smtppass;
5439 return $mailer;
5442 $nothing = null;
5444 // Keep smtp session open after sending.
5445 if ($action == 'buffer') {
5446 if (!empty($CFG->smtpmaxbulk)) {
5447 get_mailer('flush');
5448 $m = get_mailer();
5449 if ($m->Mailer == 'smtp') {
5450 $m->SMTPKeepAlive = true;
5453 return $nothing;
5456 // Close smtp session, but continue buffering.
5457 if ($action == 'flush') {
5458 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5459 if (!empty($mailer->SMTPDebug)) {
5460 echo '<pre>'."\n";
5462 $mailer->SmtpClose();
5463 if (!empty($mailer->SMTPDebug)) {
5464 echo '</pre>';
5467 return $nothing;
5470 // Close smtp session, do not buffer anymore.
5471 if ($action == 'close') {
5472 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5473 get_mailer('flush');
5474 $mailer->SMTPKeepAlive = false;
5476 $mailer = null; // Better force new instance.
5477 return $nothing;
5482 * A helper function to test for email diversion
5484 * @param string $email
5485 * @return bool Returns true if the email should be diverted
5487 function email_should_be_diverted($email) {
5488 global $CFG;
5490 if (empty($CFG->divertallemailsto)) {
5491 return false;
5494 if (empty($CFG->divertallemailsexcept)) {
5495 return true;
5498 $patterns = array_map('trim', preg_split("/[\s,]+/", $CFG->divertallemailsexcept, -1, PREG_SPLIT_NO_EMPTY));
5499 foreach ($patterns as $pattern) {
5500 if (preg_match("/{$pattern}/i", $email)) {
5501 return false;
5505 return true;
5509 * Generate a unique email Message-ID using the moodle domain and install path
5511 * @param string $localpart An optional unique message id prefix.
5512 * @return string The formatted ID ready for appending to the email headers.
5514 function generate_email_messageid($localpart = null) {
5515 global $CFG;
5517 $urlinfo = parse_url($CFG->wwwroot);
5518 $base = '@' . $urlinfo['host'];
5520 // If multiple moodles are on the same domain we want to tell them
5521 // apart so we add the install path to the local part. This means
5522 // that the id local part should never contain a / character so
5523 // we can correctly parse the id to reassemble the wwwroot.
5524 if (isset($urlinfo['path'])) {
5525 $base = $urlinfo['path'] . $base;
5528 if (empty($localpart)) {
5529 $localpart = uniqid('', true);
5532 // Because we may have an option /installpath suffix to the local part
5533 // of the id we need to escape any / chars which are in the $localpart.
5534 $localpart = str_replace('/', '%2F', $localpart);
5536 return '<' . $localpart . $base . '>';
5540 * Send an email to a specified user
5542 * @param stdClass $user A {@link $USER} object
5543 * @param stdClass $from A {@link $USER} object
5544 * @param string $subject plain text subject line of the email
5545 * @param string $messagetext plain text version of the message
5546 * @param string $messagehtml complete html version of the message (optional)
5547 * @param string $attachment a file on the filesystem, either relative to $CFG->dataroot or a full path to a file in one of
5548 * the following directories: $CFG->cachedir, $CFG->dataroot, $CFG->dirroot, $CFG->localcachedir, $CFG->tempdir
5549 * @param string $attachname the name of the file (extension indicates MIME)
5550 * @param bool $usetrueaddress determines whether $from email address should
5551 * be sent out. Will be overruled by user profile setting for maildisplay
5552 * @param string $replyto Email address to reply to
5553 * @param string $replytoname Name of reply to recipient
5554 * @param int $wordwrapwidth custom word wrap width, default 79
5555 * @return bool Returns true if mail was sent OK and false if there was an error.
5557 function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '', $attachment = '', $attachname = '',
5558 $usetrueaddress = true, $replyto = '', $replytoname = '', $wordwrapwidth = 79) {
5560 global $CFG, $PAGE, $SITE;
5562 if (empty($user) or empty($user->id)) {
5563 debugging('Can not send email to null user', DEBUG_DEVELOPER);
5564 return false;
5567 if (empty($user->email)) {
5568 debugging('Can not send email to user without email: '.$user->id, DEBUG_DEVELOPER);
5569 return false;
5572 if (!empty($user->deleted)) {
5573 debugging('Can not send email to deleted user: '.$user->id, DEBUG_DEVELOPER);
5574 return false;
5577 if (defined('BEHAT_SITE_RUNNING')) {
5578 // Fake email sending in behat.
5579 return true;
5582 if (!empty($CFG->noemailever)) {
5583 // Hidden setting for development sites, set in config.php if needed.
5584 debugging('Not sending email due to $CFG->noemailever config setting', DEBUG_NORMAL);
5585 return true;
5588 if (email_should_be_diverted($user->email)) {
5589 $subject = "[DIVERTED {$user->email}] $subject";
5590 $user = clone($user);
5591 $user->email = $CFG->divertallemailsto;
5594 // Skip mail to suspended users.
5595 if ((isset($user->auth) && $user->auth=='nologin') or (isset($user->suspended) && $user->suspended)) {
5596 return true;
5599 if (!validate_email($user->email)) {
5600 // We can not send emails to invalid addresses - it might create security issue or confuse the mailer.
5601 debugging("email_to_user: User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending.");
5602 return false;
5605 if (over_bounce_threshold($user)) {
5606 debugging("email_to_user: User $user->id (".fullname($user).") is over bounce threshold! Not sending.");
5607 return false;
5610 // TLD .invalid is specifically reserved for invalid domain names.
5611 // For More information, see {@link http://tools.ietf.org/html/rfc2606#section-2}.
5612 if (substr($user->email, -8) == '.invalid') {
5613 debugging("email_to_user: User $user->id (".fullname($user).") email domain ($user->email) is invalid! Not sending.");
5614 return true; // This is not an error.
5617 // If the user is a remote mnet user, parse the email text for URL to the
5618 // wwwroot and modify the url to direct the user's browser to login at their
5619 // home site (identity provider - idp) before hitting the link itself.
5620 if (is_mnet_remote_user($user)) {
5621 require_once($CFG->dirroot.'/mnet/lib.php');
5623 $jumpurl = mnet_get_idp_jump_url($user);
5624 $callback = partial('mnet_sso_apply_indirection', $jumpurl);
5626 $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%",
5627 $callback,
5628 $messagetext);
5629 $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%",
5630 $callback,
5631 $messagehtml);
5633 $mail = get_mailer();
5635 if (!empty($mail->SMTPDebug)) {
5636 echo '<pre>' . "\n";
5639 $temprecipients = array();
5640 $tempreplyto = array();
5642 // Make sure that we fall back onto some reasonable no-reply address.
5643 $noreplyaddressdefault = 'noreply@' . get_host_from_url($CFG->wwwroot);
5644 $noreplyaddress = empty($CFG->noreplyaddress) ? $noreplyaddressdefault : $CFG->noreplyaddress;
5646 if (!validate_email($noreplyaddress)) {
5647 debugging('email_to_user: Invalid noreply-email '.s($noreplyaddress));
5648 $noreplyaddress = $noreplyaddressdefault;
5651 // Make up an email address for handling bounces.
5652 if (!empty($CFG->handlebounces)) {
5653 $modargs = 'B'.base64_encode(pack('V', $user->id)).substr(md5($user->email), 0, 16);
5654 $mail->Sender = generate_email_processing_address(0, $modargs);
5655 } else {
5656 $mail->Sender = $noreplyaddress;
5659 // Make sure that the explicit replyto is valid, fall back to the implicit one.
5660 if (!empty($replyto) && !validate_email($replyto)) {
5661 debugging('email_to_user: Invalid replyto-email '.s($replyto));
5662 $replyto = $noreplyaddress;
5665 if (is_string($from)) { // So we can pass whatever we want if there is need.
5666 $mail->From = $noreplyaddress;
5667 $mail->FromName = $from;
5668 // Check if using the true address is true, and the email is in the list of allowed domains for sending email,
5669 // and that the senders email setting is either displayed to everyone, or display to only other users that are enrolled
5670 // in a course with the sender.
5671 } else if ($usetrueaddress && can_send_from_real_email_address($from, $user)) {
5672 if (!validate_email($from->email)) {
5673 debugging('email_to_user: Invalid from-email '.s($from->email).' - not sending');
5674 // Better not to use $noreplyaddress in this case.
5675 return false;
5677 $mail->From = $from->email;
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_ALWAYS) {
5684 $fromstring = get_string('emailvia', 'core', $fromdetails);
5686 $mail->FromName = $fromstring;
5687 if (empty($replyto)) {
5688 $tempreplyto[] = array($from->email, fullname($from));
5690 } else {
5691 $mail->From = $noreplyaddress;
5692 $fromdetails = new stdClass();
5693 $fromdetails->name = fullname($from);
5694 $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
5695 $fromdetails->siteshortname = format_string($SITE->shortname);
5696 $fromstring = $fromdetails->name;
5697 if ($CFG->emailfromvia != EMAIL_VIA_NEVER) {
5698 $fromstring = get_string('emailvia', 'core', $fromdetails);
5700 $mail->FromName = $fromstring;
5701 if (empty($replyto)) {
5702 $tempreplyto[] = array($noreplyaddress, get_string('noreplyname'));
5706 if (!empty($replyto)) {
5707 $tempreplyto[] = array($replyto, $replytoname);
5710 $temprecipients[] = array($user->email, fullname($user));
5712 // Set word wrap.
5713 $mail->WordWrap = $wordwrapwidth;
5715 if (!empty($from->customheaders)) {
5716 // Add custom headers.
5717 if (is_array($from->customheaders)) {
5718 foreach ($from->customheaders as $customheader) {
5719 $mail->addCustomHeader($customheader);
5721 } else {
5722 $mail->addCustomHeader($from->customheaders);
5726 // If the X-PHP-Originating-Script email header is on then also add an additional
5727 // header with details of where exactly in moodle the email was triggered from,
5728 // either a call to message_send() or to email_to_user().
5729 if (ini_get('mail.add_x_header')) {
5731 $stack = debug_backtrace(false);
5732 $origin = $stack[0];
5734 foreach ($stack as $depth => $call) {
5735 if ($call['function'] == 'message_send') {
5736 $origin = $call;
5740 $originheader = $CFG->wwwroot . ' => ' . gethostname() . ':'
5741 . str_replace($CFG->dirroot . '/', '', $origin['file']) . ':' . $origin['line'];
5742 $mail->addCustomHeader('X-Moodle-Originating-Script: ' . $originheader);
5745 if (!empty($CFG->emailheaders)) {
5746 $headers = array_map('trim', explode("\n", $CFG->emailheaders));
5747 foreach ($headers as $header) {
5748 if (!empty($header)) {
5749 $mail->addCustomHeader($header);
5754 if (!empty($from->priority)) {
5755 $mail->Priority = $from->priority;
5758 $renderer = $PAGE->get_renderer('core');
5759 $context = array(
5760 'sitefullname' => $SITE->fullname,
5761 'siteshortname' => $SITE->shortname,
5762 'sitewwwroot' => $CFG->wwwroot,
5763 'subject' => $subject,
5764 'prefix' => $CFG->emailsubjectprefix,
5765 'to' => $user->email,
5766 'toname' => fullname($user),
5767 'from' => $mail->From,
5768 'fromname' => $mail->FromName,
5770 if (!empty($tempreplyto[0])) {
5771 $context['replyto'] = $tempreplyto[0][0];
5772 $context['replytoname'] = $tempreplyto[0][1];
5774 if ($user->id > 0) {
5775 $context['touserid'] = $user->id;
5776 $context['tousername'] = $user->username;
5779 if (!empty($user->mailformat) && $user->mailformat == 1) {
5780 // Only process html templates if the user preferences allow html email.
5782 if (!$messagehtml) {
5783 // If no html has been given, BUT there is an html wrapping template then
5784 // auto convert the text to html and then wrap it.
5785 $messagehtml = trim(text_to_html($messagetext));
5787 $context['body'] = $messagehtml;
5788 $messagehtml = $renderer->render_from_template('core/email_html', $context);
5791 $context['body'] = html_to_text(nl2br($messagetext));
5792 $mail->Subject = $renderer->render_from_template('core/email_subject', $context);
5793 $mail->FromName = $renderer->render_from_template('core/email_fromname', $context);
5794 $messagetext = $renderer->render_from_template('core/email_text', $context);
5796 // Autogenerate a MessageID if it's missing.
5797 if (empty($mail->MessageID)) {
5798 $mail->MessageID = generate_email_messageid();
5801 if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) {
5802 // Don't ever send HTML to users who don't want it.
5803 $mail->isHTML(true);
5804 $mail->Encoding = 'quoted-printable';
5805 $mail->Body = $messagehtml;
5806 $mail->AltBody = "\n$messagetext\n";
5807 } else {
5808 $mail->IsHTML(false);
5809 $mail->Body = "\n$messagetext\n";
5812 if ($attachment && $attachname) {
5813 if (preg_match( "~\\.\\.~" , $attachment )) {
5814 // Security check for ".." in dir path.
5815 $supportuser = core_user::get_support_user();
5816 $temprecipients[] = array($supportuser->email, fullname($supportuser, true));
5817 $mail->addStringAttachment('Error in attachment. User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain');
5818 } else {
5819 require_once($CFG->libdir.'/filelib.php');
5820 $mimetype = mimeinfo('type', $attachname);
5822 // Before doing the comparison, make sure that the paths are correct (Windows uses slashes in the other direction).
5823 // The absolute (real) path is also fetched to ensure that comparisons to allowed paths are compared equally.
5824 $attachpath = str_replace('\\', '/', realpath($attachment));
5826 // Build an array of all filepaths from which attachments can be added (normalised slashes, absolute/real path).
5827 $allowedpaths = array_map(function(string $path): string {
5828 return str_replace('\\', '/', realpath($path));
5829 }, [
5830 $CFG->cachedir,
5831 $CFG->dataroot,
5832 $CFG->dirroot,
5833 $CFG->localcachedir,
5834 $CFG->tempdir,
5835 $CFG->localrequestdir,
5838 // Set addpath to true.
5839 $addpath = true;
5841 // Check if attachment includes one of the allowed paths.
5842 foreach (array_filter($allowedpaths) as $allowedpath) {
5843 // Set addpath to false if the attachment includes one of the allowed paths.
5844 if (strpos($attachpath, $allowedpath) === 0) {
5845 $addpath = false;
5846 break;
5850 // If the attachment is a full path to a file in the multiple allowed paths, use it as is,
5851 // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons).
5852 if ($addpath == true) {
5853 $attachment = $CFG->dataroot . '/' . $attachment;
5856 $mail->addAttachment($attachment, $attachname, 'base64', $mimetype);
5860 // Check if the email should be sent in an other charset then the default UTF-8.
5861 if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) {
5863 // Use the defined site mail charset or eventually the one preferred by the recipient.
5864 $charset = $CFG->sitemailcharset;
5865 if (!empty($CFG->allowusermailcharset)) {
5866 if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) {
5867 $charset = $useremailcharset;
5871 // Convert all the necessary strings if the charset is supported.
5872 $charsets = get_list_of_charsets();
5873 unset($charsets['UTF-8']);
5874 if (in_array($charset, $charsets)) {
5875 $mail->CharSet = $charset;
5876 $mail->FromName = core_text::convert($mail->FromName, 'utf-8', strtolower($charset));
5877 $mail->Subject = core_text::convert($mail->Subject, 'utf-8', strtolower($charset));
5878 $mail->Body = core_text::convert($mail->Body, 'utf-8', strtolower($charset));
5879 $mail->AltBody = core_text::convert($mail->AltBody, 'utf-8', strtolower($charset));
5881 foreach ($temprecipients as $key => $values) {
5882 $temprecipients[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
5884 foreach ($tempreplyto as $key => $values) {
5885 $tempreplyto[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
5890 foreach ($temprecipients as $values) {
5891 $mail->addAddress($values[0], $values[1]);
5893 foreach ($tempreplyto as $values) {
5894 $mail->addReplyTo($values[0], $values[1]);
5897 if (!empty($CFG->emaildkimselector)) {
5898 $domain = substr(strrchr($mail->From, "@"), 1);
5899 $pempath = "{$CFG->dataroot}/dkim/{$domain}/{$CFG->emaildkimselector}.private";
5900 if (file_exists($pempath)) {
5901 $mail->DKIM_domain = $domain;
5902 $mail->DKIM_private = $pempath;
5903 $mail->DKIM_selector = $CFG->emaildkimselector;
5904 $mail->DKIM_identity = $mail->From;
5905 } else {
5906 debugging("Email DKIM selector chosen due to {$mail->From} but no certificate found at $pempath", DEBUG_DEVELOPER);
5910 if ($mail->send()) {
5911 set_send_count($user);
5912 if (!empty($mail->SMTPDebug)) {
5913 echo '</pre>';
5915 return true;
5916 } else {
5917 // Trigger event for failing to send email.
5918 $event = \core\event\email_failed::create(array(
5919 'context' => context_system::instance(),
5920 'userid' => $from->id,
5921 'relateduserid' => $user->id,
5922 'other' => array(
5923 'subject' => $subject,
5924 'message' => $messagetext,
5925 'errorinfo' => $mail->ErrorInfo
5928 $event->trigger();
5929 if (CLI_SCRIPT) {
5930 mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo);
5932 if (!empty($mail->SMTPDebug)) {
5933 echo '</pre>';
5935 return false;
5940 * Check to see if a user's real email address should be used for the "From" field.
5942 * @param object $from The user object for the user we are sending the email from.
5943 * @param object $user The user object that we are sending the email to.
5944 * @param array $unused No longer used.
5945 * @return bool Returns true if we can use the from user's email adress in the "From" field.
5947 function can_send_from_real_email_address($from, $user, $unused = null) {
5948 global $CFG;
5949 if (!isset($CFG->allowedemaildomains) || empty(trim($CFG->allowedemaildomains))) {
5950 return false;
5952 $alloweddomains = array_map('trim', explode("\n", $CFG->allowedemaildomains));
5953 // Email is in the list of allowed domains for sending email,
5954 // and the senders email setting is either displayed to everyone, or display to only other users that are enrolled
5955 // in a course with the sender.
5956 if (\core\ip_utils::is_domain_in_allowed_list(substr($from->email, strpos($from->email, '@') + 1), $alloweddomains)
5957 && ($from->maildisplay == core_user::MAILDISPLAY_EVERYONE
5958 || ($from->maildisplay == core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY
5959 && enrol_get_shared_courses($user, $from, false, true)))) {
5960 return true;
5962 return false;
5966 * Generate a signoff for emails based on support settings
5968 * @return string
5970 function generate_email_signoff() {
5971 global $CFG, $OUTPUT;
5973 $signoff = "\n";
5974 if (!empty($CFG->supportname)) {
5975 $signoff .= $CFG->supportname."\n";
5978 $supportemail = $OUTPUT->supportemail(['class' => 'font-weight-bold']);
5980 if ($supportemail) {
5981 $signoff .= "\n" . $supportemail . "\n";
5984 return $signoff;
5988 * Sets specified user's password and send the new password to the user via email.
5990 * @param stdClass $user A {@link $USER} object
5991 * @param bool $fasthash If true, use a low cost factor when generating the hash for speed.
5992 * @return bool|string Returns "true" if mail was sent OK and "false" if there was an error
5994 function setnew_password_and_mail($user, $fasthash = false) {
5995 global $CFG, $DB;
5997 // We try to send the mail in language the user understands,
5998 // unfortunately the filter_string() does not support alternative langs yet
5999 // so multilang will not work properly for site->fullname.
6000 $lang = empty($user->lang) ? get_newuser_language() : $user->lang;
6002 $site = get_site();
6004 $supportuser = core_user::get_support_user();
6006 $newpassword = generate_password();
6008 update_internal_user_password($user, $newpassword, $fasthash);
6010 $a = new stdClass();
6011 $a->firstname = fullname($user, true);
6012 $a->sitename = format_string($site->fullname);
6013 $a->username = $user->username;
6014 $a->newpassword = $newpassword;
6015 $a->link = $CFG->wwwroot .'/login/?lang='.$lang;
6016 $a->signoff = generate_email_signoff();
6018 $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang);
6020 $subject = format_string($site->fullname) .': '. (string)new lang_string('newusernewpasswordsubj', '', $a, $lang);
6022 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6023 return email_to_user($user, $supportuser, $subject, $message);
6028 * Resets specified user's password and send the new password to the user via email.
6030 * @param stdClass $user A {@link $USER} object
6031 * @return bool Returns true if mail was sent OK and false if there was an error.
6033 function reset_password_and_mail($user) {
6034 global $CFG;
6036 $site = get_site();
6037 $supportuser = core_user::get_support_user();
6039 $userauth = get_auth_plugin($user->auth);
6040 if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) {
6041 trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth.");
6042 return false;
6045 $newpassword = generate_password();
6047 if (!$userauth->user_update_password($user, $newpassword)) {
6048 throw new \moodle_exception("cannotsetpassword");
6051 $a = new stdClass();
6052 $a->firstname = $user->firstname;
6053 $a->lastname = $user->lastname;
6054 $a->sitename = format_string($site->fullname);
6055 $a->username = $user->username;
6056 $a->newpassword = $newpassword;
6057 $a->link = $CFG->wwwroot .'/login/change_password.php';
6058 $a->signoff = generate_email_signoff();
6060 $message = get_string('newpasswordtext', '', $a);
6062 $subject = format_string($site->fullname) .': '. get_string('changedpassword');
6064 unset_user_preference('create_password', $user); // Prevent cron from generating the password.
6066 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6067 return email_to_user($user, $supportuser, $subject, $message);
6071 * Send email to specified user with confirmation text and activation link.
6073 * @param stdClass $user A {@link $USER} object
6074 * @param string $confirmationurl user confirmation URL
6075 * @return bool Returns true if mail was sent OK and false if there was an error.
6077 function send_confirmation_email($user, $confirmationurl = null) {
6078 global $CFG;
6080 $site = get_site();
6081 $supportuser = core_user::get_support_user();
6083 $data = new stdClass();
6084 $data->sitename = format_string($site->fullname);
6085 $data->admin = generate_email_signoff();
6087 $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname));
6089 if (empty($confirmationurl)) {
6090 $confirmationurl = '/login/confirm.php';
6093 $confirmationurl = new moodle_url($confirmationurl);
6094 // Remove data parameter just in case it was included in the confirmation so we can add it manually later.
6095 $confirmationurl->remove_params('data');
6096 $confirmationpath = $confirmationurl->out(false);
6098 // We need to custom encode the username to include trailing dots in the link.
6099 // Because of this custom encoding we can't use moodle_url directly.
6100 // Determine if a query string is present in the confirmation url.
6101 $hasquerystring = strpos($confirmationpath, '?') !== false;
6102 // Perform normal url encoding of the username first.
6103 $username = urlencode($user->username);
6104 // Prevent problems with trailing dots not being included as part of link in some mail clients.
6105 $username = str_replace('.', '%2E', $username);
6107 $data->link = $confirmationpath . ( $hasquerystring ? '&' : '?') . 'data='. $user->secret .'/'. $username;
6109 $message = get_string('emailconfirmation', '', $data);
6110 $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true);
6112 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6113 return email_to_user($user, $supportuser, $subject, $message, $messagehtml);
6117 * Sends a password change confirmation email.
6119 * @param stdClass $user A {@link $USER} object
6120 * @param stdClass $resetrecord An object tracking metadata regarding password reset request
6121 * @return bool Returns true if mail was sent OK and false if there was an error.
6123 function send_password_change_confirmation_email($user, $resetrecord) {
6124 global $CFG;
6126 $site = get_site();
6127 $supportuser = core_user::get_support_user();
6128 $pwresetmins = isset($CFG->pwresettime) ? floor($CFG->pwresettime / MINSECS) : 30;
6130 $data = new stdClass();
6131 $data->firstname = $user->firstname;
6132 $data->lastname = $user->lastname;
6133 $data->username = $user->username;
6134 $data->sitename = format_string($site->fullname);
6135 $data->link = $CFG->wwwroot .'/login/forgot_password.php?token='. $resetrecord->token;
6136 $data->admin = generate_email_signoff();
6137 $data->resetminutes = $pwresetmins;
6139 $message = get_string('emailresetconfirmation', '', $data);
6140 $subject = get_string('emailresetconfirmationsubject', '', format_string($site->fullname));
6142 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6143 return email_to_user($user, $supportuser, $subject, $message);
6148 * Sends an email containing information on how to change your password.
6150 * @param stdClass $user A {@link $USER} object
6151 * @return bool Returns true if mail was sent OK and false if there was an error.
6153 function send_password_change_info($user) {
6154 $site = get_site();
6155 $supportuser = core_user::get_support_user();
6157 $data = new stdClass();
6158 $data->firstname = $user->firstname;
6159 $data->lastname = $user->lastname;
6160 $data->username = $user->username;
6161 $data->sitename = format_string($site->fullname);
6162 $data->admin = generate_email_signoff();
6164 if (!is_enabled_auth($user->auth)) {
6165 $message = get_string('emailpasswordchangeinfodisabled', '', $data);
6166 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
6167 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6168 return email_to_user($user, $supportuser, $subject, $message);
6171 $userauth = get_auth_plugin($user->auth);
6172 ['subject' => $subject, 'message' => $message] = $userauth->get_password_change_info($user);
6174 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6175 return email_to_user($user, $supportuser, $subject, $message);
6179 * Check that an email is allowed. It returns an error message if there was a problem.
6181 * @param string $email Content of email
6182 * @return string|false
6184 function email_is_not_allowed($email) {
6185 global $CFG;
6187 // Comparing lowercase domains.
6188 $email = strtolower($email);
6189 if (!empty($CFG->allowemailaddresses)) {
6190 $allowed = explode(' ', strtolower($CFG->allowemailaddresses));
6191 foreach ($allowed as $allowedpattern) {
6192 $allowedpattern = trim($allowedpattern);
6193 if (!$allowedpattern) {
6194 continue;
6196 if (strpos($allowedpattern, '.') === 0) {
6197 if (strpos(strrev($email), strrev($allowedpattern)) === 0) {
6198 // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6199 return false;
6202 } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) {
6203 return false;
6206 return get_string('emailonlyallowed', '', $CFG->allowemailaddresses);
6208 } else if (!empty($CFG->denyemailaddresses)) {
6209 $denied = explode(' ', strtolower($CFG->denyemailaddresses));
6210 foreach ($denied as $deniedpattern) {
6211 $deniedpattern = trim($deniedpattern);
6212 if (!$deniedpattern) {
6213 continue;
6215 if (strpos($deniedpattern, '.') === 0) {
6216 if (strpos(strrev($email), strrev($deniedpattern)) === 0) {
6217 // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6218 return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6221 } else if (strpos(strrev($email), strrev('@'.$deniedpattern)) === 0) {
6222 return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6227 return false;
6230 // FILE HANDLING.
6233 * Returns local file storage instance
6235 * @return file_storage
6237 function get_file_storage($reset = false) {
6238 global $CFG;
6240 static $fs = null;
6242 if ($reset) {
6243 $fs = null;
6244 return;
6247 if ($fs) {
6248 return $fs;
6251 require_once("$CFG->libdir/filelib.php");
6253 $fs = new file_storage();
6255 return $fs;
6259 * Returns local file storage instance
6261 * @return file_browser
6263 function get_file_browser() {
6264 global $CFG;
6266 static $fb = null;
6268 if ($fb) {
6269 return $fb;
6272 require_once("$CFG->libdir/filelib.php");
6274 $fb = new file_browser();
6276 return $fb;
6280 * Returns file packer
6282 * @param string $mimetype default application/zip
6283 * @return file_packer
6285 function get_file_packer($mimetype='application/zip') {
6286 global $CFG;
6288 static $fp = array();
6290 if (isset($fp[$mimetype])) {
6291 return $fp[$mimetype];
6294 switch ($mimetype) {
6295 case 'application/zip':
6296 case 'application/vnd.moodle.profiling':
6297 $classname = 'zip_packer';
6298 break;
6300 case 'application/x-gzip' :
6301 $classname = 'tgz_packer';
6302 break;
6304 case 'application/vnd.moodle.backup':
6305 $classname = 'mbz_packer';
6306 break;
6308 default:
6309 return false;
6312 require_once("$CFG->libdir/filestorage/$classname.php");
6313 $fp[$mimetype] = new $classname();
6315 return $fp[$mimetype];
6319 * Returns current name of file on disk if it exists.
6321 * @param string $newfile File to be verified
6322 * @return string Current name of file on disk if true
6324 function valid_uploaded_file($newfile) {
6325 if (empty($newfile)) {
6326 return '';
6328 if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) {
6329 return $newfile['tmp_name'];
6330 } else {
6331 return '';
6336 * Returns the maximum size for uploading files.
6338 * There are seven possible upload limits:
6339 * 1. in Apache using LimitRequestBody (no way of checking or changing this)
6340 * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
6341 * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
6342 * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
6343 * 5. by the Moodle admin in $CFG->maxbytes
6344 * 6. by the teacher in the current course $course->maxbytes
6345 * 7. by the teacher for the current module, eg $assignment->maxbytes
6347 * These last two are passed to this function as arguments (in bytes).
6348 * Anything defined as 0 is ignored.
6349 * The smallest of all the non-zero numbers is returned.
6351 * @todo Finish documenting this function
6353 * @param int $sitebytes Set maximum size
6354 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6355 * @param int $modulebytes Current module ->maxbytes (in bytes)
6356 * @param bool $unused This parameter has been deprecated and is not used any more.
6357 * @return int The maximum size for uploading files.
6359 function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0, $unused = false) {
6361 if (! $filesize = ini_get('upload_max_filesize')) {
6362 $filesize = '5M';
6364 $minimumsize = get_real_size($filesize);
6366 if ($postsize = ini_get('post_max_size')) {
6367 $postsize = get_real_size($postsize);
6368 if ($postsize < $minimumsize) {
6369 $minimumsize = $postsize;
6373 if (($sitebytes > 0) and ($sitebytes < $minimumsize)) {
6374 $minimumsize = $sitebytes;
6377 if (($coursebytes > 0) and ($coursebytes < $minimumsize)) {
6378 $minimumsize = $coursebytes;
6381 if (($modulebytes > 0) and ($modulebytes < $minimumsize)) {
6382 $minimumsize = $modulebytes;
6385 return $minimumsize;
6389 * Returns the maximum size for uploading files for the current user
6391 * This function takes in account {@link get_max_upload_file_size()} the user's capabilities
6393 * @param context $context The context in which to check user capabilities
6394 * @param int $sitebytes Set maximum size
6395 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6396 * @param int $modulebytes Current module ->maxbytes (in bytes)
6397 * @param stdClass $user The user
6398 * @param bool $unused This parameter has been deprecated and is not used any more.
6399 * @return int The maximum size for uploading files.
6401 function get_user_max_upload_file_size($context, $sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $user = null,
6402 $unused = false) {
6403 global $USER;
6405 if (empty($user)) {
6406 $user = $USER;
6409 if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) {
6410 return USER_CAN_IGNORE_FILE_SIZE_LIMITS;
6413 return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes);
6417 * Returns an array of possible sizes in local language
6419 * Related to {@link get_max_upload_file_size()} - this function returns an
6420 * array of possible sizes in an array, translated to the
6421 * local language.
6423 * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes.
6425 * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)"
6426 * with the value set to 0. This option will be the first in the list.
6428 * @uses SORT_NUMERIC
6429 * @param int $sitebytes Set maximum size
6430 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6431 * @param int $modulebytes Current module ->maxbytes (in bytes)
6432 * @param int|array $custombytes custom upload size/s which will be added to list,
6433 * Only value/s smaller then maxsize will be added to list.
6434 * @return array
6436 function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null) {
6437 global $CFG;
6439 if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) {
6440 return array();
6443 if ($sitebytes == 0) {
6444 // Will get the minimum of upload_max_filesize or post_max_size.
6445 $sitebytes = get_max_upload_file_size();
6448 $filesize = array();
6449 $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152,
6450 5242880, 10485760, 20971520, 52428800, 104857600,
6451 262144000, 524288000, 786432000, 1073741824,
6452 2147483648, 4294967296, 8589934592);
6454 // If custombytes is given and is valid then add it to the list.
6455 if (is_number($custombytes) and $custombytes > 0) {
6456 $custombytes = (int)$custombytes;
6457 if (!in_array($custombytes, $sizelist)) {
6458 $sizelist[] = $custombytes;
6460 } else if (is_array($custombytes)) {
6461 $sizelist = array_unique(array_merge($sizelist, $custombytes));
6464 // Allow maxbytes to be selected if it falls outside the above boundaries.
6465 if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) {
6466 // Note: get_real_size() is used in order to prevent problems with invalid values.
6467 $sizelist[] = get_real_size($CFG->maxbytes);
6470 foreach ($sizelist as $sizebytes) {
6471 if ($sizebytes < $maxsize && $sizebytes > 0) {
6472 $filesize[(string)intval($sizebytes)] = display_size($sizebytes, 0);
6476 $limitlevel = '';
6477 $displaysize = '';
6478 if ($modulebytes &&
6479 (($modulebytes < $coursebytes || $coursebytes == 0) &&
6480 ($modulebytes < $sitebytes || $sitebytes == 0))) {
6481 $limitlevel = get_string('activity', 'core');
6482 $displaysize = display_size($modulebytes, 0);
6483 $filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list.
6485 } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) {
6486 $limitlevel = get_string('course', 'core');
6487 $displaysize = display_size($coursebytes, 0);
6488 $filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list.
6490 } else if ($sitebytes) {
6491 $limitlevel = get_string('site', 'core');
6492 $displaysize = display_size($sitebytes, 0);
6493 $filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list.
6496 krsort($filesize, SORT_NUMERIC);
6497 if ($limitlevel) {
6498 $params = (object) array('contextname' => $limitlevel, 'displaysize' => $displaysize);
6499 $filesize = array('0' => get_string('uploadlimitwithsize', 'core', $params)) + $filesize;
6502 return $filesize;
6506 * Returns an array with all the filenames in all subdirectories, relative to the given rootdir.
6508 * If excludefiles is defined, then that file/directory is ignored
6509 * If getdirs is true, then (sub)directories are included in the output
6510 * If getfiles is true, then files are included in the output
6511 * (at least one of these must be true!)
6513 * @todo Finish documenting this function. Add examples of $excludefile usage.
6515 * @param string $rootdir A given root directory to start from
6516 * @param string|array $excludefiles If defined then the specified file/directory is ignored
6517 * @param bool $descend If true then subdirectories are recursed as well
6518 * @param bool $getdirs If true then (sub)directories are included in the output
6519 * @param bool $getfiles If true then files are included in the output
6520 * @return array An array with all the filenames in all subdirectories, relative to the given rootdir
6522 function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) {
6524 $dirs = array();
6526 if (!$getdirs and !$getfiles) { // Nothing to show.
6527 return $dirs;
6530 if (!is_dir($rootdir)) { // Must be a directory.
6531 return $dirs;
6534 if (!$dir = opendir($rootdir)) { // Can't open it for some reason.
6535 return $dirs;
6538 if (!is_array($excludefiles)) {
6539 $excludefiles = array($excludefiles);
6542 while (false !== ($file = readdir($dir))) {
6543 $firstchar = substr($file, 0, 1);
6544 if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) {
6545 continue;
6547 $fullfile = $rootdir .'/'. $file;
6548 if (filetype($fullfile) == 'dir') {
6549 if ($getdirs) {
6550 $dirs[] = $file;
6552 if ($descend) {
6553 $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles);
6554 foreach ($subdirs as $subdir) {
6555 $dirs[] = $file .'/'. $subdir;
6558 } else if ($getfiles) {
6559 $dirs[] = $file;
6562 closedir($dir);
6564 asort($dirs);
6566 return $dirs;
6571 * Adds up all the files in a directory and works out the size.
6573 * @param string $rootdir The directory to start from
6574 * @param string $excludefile A file to exclude when summing directory size
6575 * @return int The summed size of all files and subfiles within the root directory
6577 function get_directory_size($rootdir, $excludefile='') {
6578 global $CFG;
6580 // Do it this way if we can, it's much faster.
6581 if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) {
6582 $command = trim($CFG->pathtodu).' -sk '.escapeshellarg($rootdir);
6583 $output = null;
6584 $return = null;
6585 exec($command, $output, $return);
6586 if (is_array($output)) {
6587 // We told it to return k.
6588 return get_real_size(intval($output[0]).'k');
6592 if (!is_dir($rootdir)) {
6593 // Must be a directory.
6594 return 0;
6597 if (!$dir = @opendir($rootdir)) {
6598 // Can't open it for some reason.
6599 return 0;
6602 $size = 0;
6604 while (false !== ($file = readdir($dir))) {
6605 $firstchar = substr($file, 0, 1);
6606 if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) {
6607 continue;
6609 $fullfile = $rootdir .'/'. $file;
6610 if (filetype($fullfile) == 'dir') {
6611 $size += get_directory_size($fullfile, $excludefile);
6612 } else {
6613 $size += filesize($fullfile);
6616 closedir($dir);
6618 return $size;
6622 * Converts bytes into display form
6624 * @param int $size The size to convert to human readable form
6625 * @param int $decimalplaces If specified, uses fixed number of decimal places
6626 * @param string $fixedunits If specified, uses fixed units (e.g. 'KB')
6627 * @return string Display version of size
6629 function display_size($size, int $decimalplaces = 1, string $fixedunits = ''): string {
6631 static $units;
6633 if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) {
6634 return get_string('unlimited');
6637 if (empty($units)) {
6638 $units[] = get_string('sizeb');
6639 $units[] = get_string('sizekb');
6640 $units[] = get_string('sizemb');
6641 $units[] = get_string('sizegb');
6642 $units[] = get_string('sizetb');
6643 $units[] = get_string('sizepb');
6646 switch ($fixedunits) {
6647 case 'PB' :
6648 $magnitude = 5;
6649 break;
6650 case 'TB' :
6651 $magnitude = 4;
6652 break;
6653 case 'GB' :
6654 $magnitude = 3;
6655 break;
6656 case 'MB' :
6657 $magnitude = 2;
6658 break;
6659 case 'KB' :
6660 $magnitude = 1;
6661 break;
6662 case 'B' :
6663 $magnitude = 0;
6664 break;
6665 case '':
6666 $magnitude = floor(log($size, 1024));
6667 $magnitude = max(0, min(5, $magnitude));
6668 break;
6669 default:
6670 throw new coding_exception('Unknown fixed units value: ' . $fixedunits);
6673 // Special case for magnitude 0 (bytes) - never use decimal places.
6674 $nbsp = "\xc2\xa0";
6675 if ($magnitude === 0) {
6676 return round($size) . $nbsp . $units[$magnitude];
6679 // Convert to specified units.
6680 $sizeinunit = $size / 1024 ** $magnitude;
6682 // Fixed decimal places.
6683 return sprintf('%.' . $decimalplaces . 'f', $sizeinunit) . $nbsp . $units[$magnitude];
6687 * Cleans a given filename by removing suspicious or troublesome characters
6689 * @see clean_param()
6690 * @param string $string file name
6691 * @return string cleaned file name
6693 function clean_filename($string) {
6694 return clean_param($string, PARAM_FILE);
6697 // STRING TRANSLATION.
6700 * Returns the code for the current language
6702 * @category string
6703 * @return string
6705 function current_language() {
6706 global $CFG, $PAGE, $SESSION, $USER;
6708 if (!empty($SESSION->forcelang)) {
6709 // Allows overriding course-forced language (useful for admins to check
6710 // issues in courses whose language they don't understand).
6711 // Also used by some code to temporarily get language-related information in a
6712 // specific language (see force_current_language()).
6713 $return = $SESSION->forcelang;
6715 } else if (!empty($PAGE->cm->lang)) {
6716 // Activity language, if set.
6717 $return = $PAGE->cm->lang;
6719 } else if (!empty($PAGE->course->id) && $PAGE->course->id != SITEID && !empty($PAGE->course->lang)) {
6720 // Course language can override all other settings for this page.
6721 $return = $PAGE->course->lang;
6723 } else if (!empty($SESSION->lang)) {
6724 // Session language can override other settings.
6725 $return = $SESSION->lang;
6727 } else if (!empty($USER->lang)) {
6728 $return = $USER->lang;
6730 } else if (isset($CFG->lang)) {
6731 $return = $CFG->lang;
6733 } else {
6734 $return = 'en';
6737 // Just in case this slipped in from somewhere by accident.
6738 $return = str_replace('_utf8', '', $return);
6740 return $return;
6744 * Fix the current language to the given language code.
6746 * @param string $lang The language code to use.
6747 * @return void
6749 function fix_current_language(string $lang): void {
6750 global $CFG, $COURSE, $SESSION, $USER;
6752 if (!get_string_manager()->translation_exists($lang)) {
6753 throw new coding_exception("The language pack for $lang is not available");
6756 $fixglobal = '';
6757 $fixlang = 'lang';
6758 if (!empty($SESSION->forcelang)) {
6759 $fixglobal = $SESSION;
6760 $fixlang = 'forcelang';
6761 } else if (!empty($COURSE->id) && $COURSE->id != SITEID && !empty($COURSE->lang)) {
6762 $fixglobal = $COURSE;
6763 } else if (!empty($SESSION->lang)) {
6764 $fixglobal = $SESSION;
6765 } else if (!empty($USER->lang)) {
6766 $fixglobal = $USER;
6767 } else if (isset($CFG->lang)) {
6768 set_config('lang', $lang);
6771 if ($fixglobal) {
6772 $fixglobal->$fixlang = $lang;
6777 * Returns parent language of current active language if defined
6779 * @category string
6780 * @param string $lang null means current language
6781 * @return string
6783 function get_parent_language($lang=null) {
6785 $parentlang = get_string_manager()->get_string('parentlanguage', 'langconfig', null, $lang);
6787 if ($parentlang === 'en') {
6788 $parentlang = '';
6791 return $parentlang;
6795 * Force the current language to get strings and dates localised in the given language.
6797 * After calling this function, all strings will be provided in the given language
6798 * until this function is called again, or equivalent code is run.
6800 * @param string $language
6801 * @return string previous $SESSION->forcelang value
6803 function force_current_language($language) {
6804 global $SESSION;
6805 $sessionforcelang = isset($SESSION->forcelang) ? $SESSION->forcelang : '';
6806 if ($language !== $sessionforcelang) {
6807 // Setting forcelang to null or an empty string disables its effect.
6808 if (empty($language) || get_string_manager()->translation_exists($language, false)) {
6809 $SESSION->forcelang = $language;
6810 moodle_setlocale();
6813 return $sessionforcelang;
6817 * Returns current string_manager instance.
6819 * The param $forcereload is needed for CLI installer only where the string_manager instance
6820 * must be replaced during the install.php script life time.
6822 * @category string
6823 * @param bool $forcereload shall the singleton be released and new instance created instead?
6824 * @return core_string_manager
6826 function get_string_manager($forcereload=false) {
6827 global $CFG;
6829 static $singleton = null;
6831 if ($forcereload) {
6832 $singleton = null;
6834 if ($singleton === null) {
6835 if (empty($CFG->early_install_lang)) {
6837 $transaliases = array();
6838 if (empty($CFG->langlist)) {
6839 $translist = array();
6840 } else {
6841 $translist = explode(',', $CFG->langlist);
6842 $translist = array_map('trim', $translist);
6843 // Each language in the $CFG->langlist can has an "alias" that would substitute the default language name.
6844 foreach ($translist as $i => $value) {
6845 $parts = preg_split('/\s*\|\s*/', $value, 2);
6846 if (count($parts) == 2) {
6847 $transaliases[$parts[0]] = $parts[1];
6848 $translist[$i] = $parts[0];
6853 if (!empty($CFG->config_php_settings['customstringmanager'])) {
6854 $classname = $CFG->config_php_settings['customstringmanager'];
6856 if (class_exists($classname)) {
6857 $implements = class_implements($classname);
6859 if (isset($implements['core_string_manager'])) {
6860 $singleton = new $classname($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
6861 return $singleton;
6863 } else {
6864 debugging('Unable to instantiate custom string manager: class '.$classname.
6865 ' does not implement the core_string_manager interface.');
6868 } else {
6869 debugging('Unable to instantiate custom string manager: class '.$classname.' can not be found.');
6873 $singleton = new core_string_manager_standard($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
6875 } else {
6876 $singleton = new core_string_manager_install();
6880 return $singleton;
6884 * Returns a localized string.
6886 * Returns the translated string specified by $identifier as
6887 * for $module. Uses the same format files as STphp.
6888 * $a is an object, string or number that can be used
6889 * within translation strings
6891 * eg 'hello {$a->firstname} {$a->lastname}'
6892 * or 'hello {$a}'
6894 * If you would like to directly echo the localized string use
6895 * the function {@link print_string()}
6897 * Example usage of this function involves finding the string you would
6898 * like a local equivalent of and using its identifier and module information
6899 * to retrieve it.<br/>
6900 * If you open moodle/lang/en/moodle.php and look near line 278
6901 * you will find a string to prompt a user for their word for 'course'
6902 * <code>
6903 * $string['course'] = 'Course';
6904 * </code>
6905 * So if you want to display the string 'Course'
6906 * in any language that supports it on your site
6907 * you just need to use the identifier 'course'
6908 * <code>
6909 * $mystring = '<strong>'. get_string('course') .'</strong>';
6910 * or
6911 * </code>
6912 * If the string you want is in another file you'd take a slightly
6913 * different approach. Looking in moodle/lang/en/calendar.php you find
6914 * around line 75:
6915 * <code>
6916 * $string['typecourse'] = 'Course event';
6917 * </code>
6918 * If you want to display the string "Course event" in any language
6919 * supported you would use the identifier 'typecourse' and the module 'calendar'
6920 * (because it is in the file calendar.php):
6921 * <code>
6922 * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>';
6923 * </code>
6925 * As a last resort, should the identifier fail to map to a string
6926 * the returned string will be [[ $identifier ]]
6928 * In Moodle 2.3 there is a new argument to this function $lazyload.
6929 * Setting $lazyload to true causes get_string to return a lang_string object
6930 * rather than the string itself. The fetching of the string is then put off until
6931 * the string object is first used. The object can be used by calling it's out
6932 * method or by casting the object to a string, either directly e.g.
6933 * (string)$stringobject
6934 * or indirectly by using the string within another string or echoing it out e.g.
6935 * echo $stringobject
6936 * return "<p>{$stringobject}</p>";
6937 * It is worth noting that using $lazyload and attempting to use the string as an
6938 * array key will cause a fatal error as objects cannot be used as array keys.
6939 * But you should never do that anyway!
6940 * For more information {@link lang_string}
6942 * @category string
6943 * @param string $identifier The key identifier for the localized string
6944 * @param string $component The module where the key identifier is stored,
6945 * usually expressed as the filename in the language pack without the
6946 * .php on the end but can also be written as mod/forum or grade/export/xls.
6947 * If none is specified then moodle.php is used.
6948 * @param string|object|array|int $a An object, string or number that can be used
6949 * within translation strings
6950 * @param bool $lazyload If set to true a string object is returned instead of
6951 * the string itself. The string then isn't calculated until it is first used.
6952 * @return string The localized string.
6953 * @throws coding_exception
6955 function get_string($identifier, $component = '', $a = null, $lazyload = false) {
6956 global $CFG;
6958 // If the lazy load argument has been supplied return a lang_string object
6959 // instead.
6960 // We need to make sure it is true (and a bool) as you will see below there
6961 // used to be a forth argument at one point.
6962 if ($lazyload === true) {
6963 return new lang_string($identifier, $component, $a);
6966 if ($CFG->debugdeveloper && clean_param($identifier, PARAM_STRINGID) === '') {
6967 throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.', DEBUG_DEVELOPER);
6970 // There is now a forth argument again, this time it is a boolean however so
6971 // we can still check for the old extralocations parameter.
6972 if (!is_bool($lazyload) && !empty($lazyload)) {
6973 debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.');
6976 if (strpos((string)$component, '/') !== false) {
6977 debugging('The module name you passed to get_string is the deprecated format ' .
6978 'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER);
6979 $componentpath = explode('/', $component);
6981 switch ($componentpath[0]) {
6982 case 'mod':
6983 $component = $componentpath[1];
6984 break;
6985 case 'blocks':
6986 case 'block':
6987 $component = 'block_'.$componentpath[1];
6988 break;
6989 case 'enrol':
6990 $component = 'enrol_'.$componentpath[1];
6991 break;
6992 case 'format':
6993 $component = 'format_'.$componentpath[1];
6994 break;
6995 case 'grade':
6996 $component = 'grade'.$componentpath[1].'_'.$componentpath[2];
6997 break;
7001 $result = get_string_manager()->get_string($identifier, $component, $a);
7003 // Debugging feature lets you display string identifier and component.
7004 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
7005 $result .= ' {' . $identifier . '/' . $component . '}';
7007 return $result;
7011 * Converts an array of strings to their localized value.
7013 * @param array $array An array of strings
7014 * @param string $component The language module that these strings can be found in.
7015 * @return stdClass translated strings.
7017 function get_strings($array, $component = '') {
7018 $string = new stdClass;
7019 foreach ($array as $item) {
7020 $string->$item = get_string($item, $component);
7022 return $string;
7026 * Prints out a translated string.
7028 * Prints out a translated string using the return value from the {@link get_string()} function.
7030 * Example usage of this function when the string is in the moodle.php file:<br/>
7031 * <code>
7032 * echo '<strong>';
7033 * print_string('course');
7034 * echo '</strong>';
7035 * </code>
7037 * Example usage of this function when the string is not in the moodle.php file:<br/>
7038 * <code>
7039 * echo '<h1>';
7040 * print_string('typecourse', 'calendar');
7041 * echo '</h1>';
7042 * </code>
7044 * @category string
7045 * @param string $identifier The key identifier for the localized string
7046 * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used.
7047 * @param string|object|array $a An object, string or number that can be used within translation strings
7049 function print_string($identifier, $component = '', $a = null) {
7050 echo get_string($identifier, $component, $a);
7054 * Returns a list of charset codes
7056 * Returns a list of charset codes. It's hardcoded, so they should be added manually
7057 * (checking that such charset is supported by the texlib library!)
7059 * @return array And associative array with contents in the form of charset => charset
7061 function get_list_of_charsets() {
7063 $charsets = array(
7064 'EUC-JP' => 'EUC-JP',
7065 'ISO-2022-JP'=> 'ISO-2022-JP',
7066 'ISO-8859-1' => 'ISO-8859-1',
7067 'SHIFT-JIS' => 'SHIFT-JIS',
7068 'GB2312' => 'GB2312',
7069 'GB18030' => 'GB18030', // GB18030 not supported by typo and mbstring.
7070 'UTF-8' => 'UTF-8');
7072 asort($charsets);
7074 return $charsets;
7078 * Returns a list of valid and compatible themes
7080 * @return array
7082 function get_list_of_themes() {
7083 global $CFG;
7085 $themes = array();
7087 if (!empty($CFG->themelist)) { // Use admin's list of themes.
7088 $themelist = explode(',', $CFG->themelist);
7089 } else {
7090 $themelist = array_keys(core_component::get_plugin_list("theme"));
7093 foreach ($themelist as $key => $themename) {
7094 $theme = theme_config::load($themename);
7095 $themes[$themename] = $theme;
7098 core_collator::asort_objects_by_method($themes, 'get_theme_name');
7100 return $themes;
7104 * Factory function for emoticon_manager
7106 * @return emoticon_manager singleton
7108 function get_emoticon_manager() {
7109 static $singleton = null;
7111 if (is_null($singleton)) {
7112 $singleton = new emoticon_manager();
7115 return $singleton;
7119 * Provides core support for plugins that have to deal with emoticons (like HTML editor or emoticon filter).
7121 * Whenever this manager mentiones 'emoticon object', the following data
7122 * structure is expected: stdClass with properties text, imagename, imagecomponent,
7123 * altidentifier and altcomponent
7125 * @see admin_setting_emoticons
7127 * @copyright 2010 David Mudrak
7128 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
7130 class emoticon_manager {
7133 * Returns the currently enabled emoticons
7135 * @param boolean $selectable - If true, only return emoticons that should be selectable from a list.
7136 * @return array of emoticon objects
7138 public function get_emoticons($selectable = false) {
7139 global $CFG;
7140 $notselectable = ['martin', 'egg'];
7142 if (empty($CFG->emoticons)) {
7143 return array();
7146 $emoticons = $this->decode_stored_config($CFG->emoticons);
7148 if (!is_array($emoticons)) {
7149 // Something is wrong with the format of stored setting.
7150 debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL);
7151 return array();
7153 if ($selectable) {
7154 foreach ($emoticons as $index => $emote) {
7155 if (in_array($emote->altidentifier, $notselectable)) {
7156 // Skip this one.
7157 unset($emoticons[$index]);
7162 return $emoticons;
7166 * Converts emoticon object into renderable pix_emoticon object
7168 * @param stdClass $emoticon emoticon object
7169 * @param array $attributes explicit HTML attributes to set
7170 * @return pix_emoticon
7172 public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array()) {
7173 $stringmanager = get_string_manager();
7174 if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) {
7175 $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent);
7176 } else {
7177 $alt = s($emoticon->text);
7179 return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes);
7183 * Encodes the array of emoticon objects into a string storable in config table
7185 * @see self::decode_stored_config()
7186 * @param array $emoticons array of emtocion objects
7187 * @return string
7189 public function encode_stored_config(array $emoticons) {
7190 return json_encode($emoticons);
7194 * Decodes the string into an array of emoticon objects
7196 * @see self::encode_stored_config()
7197 * @param string $encoded
7198 * @return array|null
7200 public function decode_stored_config($encoded) {
7201 $decoded = json_decode($encoded);
7202 if (!is_array($decoded)) {
7203 return null;
7205 return $decoded;
7209 * Returns default set of emoticons supported by Moodle
7211 * @return array of sdtClasses
7213 public function default_emoticons() {
7214 return array(
7215 $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'),
7216 $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'),
7217 $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'),
7218 $this->prepare_emoticon_object(";-)", 's/wink', 'wink'),
7219 $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'),
7220 $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'),
7221 $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'),
7222 $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'),
7223 $this->prepare_emoticon_object("B-)", 's/cool', 'cool'),
7224 $this->prepare_emoticon_object("^-)", 's/approve', 'approve'),
7225 $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'),
7226 $this->prepare_emoticon_object(":o)", 's/clown', 'clown'),
7227 $this->prepare_emoticon_object(":-(", 's/sad', 'sad'),
7228 $this->prepare_emoticon_object(":(", 's/sad', 'sad'),
7229 $this->prepare_emoticon_object("8-.", 's/shy', 'shy'),
7230 $this->prepare_emoticon_object(":-I", 's/blush', 'blush'),
7231 $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'),
7232 $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'),
7233 $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'),
7234 $this->prepare_emoticon_object("8-[", 's/angry', 'angry'),
7235 $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'),
7236 $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'),
7237 $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'),
7238 $this->prepare_emoticon_object("}-]", 's/evil', 'evil'),
7239 $this->prepare_emoticon_object("(h)", 's/heart', 'heart'),
7240 $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'),
7241 $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'),
7242 $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'),
7243 $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'),
7244 $this->prepare_emoticon_object("( )", 's/egg', 'egg'),
7249 * Helper method preparing the stdClass with the emoticon properties
7251 * @param string|array $text or array of strings
7252 * @param string $imagename to be used by {@link pix_emoticon}
7253 * @param string $altidentifier alternative string identifier, null for no alt
7254 * @param string $altcomponent where the alternative string is defined
7255 * @param string $imagecomponent to be used by {@link pix_emoticon}
7256 * @return stdClass
7258 protected function prepare_emoticon_object($text, $imagename, $altidentifier = null,
7259 $altcomponent = 'core_pix', $imagecomponent = 'core') {
7260 return (object)array(
7261 'text' => $text,
7262 'imagename' => $imagename,
7263 'imagecomponent' => $imagecomponent,
7264 'altidentifier' => $altidentifier,
7265 'altcomponent' => $altcomponent,
7270 // ENCRYPTION.
7273 * rc4encrypt
7275 * @param string $data Data to encrypt.
7276 * @return string The now encrypted data.
7278 function rc4encrypt($data) {
7279 return endecrypt(get_site_identifier(), $data, '');
7283 * rc4decrypt
7285 * @param string $data Data to decrypt.
7286 * @return string The now decrypted data.
7288 function rc4decrypt($data) {
7289 return endecrypt(get_site_identifier(), $data, 'de');
7293 * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com]
7295 * @todo Finish documenting this function
7297 * @param string $pwd The password to use when encrypting or decrypting
7298 * @param string $data The data to be decrypted/encrypted
7299 * @param string $case Either 'de' for decrypt or '' for encrypt
7300 * @return string
7302 function endecrypt ($pwd, $data, $case) {
7304 if ($case == 'de') {
7305 $data = urldecode($data);
7308 $key[] = '';
7309 $box[] = '';
7310 $pwdlength = strlen($pwd);
7312 for ($i = 0; $i <= 255; $i++) {
7313 $key[$i] = ord(substr($pwd, ($i % $pwdlength), 1));
7314 $box[$i] = $i;
7317 $x = 0;
7319 for ($i = 0; $i <= 255; $i++) {
7320 $x = ($x + $box[$i] + $key[$i]) % 256;
7321 $tempswap = $box[$i];
7322 $box[$i] = $box[$x];
7323 $box[$x] = $tempswap;
7326 $cipher = '';
7328 $a = 0;
7329 $j = 0;
7331 for ($i = 0; $i < strlen($data); $i++) {
7332 $a = ($a + 1) % 256;
7333 $j = ($j + $box[$a]) % 256;
7334 $temp = $box[$a];
7335 $box[$a] = $box[$j];
7336 $box[$j] = $temp;
7337 $k = $box[(($box[$a] + $box[$j]) % 256)];
7338 $cipherby = ord(substr($data, $i, 1)) ^ $k;
7339 $cipher .= chr($cipherby);
7342 if ($case == 'de') {
7343 $cipher = urldecode(urlencode($cipher));
7344 } else {
7345 $cipher = urlencode($cipher);
7348 return $cipher;
7351 // ENVIRONMENT CHECKING.
7354 * This method validates a plug name. It is much faster than calling clean_param.
7356 * @param string $name a string that might be a plugin name.
7357 * @return bool if this string is a valid plugin name.
7359 function is_valid_plugin_name($name) {
7360 // This does not work for 'mod', bad luck, use any other type.
7361 return core_component::is_valid_plugin_name('tool', $name);
7365 * Get a list of all the plugins of a given type that define a certain API function
7366 * in a certain file. The plugin component names and function names are returned.
7368 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
7369 * @param string $function the part of the name of the function after the
7370 * frankenstyle prefix. e.g 'hook' if you are looking for functions with
7371 * names like report_courselist_hook.
7372 * @param string $file the name of file within the plugin that defines the
7373 * function. Defaults to lib.php.
7374 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
7375 * and the function names as values (e.g. 'report_courselist_hook', 'forum_hook').
7377 function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php') {
7378 global $CFG;
7380 // We don't include here as all plugin types files would be included.
7381 $plugins = get_plugins_with_function($function, $file, false);
7383 if (empty($plugins[$plugintype])) {
7384 return array();
7387 $allplugins = core_component::get_plugin_list($plugintype);
7389 // Reformat the array and include the files.
7390 $pluginfunctions = array();
7391 foreach ($plugins[$plugintype] as $pluginname => $functionname) {
7393 // Check that it has not been removed and the file is still available.
7394 if (!empty($allplugins[$pluginname])) {
7396 $filepath = $allplugins[$pluginname] . DIRECTORY_SEPARATOR . $file;
7397 if (file_exists($filepath)) {
7398 include_once($filepath);
7400 // Now that the file is loaded, we must verify the function still exists.
7401 if (function_exists($functionname)) {
7402 $pluginfunctions[$plugintype . '_' . $pluginname] = $functionname;
7403 } else {
7404 // Invalidate the cache for next run.
7405 \cache_helper::invalidate_by_definition('core', 'plugin_functions');
7411 return $pluginfunctions;
7415 * Get a list of all the plugins that define a certain API function in a certain file.
7417 * @param string $function the part of the name of the function after the
7418 * frankenstyle prefix. e.g 'hook' if you are looking for functions with
7419 * names like report_courselist_hook.
7420 * @param string $file the name of file within the plugin that defines the
7421 * function. Defaults to lib.php.
7422 * @param bool $include Whether to include the files that contain the functions or not.
7423 * @param bool $migratedtohook if true this is a deprecated lib.php callback, if hook callback is present then do nothing
7424 * @return array with [plugintype][plugin] = functionname
7426 function get_plugins_with_function($function, $file = 'lib.php', $include = true, bool $migratedtohook = false) {
7427 global $CFG;
7429 if (during_initial_install() || isset($CFG->upgraderunning)) {
7430 // API functions _must not_ be called during an installation or upgrade.
7431 return [];
7434 $plugincallback = $function;
7435 $filtermigrated = function($plugincallback, $pluginfunctions): array {
7436 foreach ($pluginfunctions as $plugintype => $plugins) {
7437 foreach ($plugins as $plugin => $unusedfunction) {
7438 $component = $plugintype . '_' . $plugin;
7439 if ($hooks = \core\hook\manager::get_instance()->get_hooks_deprecating_plugin_callback($plugincallback)) {
7440 if (\core\hook\manager::get_instance()->is_deprecating_hook_present($component, $plugincallback)) {
7441 // Ignore the old callback, it is there only for older Moodle versions.
7442 unset($pluginfunctions[$plugintype][$plugin]);
7443 } else {
7444 $hookmessage = count($hooks) == 1 ? reset($hooks) : 'one of ' . implode(', ', $hooks);
7445 debugging(
7446 "Callback $plugincallback in $component component should be migrated to new " .
7447 "hook callback for $hookmessage",
7448 DEBUG_DEVELOPER
7454 return $pluginfunctions;
7457 $cache = \cache::make('core', 'plugin_functions');
7459 // Including both although I doubt that we will find two functions definitions with the same name.
7460 // Clean the filename as cache_helper::hash_key only allows a-zA-Z0-9_.
7461 $pluginfunctions = false;
7462 if (!empty($CFG->allversionshash)) {
7463 $key = $CFG->allversionshash . '_' . $function . '_' . clean_param($file, PARAM_ALPHA);
7464 $pluginfunctions = $cache->get($key);
7466 $dirty = false;
7468 // Use the plugin manager to check that plugins are currently installed.
7469 $pluginmanager = \core_plugin_manager::instance();
7471 if ($pluginfunctions !== false) {
7473 // Checking that the files are still available.
7474 foreach ($pluginfunctions as $plugintype => $plugins) {
7476 $allplugins = \core_component::get_plugin_list($plugintype);
7477 $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7478 foreach ($plugins as $plugin => $function) {
7479 if (!isset($installedplugins[$plugin])) {
7480 // Plugin code is still present on disk but it is not installed.
7481 $dirty = true;
7482 break 2;
7485 // Cache might be out of sync with the codebase, skip the plugin if it is not available.
7486 if (empty($allplugins[$plugin])) {
7487 $dirty = true;
7488 break 2;
7491 $fileexists = file_exists($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7492 if ($include && $fileexists) {
7493 // Include the files if it was requested.
7494 include_once($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7495 } else if (!$fileexists) {
7496 // If the file is not available any more it should not be returned.
7497 $dirty = true;
7498 break 2;
7501 // Check if the function still exists in the file.
7502 if ($include && !function_exists($function)) {
7503 $dirty = true;
7504 break 2;
7509 // If the cache is dirty, we should fall through and let it rebuild.
7510 if (!$dirty) {
7511 if ($migratedtohook && $file === 'lib.php') {
7512 $pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions);
7514 return $pluginfunctions;
7518 $pluginfunctions = array();
7520 // To fill the cached. Also, everything should continue working with cache disabled.
7521 $plugintypes = \core_component::get_plugin_types();
7522 foreach ($plugintypes as $plugintype => $unused) {
7524 // We need to include files here.
7525 $pluginswithfile = \core_component::get_plugin_list_with_file($plugintype, $file, true);
7526 $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7527 foreach ($pluginswithfile as $plugin => $notused) {
7529 if (!isset($installedplugins[$plugin])) {
7530 continue;
7533 $fullfunction = $plugintype . '_' . $plugin . '_' . $function;
7535 $pluginfunction = false;
7536 if (function_exists($fullfunction)) {
7537 // Function exists with standard name. Store, indexed by frankenstyle name of plugin.
7538 $pluginfunction = $fullfunction;
7540 } else if ($plugintype === 'mod') {
7541 // For modules, we also allow plugin without full frankenstyle but just starting with the module name.
7542 $shortfunction = $plugin . '_' . $function;
7543 if (function_exists($shortfunction)) {
7544 $pluginfunction = $shortfunction;
7548 if ($pluginfunction) {
7549 if (empty($pluginfunctions[$plugintype])) {
7550 $pluginfunctions[$plugintype] = array();
7552 $pluginfunctions[$plugintype][$plugin] = $pluginfunction;
7557 if (!empty($CFG->allversionshash)) {
7558 $cache->set($key, $pluginfunctions);
7561 if ($migratedtohook && $file === 'lib.php') {
7562 $pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions);
7565 return $pluginfunctions;
7570 * Lists plugin-like directories within specified directory
7572 * This function was originally used for standard Moodle plugins, please use
7573 * new core_component::get_plugin_list() now.
7575 * This function is used for general directory listing and backwards compatility.
7577 * @param string $directory relative directory from root
7578 * @param string $exclude dir name to exclude from the list (defaults to none)
7579 * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot)
7580 * @return array Sorted array of directory names found under the requested parameters
7582 function get_list_of_plugins($directory='mod', $exclude='', $basedir='') {
7583 global $CFG;
7585 $plugins = array();
7587 if (empty($basedir)) {
7588 $basedir = $CFG->dirroot .'/'. $directory;
7590 } else {
7591 $basedir = $basedir .'/'. $directory;
7594 if ($CFG->debugdeveloper and empty($exclude)) {
7595 // Make sure devs do not use this to list normal plugins,
7596 // this is intended for general directories that are not plugins!
7598 $subtypes = core_component::get_plugin_types();
7599 if (in_array($basedir, $subtypes)) {
7600 debugging('get_list_of_plugins() should not be used to list real plugins, use core_component::get_plugin_list() instead!', DEBUG_DEVELOPER);
7602 unset($subtypes);
7605 $ignorelist = array_flip(array_filter([
7606 'CVS',
7607 '_vti_cnf',
7608 'amd',
7609 'classes',
7610 'simpletest',
7611 'tests',
7612 'templates',
7613 'yui',
7614 $exclude,
7615 ]));
7617 if (file_exists($basedir) && filetype($basedir) == 'dir') {
7618 if (!$dirhandle = opendir($basedir)) {
7619 debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER);
7620 return array();
7622 while (false !== ($dir = readdir($dirhandle))) {
7623 if (strpos($dir, '.') === 0) {
7624 // Ignore directories starting with .
7625 // These are treated as hidden directories.
7626 continue;
7628 if (array_key_exists($dir, $ignorelist)) {
7629 // This directory features on the ignore list.
7630 continue;
7632 if (filetype($basedir .'/'. $dir) != 'dir') {
7633 continue;
7635 $plugins[] = $dir;
7637 closedir($dirhandle);
7639 if ($plugins) {
7640 asort($plugins);
7642 return $plugins;
7646 * Invoke plugin's callback functions
7648 * @param string $type plugin type e.g. 'mod'
7649 * @param string $name plugin name
7650 * @param string $feature feature name
7651 * @param string $action feature's action
7652 * @param array $params parameters of callback function, should be an array
7653 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
7654 * @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing
7655 * @return mixed
7657 * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743
7659 function plugin_callback($type, $name, $feature, $action, $params = null, $default = null, bool $migratedtohook = false) {
7660 return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default, $migratedtohook);
7664 * Invoke component's callback functions
7666 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
7667 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
7668 * @param array $params parameters of callback function
7669 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
7670 * @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing
7671 * @return mixed
7673 function component_callback($component, $function, array $params = array(), $default = null, bool $migratedtohook = false) {
7674 $functionname = component_callback_exists($component, $function);
7676 if ($functionname) {
7677 if ($migratedtohook) {
7678 if ($hooks = \core\hook\manager::get_instance()->get_hooks_deprecating_plugin_callback($function)) {
7679 if (\core\hook\manager::get_instance()->is_deprecating_hook_present($component, $function)) {
7680 // Do not call the old lib.php callback,
7681 // it is there for compatibility with older Moodle versions only.
7682 return null;
7683 } else {
7684 $hookmessage = count($hooks) == 1 ? reset($hooks) : 'one of ' . implode(', ', $hooks);
7685 debugging(
7686 "Callback $function in $component component should be migrated to new hook callback for $hookmessage",
7687 DEBUG_DEVELOPER);
7692 // Function exists, so just return function result.
7693 $ret = call_user_func_array($functionname, $params);
7694 if (is_null($ret)) {
7695 return $default;
7696 } else {
7697 return $ret;
7700 return $default;
7704 * Determine if a component callback exists and return the function name to call. Note that this
7705 * function will include the required library files so that the functioname returned can be
7706 * called directly.
7708 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
7709 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
7710 * @return mixed Complete function name to call if the callback exists or false if it doesn't.
7711 * @throws coding_exception if invalid component specfied
7713 function component_callback_exists($component, $function) {
7714 global $CFG; // This is needed for the inclusions.
7716 $cleancomponent = clean_param($component, PARAM_COMPONENT);
7717 if (empty($cleancomponent)) {
7718 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
7720 $component = $cleancomponent;
7722 list($type, $name) = core_component::normalize_component($component);
7723 $component = $type . '_' . $name;
7725 $oldfunction = $name.'_'.$function;
7726 $function = $component.'_'.$function;
7728 $dir = core_component::get_component_directory($component);
7729 if (empty($dir)) {
7730 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
7733 // Load library and look for function.
7734 if (file_exists($dir.'/lib.php')) {
7735 require_once($dir.'/lib.php');
7738 if (!function_exists($function) and function_exists($oldfunction)) {
7739 if ($type !== 'mod' and $type !== 'core') {
7740 debugging("Please use new function name $function instead of legacy $oldfunction", DEBUG_DEVELOPER);
7742 $function = $oldfunction;
7745 if (function_exists($function)) {
7746 return $function;
7748 return false;
7752 * Call the specified callback method on the provided class.
7754 * If the callback returns null, then the default value is returned instead.
7755 * If the class does not exist, then the default value is returned.
7757 * @param string $classname The name of the class to call upon.
7758 * @param string $methodname The name of the staticically defined method on the class.
7759 * @param array $params The arguments to pass into the method.
7760 * @param mixed $default The default value.
7761 * @param bool $migratedtohook True if the callback has been migrated to a hook.
7762 * @return mixed The return value.
7764 function component_class_callback($classname, $methodname, array $params, $default = null, bool $migratedtohook = false) {
7765 if (!class_exists($classname)) {
7766 return $default;
7769 if (!method_exists($classname, $methodname)) {
7770 return $default;
7773 $fullfunction = $classname . '::' . $methodname;
7775 if ($migratedtohook) {
7776 $functionparts = explode('\\', trim($fullfunction, '\\'));
7777 $component = $functionparts[0];
7778 $callback = end($functionparts);
7779 if ($hooks = \core\hook\manager::get_instance()->get_hooks_deprecating_plugin_callback($callback)) {
7780 if (\core\hook\manager::get_instance()->is_deprecating_hook_present($component, $callback)) {
7781 // Do not call the old class callback,
7782 // it is there for compatibility with older Moodle versions only.
7783 return null;
7784 } else {
7785 $hookmessage = count($hooks) == 1 ? reset($hooks) : 'one of ' . implode(', ', $hooks);
7786 debugging("Callback $callback in $component component should be migrated to new hook callback for $hookmessage",
7787 DEBUG_DEVELOPER);
7792 $result = call_user_func_array($fullfunction, $params);
7794 if (null === $result) {
7795 return $default;
7796 } else {
7797 return $result;
7802 * Checks whether a plugin supports a specified feature.
7804 * @param string $type Plugin type e.g. 'mod'
7805 * @param string $name Plugin name e.g. 'forum'
7806 * @param string $feature Feature code (FEATURE_xx constant)
7807 * @param mixed $default default value if feature support unknown
7808 * @return mixed Feature result (false if not supported, null if feature is unknown,
7809 * otherwise usually true but may have other feature-specific value such as array)
7810 * @throws coding_exception
7812 function plugin_supports($type, $name, $feature, $default = null) {
7813 global $CFG;
7815 if ($type === 'mod' and $name === 'NEWMODULE') {
7816 // Somebody forgot to rename the module template.
7817 return false;
7820 $component = clean_param($type . '_' . $name, PARAM_COMPONENT);
7821 if (empty($component)) {
7822 throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name);
7825 $function = null;
7827 if ($type === 'mod') {
7828 // We need this special case because we support subplugins in modules,
7829 // otherwise it would end up in infinite loop.
7830 if (file_exists("$CFG->dirroot/mod/$name/lib.php")) {
7831 include_once("$CFG->dirroot/mod/$name/lib.php");
7832 $function = $component.'_supports';
7833 if (!function_exists($function)) {
7834 // Legacy non-frankenstyle function name.
7835 $function = $name.'_supports';
7839 } else {
7840 if (!$path = core_component::get_plugin_directory($type, $name)) {
7841 // Non existent plugin type.
7842 return false;
7844 if (file_exists("$path/lib.php")) {
7845 include_once("$path/lib.php");
7846 $function = $component.'_supports';
7850 if ($function and function_exists($function)) {
7851 $supports = $function($feature);
7852 if (is_null($supports)) {
7853 // Plugin does not know - use default.
7854 return $default;
7855 } else {
7856 return $supports;
7860 // Plugin does not care, so use default.
7861 return $default;
7865 * Returns true if the current version of PHP is greater that the specified one.
7867 * @todo Check PHP version being required here is it too low?
7869 * @param string $version The version of php being tested.
7870 * @return bool
7872 function check_php_version($version='5.2.4') {
7873 return (version_compare(phpversion(), $version) >= 0);
7877 * Determine if moodle installation requires update.
7879 * Checks version numbers of main code and all plugins to see
7880 * if there are any mismatches.
7882 * @param bool $checkupgradeflag check the outagelessupgrade flag to see if an upgrade is running.
7883 * @return bool
7885 function moodle_needs_upgrading($checkupgradeflag = true) {
7886 global $CFG, $DB;
7888 // Say no if there is already an upgrade running.
7889 if ($checkupgradeflag) {
7890 $lock = $DB->get_field('config', 'value', ['name' => 'outagelessupgrade']);
7891 $currentprocessrunningupgrade = (defined('CLI_UPGRADE_RUNNING') && CLI_UPGRADE_RUNNING);
7892 // If we ARE locked, but this PHP process is NOT the process running the upgrade,
7893 // We should always return false.
7894 // This means the upgrade is running from CLI somewhere, or about to.
7895 if (!empty($lock) && !$currentprocessrunningupgrade) {
7896 return false;
7900 if (empty($CFG->version)) {
7901 return true;
7904 // There is no need to purge plugininfo caches here because
7905 // these caches are not used during upgrade and they are purged after
7906 // every upgrade.
7908 if (empty($CFG->allversionshash)) {
7909 return true;
7912 $hash = core_component::get_all_versions_hash();
7914 return ($hash !== $CFG->allversionshash);
7918 * Returns the major version of this site
7920 * Moodle version numbers consist of three numbers separated by a dot, for
7921 * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so
7922 * called major version. This function extracts the major version from either
7923 * $CFG->release (default) or eventually from the $release variable defined in
7924 * the main version.php.
7926 * @param bool $fromdisk should the version if source code files be used
7927 * @return string|false the major version like '2.3', false if could not be determined
7929 function moodle_major_version($fromdisk = false) {
7930 global $CFG;
7932 if ($fromdisk) {
7933 $release = null;
7934 require($CFG->dirroot.'/version.php');
7935 if (empty($release)) {
7936 return false;
7939 } else {
7940 if (empty($CFG->release)) {
7941 return false;
7943 $release = $CFG->release;
7946 if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) {
7947 return $matches[0];
7948 } else {
7949 return false;
7953 // MISCELLANEOUS.
7956 * Gets the system locale
7958 * @return string Retuns the current locale.
7960 function moodle_getlocale() {
7961 global $CFG;
7963 // Fetch the correct locale based on ostype.
7964 if ($CFG->ostype == 'WINDOWS') {
7965 $stringtofetch = 'localewin';
7966 } else {
7967 $stringtofetch = 'locale';
7970 if (!empty($CFG->locale)) { // Override locale for all language packs.
7971 return $CFG->locale;
7974 return get_string($stringtofetch, 'langconfig');
7978 * Sets the system locale
7980 * @category string
7981 * @param string $locale Can be used to force a locale
7983 function moodle_setlocale($locale='') {
7984 global $CFG;
7986 static $currentlocale = ''; // Last locale caching.
7988 $oldlocale = $currentlocale;
7990 // The priority is the same as in get_string() - parameter, config, course, session, user, global language.
7991 if (!empty($locale)) {
7992 $currentlocale = $locale;
7993 } else {
7994 $currentlocale = moodle_getlocale();
7997 // Do nothing if locale already set up.
7998 if ($oldlocale == $currentlocale) {
7999 return;
8002 // Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values,
8003 // set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7
8004 // Some day, numeric, monetary and other categories should be set too, I think. :-/.
8006 // Get current values.
8007 $monetary= setlocale (LC_MONETARY, 0);
8008 $numeric = setlocale (LC_NUMERIC, 0);
8009 $ctype = setlocale (LC_CTYPE, 0);
8010 if ($CFG->ostype != 'WINDOWS') {
8011 $messages= setlocale (LC_MESSAGES, 0);
8013 // Set locale to all.
8014 $result = setlocale (LC_ALL, $currentlocale);
8015 // If setting of locale fails try the other utf8 or utf-8 variant,
8016 // some operating systems support both (Debian), others just one (OSX).
8017 if ($result === false) {
8018 if (stripos($currentlocale, '.UTF-8') !== false) {
8019 $newlocale = str_ireplace('.UTF-8', '.UTF8', $currentlocale);
8020 setlocale (LC_ALL, $newlocale);
8021 } else if (stripos($currentlocale, '.UTF8') !== false) {
8022 $newlocale = str_ireplace('.UTF8', '.UTF-8', $currentlocale);
8023 setlocale (LC_ALL, $newlocale);
8026 // Set old values.
8027 setlocale (LC_MONETARY, $monetary);
8028 setlocale (LC_NUMERIC, $numeric);
8029 if ($CFG->ostype != 'WINDOWS') {
8030 setlocale (LC_MESSAGES, $messages);
8032 if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') {
8033 // To workaround a well-known PHP problem with Turkish letter Ii.
8034 setlocale (LC_CTYPE, $ctype);
8039 * Count words in a string.
8041 * Words are defined as things between whitespace.
8043 * @category string
8044 * @param string $string The text to be searched for words. May be HTML.
8045 * @param int|null $format
8046 * @return int The count of words in the specified string
8048 function count_words($string, $format = null) {
8049 // Before stripping tags, add a space after the close tag of anything that is not obviously inline.
8050 // Also, br is a special case because it definitely delimits a word, but has no close tag.
8051 $string = preg_replace('~
8052 ( # Capture the tag we match.
8053 </ # Start of close tag.
8054 (?! # Do not match any of these specific close tag names.
8055 a> | b> | del> | em> | i> |
8056 ins> | s> | small> | span> |
8057 strong> | sub> | sup> | u>
8059 \w+ # But, apart from those execptions, match any tag name.
8060 > # End of close tag.
8062 <br> | <br\s*/> # Special cases that are not close tags.
8064 ~x', '$1 ', $string); // Add a space after the close tag.
8065 if ($format !== null && $format != FORMAT_PLAIN) {
8066 // Match the usual text cleaning before display.
8067 // Ideally we should apply multilang filter only here, other filters might add extra text.
8068 $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]);
8070 // Now remove HTML tags.
8071 $string = strip_tags($string);
8072 // Decode HTML entities.
8073 $string = html_entity_decode($string, ENT_COMPAT);
8075 // Now, the word count is the number of blocks of characters separated
8076 // by any sort of space. That seems to be the definition used by all other systems.
8077 // To be precise about what is considered to separate words:
8078 // * Anything that Unicode considers a 'Separator'
8079 // * Anything that Unicode considers a 'Control character'
8080 // * An em- or en- dash.
8081 return count(preg_split('~[\p{Z}\p{Cc}—–]+~u', $string, -1, PREG_SPLIT_NO_EMPTY));
8085 * Count letters in a string.
8087 * Letters are defined as chars not in tags and different from whitespace.
8089 * @category string
8090 * @param string $string The text to be searched for letters. May be HTML.
8091 * @param int|null $format
8092 * @return int The count of letters in the specified text.
8094 function count_letters($string, $format = null) {
8095 if ($format !== null && $format != FORMAT_PLAIN) {
8096 // Match the usual text cleaning before display.
8097 // Ideally we should apply multilang filter only here, other filters might add extra text.
8098 $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]);
8100 $string = strip_tags($string); // Tags are out now.
8101 $string = html_entity_decode($string, ENT_COMPAT);
8102 $string = preg_replace('/[[:space:]]*/', '', $string); // Whitespace are out now.
8104 return core_text::strlen($string);
8108 * Generate and return a random string of the specified length.
8110 * @param int $length The length of the string to be created.
8111 * @return string
8113 function random_string($length=15) {
8114 $randombytes = random_bytes($length);
8115 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
8116 $pool .= 'abcdefghijklmnopqrstuvwxyz';
8117 $pool .= '0123456789';
8118 $poollen = strlen($pool);
8119 $string = '';
8120 for ($i = 0; $i < $length; $i++) {
8121 $rand = ord($randombytes[$i]);
8122 $string .= substr($pool, ($rand%($poollen)), 1);
8124 return $string;
8128 * Generate a complex random string (useful for md5 salts)
8130 * This function is based on the above {@link random_string()} however it uses a
8131 * larger pool of characters and generates a string between 24 and 32 characters
8133 * @param int $length Optional if set generates a string to exactly this length
8134 * @return string
8136 function complex_random_string($length=null) {
8137 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
8138 $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
8139 $poollen = strlen($pool);
8140 if ($length===null) {
8141 $length = floor(rand(24, 32));
8143 $randombytes = random_bytes($length);
8144 $string = '';
8145 for ($i = 0; $i < $length; $i++) {
8146 $rand = ord($randombytes[$i]);
8147 $string .= $pool[($rand%$poollen)];
8149 return $string;
8153 * Given some text (which may contain HTML) and an ideal length,
8154 * this function truncates the text neatly on a word boundary if possible
8156 * @category string
8157 * @param string $text text to be shortened
8158 * @param int $ideal ideal string length
8159 * @param boolean $exact if false, $text will not be cut mid-word
8160 * @param string $ending The string to append if the passed string is truncated
8161 * @return string $truncate shortened string
8163 function shorten_text($text, $ideal=30, $exact = false, $ending='...') {
8164 // If the plain text is shorter than the maximum length, return the whole text.
8165 if (core_text::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) {
8166 return $text;
8169 // Splits on HTML tags. Each open/close/empty tag will be the first thing
8170 // and only tag in its 'line'.
8171 preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
8173 $totallength = core_text::strlen($ending);
8174 $truncate = '';
8176 // This array stores information about open and close tags and their position
8177 // in the truncated string. Each item in the array is an object with fields
8178 // ->open (true if open), ->tag (tag name in lower case), and ->pos
8179 // (byte position in truncated text).
8180 $tagdetails = array();
8182 foreach ($lines as $linematchings) {
8183 // If there is any html-tag in this line, handle it and add it (uncounted) to the output.
8184 if (!empty($linematchings[1])) {
8185 // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>).
8186 if (!preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $linematchings[1])) {
8187 if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $linematchings[1], $tagmatchings)) {
8188 // Record closing tag.
8189 $tagdetails[] = (object) array(
8190 'open' => false,
8191 'tag' => core_text::strtolower($tagmatchings[1]),
8192 'pos' => core_text::strlen($truncate),
8195 } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $linematchings[1], $tagmatchings)) {
8196 // Record opening tag.
8197 $tagdetails[] = (object) array(
8198 'open' => true,
8199 'tag' => core_text::strtolower($tagmatchings[1]),
8200 'pos' => core_text::strlen($truncate),
8202 } else if (preg_match('/^<!--\[if\s.*?\]>$/s', $linematchings[1], $tagmatchings)) {
8203 $tagdetails[] = (object) array(
8204 'open' => true,
8205 'tag' => core_text::strtolower('if'),
8206 'pos' => core_text::strlen($truncate),
8208 } else if (preg_match('/^<!--<!\[endif\]-->$/s', $linematchings[1], $tagmatchings)) {
8209 $tagdetails[] = (object) array(
8210 'open' => false,
8211 'tag' => core_text::strtolower('if'),
8212 'pos' => core_text::strlen($truncate),
8216 // Add html-tag to $truncate'd text.
8217 $truncate .= $linematchings[1];
8220 // Calculate the length of the plain text part of the line; handle entities as one character.
8221 $contentlength = core_text::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $linematchings[2]));
8222 if ($totallength + $contentlength > $ideal) {
8223 // The number of characters which are left.
8224 $left = $ideal - $totallength;
8225 $entitieslength = 0;
8226 // Search for html entities.
8227 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)) {
8228 // Calculate the real length of all entities in the legal range.
8229 foreach ($entities[0] as $entity) {
8230 if ($entity[1]+1-$entitieslength <= $left) {
8231 $left--;
8232 $entitieslength += core_text::strlen($entity[0]);
8233 } else {
8234 // No more characters left.
8235 break;
8239 $breakpos = $left + $entitieslength;
8241 // If the words shouldn't be cut in the middle...
8242 if (!$exact) {
8243 // Search the last occurence of a space.
8244 for (; $breakpos > 0; $breakpos--) {
8245 if ($char = core_text::substr($linematchings[2], $breakpos, 1)) {
8246 if ($char === '.' or $char === ' ') {
8247 $breakpos += 1;
8248 break;
8249 } else if (strlen($char) > 2) {
8250 // Chinese/Japanese/Korean text can be truncated at any UTF-8 character boundary.
8251 $breakpos += 1;
8252 break;
8257 if ($breakpos == 0) {
8258 // This deals with the test_shorten_text_no_spaces case.
8259 $breakpos = $left + $entitieslength;
8260 } else if ($breakpos > $left + $entitieslength) {
8261 // This deals with the previous for loop breaking on the first char.
8262 $breakpos = $left + $entitieslength;
8265 $truncate .= core_text::substr($linematchings[2], 0, $breakpos);
8266 // Maximum length is reached, so get off the loop.
8267 break;
8268 } else {
8269 $truncate .= $linematchings[2];
8270 $totallength += $contentlength;
8273 // If the maximum length is reached, get off the loop.
8274 if ($totallength >= $ideal) {
8275 break;
8279 // Add the defined ending to the text.
8280 $truncate .= $ending;
8282 // Now calculate the list of open html tags based on the truncate position.
8283 $opentags = array();
8284 foreach ($tagdetails as $taginfo) {
8285 if ($taginfo->open) {
8286 // Add tag to the beginning of $opentags list.
8287 array_unshift($opentags, $taginfo->tag);
8288 } else {
8289 // Can have multiple exact same open tags, close the last one.
8290 $pos = array_search($taginfo->tag, array_reverse($opentags, true));
8291 if ($pos !== false) {
8292 unset($opentags[$pos]);
8297 // Close all unclosed html-tags.
8298 foreach ($opentags as $tag) {
8299 if ($tag === 'if') {
8300 $truncate .= '<!--<![endif]-->';
8301 } else {
8302 $truncate .= '</' . $tag . '>';
8306 return $truncate;
8310 * Shortens a given filename by removing characters positioned after the ideal string length.
8311 * When the filename is too long, the file cannot be created on the filesystem due to exceeding max byte size.
8312 * Limiting the filename to a certain size (considering multibyte characters) will prevent this.
8314 * @param string $filename file name
8315 * @param int $length ideal string length
8316 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8317 * @return string $shortened shortened file name
8319 function shorten_filename($filename, $length = MAX_FILENAME_SIZE, $includehash = false) {
8320 $shortened = $filename;
8321 // Extract a part of the filename if it's char size exceeds the ideal string length.
8322 if (core_text::strlen($filename) > $length) {
8323 // Exclude extension if present in filename.
8324 $mimetypes = get_mimetypes_array();
8325 $extension = pathinfo($filename, PATHINFO_EXTENSION);
8326 if ($extension && !empty($mimetypes[$extension])) {
8327 $basename = pathinfo($filename, PATHINFO_FILENAME);
8328 $hash = empty($includehash) ? '' : ' - ' . substr(sha1($basename), 0, 10);
8329 $shortened = core_text::substr($basename, 0, $length - strlen($hash)) . $hash;
8330 $shortened .= '.' . $extension;
8331 } else {
8332 $hash = empty($includehash) ? '' : ' - ' . substr(sha1($filename), 0, 10);
8333 $shortened = core_text::substr($filename, 0, $length - strlen($hash)) . $hash;
8336 return $shortened;
8340 * Shortens a given array of filenames by removing characters positioned after the ideal string length.
8342 * @param array $path The paths to reduce the length.
8343 * @param int $length Ideal string length
8344 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8345 * @return array $result Shortened paths in array.
8347 function shorten_filenames(array $path, $length = MAX_FILENAME_SIZE, $includehash = false) {
8348 $result = null;
8350 $result = array_reduce($path, function($carry, $singlepath) use ($length, $includehash) {
8351 $carry[] = shorten_filename($singlepath, $length, $includehash);
8352 return $carry;
8353 }, []);
8355 return $result;
8359 * Given dates in seconds, how many weeks is the date from startdate
8360 * The first week is 1, the second 2 etc ...
8362 * @param int $startdate Timestamp for the start date
8363 * @param int $thedate Timestamp for the end date
8364 * @return string
8366 function getweek ($startdate, $thedate) {
8367 if ($thedate < $startdate) {
8368 return 0;
8371 return floor(($thedate - $startdate) / WEEKSECS) + 1;
8375 * Returns a randomly generated password of length $maxlen. inspired by
8377 * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and
8378 * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254}
8380 * @param int $maxlen The maximum size of the password being generated.
8381 * @return string
8383 function generate_password($maxlen=10) {
8384 global $CFG;
8386 if (empty($CFG->passwordpolicy)) {
8387 $fillers = PASSWORD_DIGITS;
8388 $wordlist = file($CFG->wordlist);
8389 $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8390 $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8391 $filler1 = $fillers[rand(0, strlen($fillers) - 1)];
8392 $password = $word1 . $filler1 . $word2;
8393 } else {
8394 $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0;
8395 $digits = $CFG->minpassworddigits;
8396 $lower = $CFG->minpasswordlower;
8397 $upper = $CFG->minpasswordupper;
8398 $nonalphanum = $CFG->minpasswordnonalphanum;
8399 $total = $lower + $upper + $digits + $nonalphanum;
8400 // Var minlength should be the greater one of the two ( $minlen and $total ).
8401 $minlen = $minlen < $total ? $total : $minlen;
8402 // Var maxlen can never be smaller than minlen.
8403 $maxlen = $minlen > $maxlen ? $minlen : $maxlen;
8404 $additional = $maxlen - $total;
8406 // Make sure we have enough characters to fulfill
8407 // complexity requirements.
8408 $passworddigits = PASSWORD_DIGITS;
8409 while ($digits > strlen($passworddigits)) {
8410 $passworddigits .= PASSWORD_DIGITS;
8412 $passwordlower = PASSWORD_LOWER;
8413 while ($lower > strlen($passwordlower)) {
8414 $passwordlower .= PASSWORD_LOWER;
8416 $passwordupper = PASSWORD_UPPER;
8417 while ($upper > strlen($passwordupper)) {
8418 $passwordupper .= PASSWORD_UPPER;
8420 $passwordnonalphanum = PASSWORD_NONALPHANUM;
8421 while ($nonalphanum > strlen($passwordnonalphanum)) {
8422 $passwordnonalphanum .= PASSWORD_NONALPHANUM;
8425 // Now mix and shuffle it all.
8426 $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) .
8427 substr(str_shuffle ($passwordupper), 0, $upper) .
8428 substr(str_shuffle ($passworddigits), 0, $digits) .
8429 substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) .
8430 substr(str_shuffle ($passwordlower .
8431 $passwordupper .
8432 $passworddigits .
8433 $passwordnonalphanum), 0 , $additional));
8436 return substr ($password, 0, $maxlen);
8440 * Given a float, prints it nicely.
8441 * Localized floats must not be used in calculations!
8443 * The stripzeros feature is intended for making numbers look nicer in small
8444 * areas where it is not necessary to indicate the degree of accuracy by showing
8445 * ending zeros. If you turn it on with $decimalpoints set to 3, for example,
8446 * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'.
8448 * @param float $float The float to print
8449 * @param int $decimalpoints The number of decimal places to print. -1 is a special value for auto detect (full precision).
8450 * @param bool $localized use localized decimal separator
8451 * @param bool $stripzeros If true, removes final zeros after decimal point. It will be ignored and the trailing zeros after
8452 * the decimal point are always striped if $decimalpoints is -1.
8453 * @return string locale float
8455 function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=false) {
8456 if (is_null($float)) {
8457 return '';
8459 if ($localized) {
8460 $separator = get_string('decsep', 'langconfig');
8461 } else {
8462 $separator = '.';
8464 if ($decimalpoints == -1) {
8465 // The following counts the number of decimals.
8466 // It is safe as both floatval() and round() functions have same behaviour when non-numeric values are provided.
8467 $floatval = floatval($float);
8468 for ($decimalpoints = 0; $floatval != round($float, $decimalpoints); $decimalpoints++);
8471 $result = number_format($float, $decimalpoints, $separator, '');
8472 if ($stripzeros && $decimalpoints > 0) {
8473 // Remove zeros and final dot if not needed.
8474 // However, only do this if there is a decimal point!
8475 $result = preg_replace('~(' . preg_quote($separator, '~') . ')?0+$~', '', $result);
8477 return $result;
8481 * Converts locale specific floating point/comma number back to standard PHP float value
8482 * Do NOT try to do any math operations before this conversion on any user submitted floats!
8484 * @param string $localefloat locale aware float representation
8485 * @param bool $strict If true, then check the input and return false if it is not a valid number.
8486 * @return mixed float|bool - false or the parsed float.
8488 function unformat_float($localefloat, $strict = false) {
8489 $localefloat = trim((string)$localefloat);
8491 if ($localefloat == '') {
8492 return null;
8495 $localefloat = str_replace(' ', '', $localefloat); // No spaces - those might be used as thousand separators.
8496 $localefloat = str_replace(get_string('decsep', 'langconfig'), '.', $localefloat);
8498 if ($strict && !is_numeric($localefloat)) {
8499 return false;
8502 return (float)$localefloat;
8506 * Given a simple array, this shuffles it up just like shuffle()
8507 * Unlike PHP's shuffle() this function works on any machine.
8509 * @param array $array The array to be rearranged
8510 * @return array
8512 function swapshuffle($array) {
8514 $last = count($array) - 1;
8515 for ($i = 0; $i <= $last; $i++) {
8516 $from = rand(0, $last);
8517 $curr = $array[$i];
8518 $array[$i] = $array[$from];
8519 $array[$from] = $curr;
8521 return $array;
8525 * Like {@link swapshuffle()}, but works on associative arrays
8527 * @param array $array The associative array to be rearranged
8528 * @return array
8530 function swapshuffle_assoc($array) {
8532 $newarray = array();
8533 $newkeys = swapshuffle(array_keys($array));
8535 foreach ($newkeys as $newkey) {
8536 $newarray[$newkey] = $array[$newkey];
8538 return $newarray;
8542 * Given an arbitrary array, and a number of draws,
8543 * this function returns an array with that amount
8544 * of items. The indexes are retained.
8546 * @todo Finish documenting this function
8548 * @param array $array
8549 * @param int $draws
8550 * @return array
8552 function draw_rand_array($array, $draws) {
8554 $return = array();
8556 $last = count($array);
8558 if ($draws > $last) {
8559 $draws = $last;
8562 while ($draws > 0) {
8563 $last--;
8565 $keys = array_keys($array);
8566 $rand = rand(0, $last);
8568 $return[$keys[$rand]] = $array[$keys[$rand]];
8569 unset($array[$keys[$rand]]);
8571 $draws--;
8574 return $return;
8578 * Calculate the difference between two microtimes
8580 * @param string $a The first Microtime
8581 * @param string $b The second Microtime
8582 * @return string
8584 function microtime_diff($a, $b) {
8585 list($adec, $asec) = explode(' ', $a);
8586 list($bdec, $bsec) = explode(' ', $b);
8587 return $bsec - $asec + $bdec - $adec;
8591 * Given a list (eg a,b,c,d,e) this function returns
8592 * an array of 1->a, 2->b, 3->c etc
8594 * @param string $list The string to explode into array bits
8595 * @param string $separator The separator used within the list string
8596 * @return array The now assembled array
8598 function make_menu_from_list($list, $separator=',') {
8600 $array = array_reverse(explode($separator, $list), true);
8601 foreach ($array as $key => $item) {
8602 $outarray[$key+1] = trim($item);
8604 return $outarray;
8608 * Creates an array that represents all the current grades that
8609 * can be chosen using the given grading type.
8611 * Negative numbers
8612 * are scales, zero is no grade, and positive numbers are maximum
8613 * grades.
8615 * @todo Finish documenting this function or better deprecated this completely!
8617 * @param int $gradingtype
8618 * @return array
8620 function make_grades_menu($gradingtype) {
8621 global $DB;
8623 $grades = array();
8624 if ($gradingtype < 0) {
8625 if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) {
8626 return make_menu_from_list($scale->scale);
8628 } else if ($gradingtype > 0) {
8629 for ($i=$gradingtype; $i>=0; $i--) {
8630 $grades[$i] = $i .' / '. $gradingtype;
8632 return $grades;
8634 return $grades;
8638 * make_unique_id_code
8640 * @todo Finish documenting this function
8642 * @uses $_SERVER
8643 * @param string $extra Extra string to append to the end of the code
8644 * @return string
8646 function make_unique_id_code($extra = '') {
8648 $hostname = 'unknownhost';
8649 if (!empty($_SERVER['HTTP_HOST'])) {
8650 $hostname = $_SERVER['HTTP_HOST'];
8651 } else if (!empty($_ENV['HTTP_HOST'])) {
8652 $hostname = $_ENV['HTTP_HOST'];
8653 } else if (!empty($_SERVER['SERVER_NAME'])) {
8654 $hostname = $_SERVER['SERVER_NAME'];
8655 } else if (!empty($_ENV['SERVER_NAME'])) {
8656 $hostname = $_ENV['SERVER_NAME'];
8659 $date = gmdate("ymdHis");
8661 $random = random_string(6);
8663 if ($extra) {
8664 return $hostname .'+'. $date .'+'. $random .'+'. $extra;
8665 } else {
8666 return $hostname .'+'. $date .'+'. $random;
8672 * Function to check the passed address is within the passed subnet
8674 * The parameter is a comma separated string of subnet definitions.
8675 * Subnet strings can be in one of three formats:
8676 * 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn (number of bits in net mask)
8677 * 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)
8678 * 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. (incomplete address, a bit non-technical ;-)
8679 * Code for type 1 modified from user posted comments by mediator at
8680 * {@link http://au.php.net/manual/en/function.ip2long.php}
8682 * @param string $addr The address you are checking
8683 * @param string $subnetstr The string of subnet addresses
8684 * @param bool $checkallzeros The state to whether check for 0.0.0.0
8685 * @return bool
8687 function address_in_subnet($addr, $subnetstr, $checkallzeros = false) {
8689 if ($addr == '0.0.0.0' && !$checkallzeros) {
8690 return false;
8692 $subnets = explode(',', $subnetstr);
8693 $found = false;
8694 $addr = trim($addr);
8695 $addr = cleanremoteaddr($addr, false); // Normalise.
8696 if ($addr === null) {
8697 return false;
8699 $addrparts = explode(':', $addr);
8701 $ipv6 = strpos($addr, ':');
8703 foreach ($subnets as $subnet) {
8704 $subnet = trim($subnet);
8705 if ($subnet === '') {
8706 continue;
8709 if (strpos($subnet, '/') !== false) {
8710 // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn.
8711 list($ip, $mask) = explode('/', $subnet);
8712 $mask = trim($mask);
8713 if (!is_number($mask)) {
8714 continue; // Incorect mask number, eh?
8716 $ip = cleanremoteaddr($ip, false); // Normalise.
8717 if ($ip === null) {
8718 continue;
8720 if (strpos($ip, ':') !== false) {
8721 // IPv6.
8722 if (!$ipv6) {
8723 continue;
8725 if ($mask > 128 or $mask < 0) {
8726 continue; // Nonsense.
8728 if ($mask == 0) {
8729 return true; // Any address.
8731 if ($mask == 128) {
8732 if ($ip === $addr) {
8733 return true;
8735 continue;
8737 $ipparts = explode(':', $ip);
8738 $modulo = $mask % 16;
8739 $ipnet = array_slice($ipparts, 0, ($mask-$modulo)/16);
8740 $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16);
8741 if (implode(':', $ipnet) === implode(':', $addrnet)) {
8742 if ($modulo == 0) {
8743 return true;
8745 $pos = ($mask-$modulo)/16;
8746 $ipnet = hexdec($ipparts[$pos]);
8747 $addrnet = hexdec($addrparts[$pos]);
8748 $mask = 0xffff << (16 - $modulo);
8749 if (($addrnet & $mask) == ($ipnet & $mask)) {
8750 return true;
8754 } else {
8755 // IPv4.
8756 if ($ipv6) {
8757 continue;
8759 if ($mask > 32 or $mask < 0) {
8760 continue; // Nonsense.
8762 if ($mask == 0) {
8763 return true;
8765 if ($mask == 32) {
8766 if ($ip === $addr) {
8767 return true;
8769 continue;
8771 $mask = 0xffffffff << (32 - $mask);
8772 if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
8773 return true;
8777 } else if (strpos($subnet, '-') !== false) {
8778 // 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.
8779 $parts = explode('-', $subnet);
8780 if (count($parts) != 2) {
8781 continue;
8784 if (strpos($subnet, ':') !== false) {
8785 // IPv6.
8786 if (!$ipv6) {
8787 continue;
8789 $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
8790 if ($ipstart === null) {
8791 continue;
8793 $ipparts = explode(':', $ipstart);
8794 $start = hexdec(array_pop($ipparts));
8795 $ipparts[] = trim($parts[1]);
8796 $ipend = cleanremoteaddr(implode(':', $ipparts), false); // Normalise.
8797 if ($ipend === null) {
8798 continue;
8800 $ipparts[7] = '';
8801 $ipnet = implode(':', $ipparts);
8802 if (strpos($addr, $ipnet) !== 0) {
8803 continue;
8805 $ipparts = explode(':', $ipend);
8806 $end = hexdec($ipparts[7]);
8808 $addrend = hexdec($addrparts[7]);
8810 if (($addrend >= $start) and ($addrend <= $end)) {
8811 return true;
8814 } else {
8815 // IPv4.
8816 if ($ipv6) {
8817 continue;
8819 $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
8820 if ($ipstart === null) {
8821 continue;
8823 $ipparts = explode('.', $ipstart);
8824 $ipparts[3] = trim($parts[1]);
8825 $ipend = cleanremoteaddr(implode('.', $ipparts), false); // Normalise.
8826 if ($ipend === null) {
8827 continue;
8830 if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
8831 return true;
8835 } else {
8836 // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
8837 if (strpos($subnet, ':') !== false) {
8838 // IPv6.
8839 if (!$ipv6) {
8840 continue;
8842 $parts = explode(':', $subnet);
8843 $count = count($parts);
8844 if ($parts[$count-1] === '') {
8845 unset($parts[$count-1]); // Trim trailing :'s.
8846 $count--;
8847 $subnet = implode('.', $parts);
8849 $isip = cleanremoteaddr($subnet, false); // Normalise.
8850 if ($isip !== null) {
8851 if ($isip === $addr) {
8852 return true;
8854 continue;
8855 } else if ($count > 8) {
8856 continue;
8858 $zeros = array_fill(0, 8-$count, '0');
8859 $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16);
8860 if (address_in_subnet($addr, $subnet)) {
8861 return true;
8864 } else {
8865 // IPv4.
8866 if ($ipv6) {
8867 continue;
8869 $parts = explode('.', $subnet);
8870 $count = count($parts);
8871 if ($parts[$count-1] === '') {
8872 unset($parts[$count-1]); // Trim trailing .
8873 $count--;
8874 $subnet = implode('.', $parts);
8876 if ($count == 4) {
8877 $subnet = cleanremoteaddr($subnet, false); // Normalise.
8878 if ($subnet === $addr) {
8879 return true;
8881 continue;
8882 } else if ($count > 4) {
8883 continue;
8885 $zeros = array_fill(0, 4-$count, '0');
8886 $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8);
8887 if (address_in_subnet($addr, $subnet)) {
8888 return true;
8894 return false;
8898 * For outputting debugging info
8900 * @param string $string The string to write
8901 * @param string $eol The end of line char(s) to use
8902 * @param string $sleep Period to make the application sleep
8903 * This ensures any messages have time to display before redirect
8905 function mtrace($string, $eol="\n", $sleep=0) {
8906 global $CFG;
8908 if (isset($CFG->mtrace_wrapper) && function_exists($CFG->mtrace_wrapper)) {
8909 $fn = $CFG->mtrace_wrapper;
8910 $fn($string, $eol);
8911 return;
8912 } else if (defined('STDOUT') && !PHPUNIT_TEST && !defined('BEHAT_TEST')) {
8913 // We must explicitly call the add_line function here.
8914 // Uses of fwrite to STDOUT are not picked up by ob_start.
8915 if ($output = \core\task\logmanager::add_line("{$string}{$eol}")) {
8916 fwrite(STDOUT, $output);
8918 } else {
8919 echo $string . $eol;
8922 // Flush again.
8923 flush();
8925 // Delay to keep message on user's screen in case of subsequent redirect.
8926 if ($sleep) {
8927 sleep($sleep);
8932 * Helper to {@see mtrace()} an exception or throwable, including all relevant information.
8934 * @param Throwable $e the error to ouptput.
8936 function mtrace_exception(Throwable $e): void {
8937 $info = get_exception_info($e);
8939 $message = $info->message;
8940 if ($info->debuginfo) {
8941 $message .= "\n\n" . $info->debuginfo;
8943 if ($info->backtrace) {
8944 $message .= "\n\n" . format_backtrace($info->backtrace, true);
8947 mtrace($message);
8951 * Replace 1 or more slashes or backslashes to 1 slash
8953 * @param string $path The path to strip
8954 * @return string the path with double slashes removed
8956 function cleardoubleslashes ($path) {
8957 return preg_replace('/(\/|\\\){1,}/', '/', $path);
8961 * Is the current ip in a given list?
8963 * @param string $list
8964 * @return bool
8966 function remoteip_in_list($list) {
8967 $clientip = getremoteaddr(null);
8969 if (!$clientip) {
8970 // Ensure access on cli.
8971 return true;
8973 return \core\ip_utils::is_ip_in_subnet_list($clientip, $list);
8977 * Returns most reliable client address
8979 * @param string $default If an address can't be determined, then return this
8980 * @return string The remote IP address
8982 function getremoteaddr($default='0.0.0.0') {
8983 global $CFG;
8985 if (!isset($CFG->getremoteaddrconf)) {
8986 // This will happen, for example, before just after the upgrade, as the
8987 // user is redirected to the admin screen.
8988 $variablestoskip = GETREMOTEADDR_SKIP_DEFAULT;
8989 } else {
8990 $variablestoskip = $CFG->getremoteaddrconf;
8992 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) {
8993 if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
8994 $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']);
8995 return $address ? $address : $default;
8998 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
8999 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
9000 $forwardedaddresses = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']);
9002 $forwardedaddresses = array_filter($forwardedaddresses, function($ip) {
9003 global $CFG;
9004 return !\core\ip_utils::is_ip_in_subnet_list($ip, $CFG->reverseproxyignore ?? '', ',');
9007 // Multiple proxies can append values to this header including an
9008 // untrusted original request header so we must only trust the last ip.
9009 $address = end($forwardedaddresses);
9011 if (substr_count($address, ":") > 1) {
9012 // Remove port and brackets from IPv6.
9013 if (preg_match("/\[(.*)\]:/", $address, $matches)) {
9014 $address = $matches[1];
9016 } else {
9017 // Remove port from IPv4.
9018 if (substr_count($address, ":") == 1) {
9019 $parts = explode(":", $address);
9020 $address = $parts[0];
9024 $address = cleanremoteaddr($address);
9025 return $address ? $address : $default;
9028 if (!empty($_SERVER['REMOTE_ADDR'])) {
9029 $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']);
9030 return $address ? $address : $default;
9031 } else {
9032 return $default;
9037 * Cleans an ip address. Internal addresses are now allowed.
9038 * (Originally local addresses were not allowed.)
9040 * @param string $addr IPv4 or IPv6 address
9041 * @param bool $compress use IPv6 address compression
9042 * @return string normalised ip address string, null if error
9044 function cleanremoteaddr($addr, $compress=false) {
9045 $addr = trim($addr);
9047 if (strpos($addr, ':') !== false) {
9048 // Can be only IPv6.
9049 $parts = explode(':', $addr);
9050 $count = count($parts);
9052 if (strpos($parts[$count-1], '.') !== false) {
9053 // Legacy ipv4 notation.
9054 $last = array_pop($parts);
9055 $ipv4 = cleanremoteaddr($last, true);
9056 if ($ipv4 === null) {
9057 return null;
9059 $bits = explode('.', $ipv4);
9060 $parts[] = dechex($bits[0]).dechex($bits[1]);
9061 $parts[] = dechex($bits[2]).dechex($bits[3]);
9062 $count = count($parts);
9063 $addr = implode(':', $parts);
9066 if ($count < 3 or $count > 8) {
9067 return null; // Severly malformed.
9070 if ($count != 8) {
9071 if (strpos($addr, '::') === false) {
9072 return null; // Malformed.
9074 // Uncompress.
9075 $insertat = array_search('', $parts, true);
9076 $missing = array_fill(0, 1 + 8 - $count, '0');
9077 array_splice($parts, $insertat, 1, $missing);
9078 foreach ($parts as $key => $part) {
9079 if ($part === '') {
9080 $parts[$key] = '0';
9085 $adr = implode(':', $parts);
9086 if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) {
9087 return null; // Incorrect format - sorry.
9090 // Normalise 0s and case.
9091 $parts = array_map('hexdec', $parts);
9092 $parts = array_map('dechex', $parts);
9094 $result = implode(':', $parts);
9096 if (!$compress) {
9097 return $result;
9100 if ($result === '0:0:0:0:0:0:0:0') {
9101 return '::'; // All addresses.
9104 $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1);
9105 if ($compressed !== $result) {
9106 return $compressed;
9109 $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
9110 if ($compressed !== $result) {
9111 return $compressed;
9114 $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
9115 if ($compressed !== $result) {
9116 return $compressed;
9119 return $result;
9122 // First get all things that look like IPv4 addresses.
9123 $parts = array();
9124 if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
9125 return null;
9127 unset($parts[0]);
9129 foreach ($parts as $key => $match) {
9130 if ($match > 255) {
9131 return null;
9133 $parts[$key] = (int)$match; // Normalise 0s.
9136 return implode('.', $parts);
9141 * Is IP address a public address?
9143 * @param string $ip The ip to check
9144 * @return bool true if the ip is public
9146 function ip_is_public($ip) {
9147 return (bool) filter_var($ip, FILTER_VALIDATE_IP, (FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE));
9151 * This function will make a complete copy of anything it's given,
9152 * regardless of whether it's an object or not.
9154 * @param mixed $thing Something you want cloned
9155 * @return mixed What ever it is you passed it
9157 function fullclone($thing) {
9158 return unserialize(serialize($thing));
9162 * Used to make sure that $min <= $value <= $max
9164 * Make sure that value is between min, and max
9166 * @param int $min The minimum value
9167 * @param int $value The value to check
9168 * @param int $max The maximum value
9169 * @return int
9171 function bounded_number($min, $value, $max) {
9172 if ($value < $min) {
9173 return $min;
9175 if ($value > $max) {
9176 return $max;
9178 return $value;
9182 * Check if there is a nested array within the passed array
9184 * @param array $array
9185 * @return bool true if there is a nested array false otherwise
9187 function array_is_nested($array) {
9188 foreach ($array as $value) {
9189 if (is_array($value)) {
9190 return true;
9193 return false;
9197 * get_performance_info() pairs up with init_performance_info()
9198 * loaded in setup.php. Returns an array with 'html' and 'txt'
9199 * values ready for use, and each of the individual stats provided
9200 * separately as well.
9202 * @return array
9204 function get_performance_info() {
9205 global $CFG, $PERF, $DB, $PAGE;
9207 $info = array();
9208 $info['txt'] = me() . ' '; // Holds log-friendly representation.
9210 $info['html'] = '';
9211 if (!empty($CFG->themedesignermode)) {
9212 // Attempt to avoid devs debugging peformance issues, when its caused by css building and so on.
9213 $info['html'] .= '<p><strong>Warning: Theme designer mode is enabled.</strong></p>';
9215 $info['html'] .= '<ul class="list-unstyled row mx-md-0">'; // Holds userfriendly HTML representation.
9217 $info['realtime'] = microtime_diff($PERF->starttime, microtime());
9219 $info['html'] .= '<li class="timeused col-sm-4">'.$info['realtime'].' secs</li> ';
9220 $info['txt'] .= 'time: '.$info['realtime'].'s ';
9222 // GET/POST (or NULL if $_SERVER['REQUEST_METHOD'] is undefined) is useful for txt logged information.
9223 $info['txt'] .= 'method: ' . ($_SERVER['REQUEST_METHOD'] ?? "NULL") . ' ';
9225 if (function_exists('memory_get_usage')) {
9226 $info['memory_total'] = memory_get_usage();
9227 $info['memory_growth'] = memory_get_usage() - $PERF->startmemory;
9228 $info['html'] .= '<li class="memoryused col-sm-4">RAM: '.display_size($info['memory_total']).'</li> ';
9229 $info['txt'] .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '.
9230 $info['memory_growth'].'B ('.display_size($info['memory_growth']).') ';
9233 if (function_exists('memory_get_peak_usage')) {
9234 $info['memory_peak'] = memory_get_peak_usage();
9235 $info['html'] .= '<li class="memoryused col-sm-4">RAM peak: '.display_size($info['memory_peak']).'</li> ';
9236 $info['txt'] .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') ';
9239 $info['html'] .= '</ul><ul class="list-unstyled row mx-md-0">';
9240 $inc = get_included_files();
9241 $info['includecount'] = count($inc);
9242 $info['html'] .= '<li class="included col-sm-4">Included '.$info['includecount'].' files</li> ';
9243 $info['txt'] .= 'includecount: '.$info['includecount'].' ';
9245 if (!empty($CFG->early_install_lang) or empty($PAGE)) {
9246 // We can not track more performance before installation or before PAGE init, sorry.
9247 return $info;
9250 $filtermanager = filter_manager::instance();
9251 if (method_exists($filtermanager, 'get_performance_summary')) {
9252 list($filterinfo, $nicenames) = $filtermanager->get_performance_summary();
9253 $info = array_merge($filterinfo, $info);
9254 foreach ($filterinfo as $key => $value) {
9255 $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9256 $info['txt'] .= "$key: $value ";
9260 $stringmanager = get_string_manager();
9261 if (method_exists($stringmanager, 'get_performance_summary')) {
9262 list($filterinfo, $nicenames) = $stringmanager->get_performance_summary();
9263 $info = array_merge($filterinfo, $info);
9264 foreach ($filterinfo as $key => $value) {
9265 $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9266 $info['txt'] .= "$key: $value ";
9270 $info['dbqueries'] = $DB->perf_get_reads().'/'.$DB->perf_get_writes();
9271 $info['html'] .= '<li class="dbqueries col-sm-4">DB reads/writes: '.$info['dbqueries'].'</li> ';
9272 $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' ';
9274 if ($DB->want_read_slave()) {
9275 $info['dbreads_slave'] = $DB->perf_get_reads_slave();
9276 $info['html'] .= '<li class="dbqueries col-sm-4">DB reads from slave: '.$info['dbreads_slave'].'</li> ';
9277 $info['txt'] .= 'db reads from slave: '.$info['dbreads_slave'].' ';
9280 $info['dbtime'] = round($DB->perf_get_queries_time(), 5);
9281 $info['html'] .= '<li class="dbtime col-sm-4">DB queries time: '.$info['dbtime'].' secs</li> ';
9282 $info['txt'] .= 'db queries time: ' . $info['dbtime'] . 's ';
9284 if (function_exists('posix_times')) {
9285 $ptimes = posix_times();
9286 if (is_array($ptimes)) {
9287 foreach ($ptimes as $key => $val) {
9288 $info[$key] = $ptimes[$key] - $PERF->startposixtimes[$key];
9290 $info['html'] .= "<li class=\"posixtimes col-sm-4\">ticks: $info[ticks] user: $info[utime]";
9291 $info['html'] .= "sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</li> ";
9292 $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] ";
9296 // Grab the load average for the last minute.
9297 // /proc will only work under some linux configurations
9298 // while uptime is there under MacOSX/Darwin and other unices.
9299 if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) {
9300 list($serverload) = explode(' ', $loadavg[0]);
9301 unset($loadavg);
9302 } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) {
9303 if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) {
9304 $serverload = $matches[1];
9305 } else {
9306 trigger_error('Could not parse uptime output!');
9309 if (!empty($serverload)) {
9310 $info['serverload'] = $serverload;
9311 $info['html'] .= '<li class="serverload col-sm-4">Load average: '.$info['serverload'].'</li> ';
9312 $info['txt'] .= "serverload: {$info['serverload']} ";
9315 // Display size of session if session started.
9316 if ($si = \core\session\manager::get_performance_info()) {
9317 $info['sessionsize'] = $si['size'];
9318 $info['html'] .= "<li class=\"serverload col-sm-4\">" . $si['html'] . "</li>";
9319 $info['txt'] .= $si['txt'];
9322 // Display time waiting for session if applicable.
9323 if (!empty($PERF->sessionlock['wait'])) {
9324 $sessionwait = number_format($PERF->sessionlock['wait'], 3) . ' secs';
9325 $info['html'] .= html_writer::tag('li', 'Session wait: ' . $sessionwait, [
9326 'class' => 'sessionwait col-sm-4'
9328 $info['txt'] .= 'sessionwait: ' . $sessionwait . ' ';
9331 $info['html'] .= '</ul>';
9332 $html = '';
9333 if ($stats = cache_helper::get_stats()) {
9335 $table = new html_table();
9336 $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9337 $table->head = ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S', 'I/O'];
9338 $table->data = [];
9339 $table->align = ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right', 'right'];
9341 $text = 'Caches used (hits/misses/sets): ';
9342 $hits = 0;
9343 $misses = 0;
9344 $sets = 0;
9345 $maxstores = 0;
9347 // We want to align static caches into their own column.
9348 $hasstatic = false;
9349 foreach ($stats as $definition => $details) {
9350 $numstores = count($details['stores']);
9351 $first = key($details['stores']);
9352 if ($first !== cache_store::STATIC_ACCEL) {
9353 $numstores++; // Add a blank space for the missing static store.
9355 $maxstores = max($maxstores, $numstores);
9358 $storec = 0;
9360 while ($storec++ < ($maxstores - 2)) {
9361 if ($storec == ($maxstores - 2)) {
9362 $table->head[] = get_string('mappingfinal', 'cache');
9363 } else {
9364 $table->head[] = "Store $storec";
9366 $table->align[] = 'left';
9367 $table->align[] = 'right';
9368 $table->align[] = 'right';
9369 $table->align[] = 'right';
9370 $table->align[] = 'right';
9371 $table->head[] = 'H';
9372 $table->head[] = 'M';
9373 $table->head[] = 'S';
9374 $table->head[] = 'I/O';
9377 ksort($stats);
9379 foreach ($stats as $definition => $details) {
9380 switch ($details['mode']) {
9381 case cache_store::MODE_APPLICATION:
9382 $modeclass = 'application';
9383 $mode = ' <span title="application cache">App</span>';
9384 break;
9385 case cache_store::MODE_SESSION:
9386 $modeclass = 'session';
9387 $mode = ' <span title="session cache">Ses</span>';
9388 break;
9389 case cache_store::MODE_REQUEST:
9390 $modeclass = 'request';
9391 $mode = ' <span title="request cache">Req</span>';
9392 break;
9394 $row = [$mode, $definition];
9396 $text .= "$definition {";
9398 $storec = 0;
9399 foreach ($details['stores'] as $store => $data) {
9401 if ($storec == 0 && $store !== cache_store::STATIC_ACCEL) {
9402 $row[] = '';
9403 $row[] = '';
9404 $row[] = '';
9405 $storec++;
9408 $hits += $data['hits'];
9409 $misses += $data['misses'];
9410 $sets += $data['sets'];
9411 if ($data['hits'] == 0 and $data['misses'] > 0) {
9412 $cachestoreclass = 'nohits bg-danger';
9413 } else if ($data['hits'] < $data['misses']) {
9414 $cachestoreclass = 'lowhits bg-warning text-dark';
9415 } else {
9416 $cachestoreclass = 'hihits';
9418 $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
9419 $cell = new html_table_cell($store);
9420 $cell->attributes = ['class' => $cachestoreclass];
9421 $row[] = $cell;
9422 $cell = new html_table_cell($data['hits']);
9423 $cell->attributes = ['class' => $cachestoreclass];
9424 $row[] = $cell;
9425 $cell = new html_table_cell($data['misses']);
9426 $cell->attributes = ['class' => $cachestoreclass];
9427 $row[] = $cell;
9429 if ($store !== cache_store::STATIC_ACCEL) {
9430 // The static cache is never set.
9431 $cell = new html_table_cell($data['sets']);
9432 $cell->attributes = ['class' => $cachestoreclass];
9433 $row[] = $cell;
9435 if ($data['hits'] || $data['sets']) {
9436 if ($data['iobytes'] === cache_store::IO_BYTES_NOT_SUPPORTED) {
9437 $size = '-';
9438 } else {
9439 $size = display_size($data['iobytes'], 1, 'KB');
9440 if ($data['iobytes'] >= 10 * 1024) {
9441 $cachestoreclass = ' bg-warning text-dark';
9444 } else {
9445 $size = '';
9447 $cell = new html_table_cell($size);
9448 $cell->attributes = ['class' => $cachestoreclass];
9449 $row[] = $cell;
9451 $storec++;
9453 while ($storec++ < $maxstores) {
9454 $row[] = '';
9455 $row[] = '';
9456 $row[] = '';
9457 $row[] = '';
9458 $row[] = '';
9460 $text .= '} ';
9462 $table->data[] = $row;
9465 $html .= html_writer::table($table);
9467 // Now lets also show sub totals for each cache store.
9468 $storetotals = [];
9469 $storetotal = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
9470 foreach ($stats as $definition => $details) {
9471 foreach ($details['stores'] as $store => $data) {
9472 if (!array_key_exists($store, $storetotals)) {
9473 $storetotals[$store] = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
9475 $storetotals[$store]['class'] = $data['class'];
9476 $storetotals[$store]['hits'] += $data['hits'];
9477 $storetotals[$store]['misses'] += $data['misses'];
9478 $storetotals[$store]['sets'] += $data['sets'];
9479 $storetotal['hits'] += $data['hits'];
9480 $storetotal['misses'] += $data['misses'];
9481 $storetotal['sets'] += $data['sets'];
9482 if ($data['iobytes'] !== cache_store::IO_BYTES_NOT_SUPPORTED) {
9483 $storetotals[$store]['iobytes'] += $data['iobytes'];
9484 $storetotal['iobytes'] += $data['iobytes'];
9489 $table = new html_table();
9490 $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9491 $table->head = [get_string('storename', 'cache'), get_string('type_cachestore', 'plugin'), 'H', 'M', 'S', 'I/O'];
9492 $table->data = [];
9493 $table->align = ['left', 'left', 'right', 'right', 'right', 'right'];
9495 ksort($storetotals);
9497 foreach ($storetotals as $store => $data) {
9498 $row = [];
9499 if ($data['hits'] == 0 and $data['misses'] > 0) {
9500 $cachestoreclass = 'nohits bg-danger';
9501 } else if ($data['hits'] < $data['misses']) {
9502 $cachestoreclass = 'lowhits bg-warning text-dark';
9503 } else {
9504 $cachestoreclass = 'hihits';
9506 $cell = new html_table_cell($store);
9507 $cell->attributes = ['class' => $cachestoreclass];
9508 $row[] = $cell;
9509 $cell = new html_table_cell($data['class']);
9510 $cell->attributes = ['class' => $cachestoreclass];
9511 $row[] = $cell;
9512 $cell = new html_table_cell($data['hits']);
9513 $cell->attributes = ['class' => $cachestoreclass];
9514 $row[] = $cell;
9515 $cell = new html_table_cell($data['misses']);
9516 $cell->attributes = ['class' => $cachestoreclass];
9517 $row[] = $cell;
9518 $cell = new html_table_cell($data['sets']);
9519 $cell->attributes = ['class' => $cachestoreclass];
9520 $row[] = $cell;
9521 if ($data['hits'] || $data['sets']) {
9522 if ($data['iobytes']) {
9523 $size = display_size($data['iobytes'], 1, 'KB');
9524 } else {
9525 $size = '-';
9527 } else {
9528 $size = '';
9530 $cell = new html_table_cell($size);
9531 $cell->attributes = ['class' => $cachestoreclass];
9532 $row[] = $cell;
9533 $table->data[] = $row;
9535 if (!empty($storetotal['iobytes'])) {
9536 $size = display_size($storetotal['iobytes'], 1, 'KB');
9537 } else if (!empty($storetotal['hits']) || !empty($storetotal['sets'])) {
9538 $size = '-';
9539 } else {
9540 $size = '';
9542 $row = [
9543 get_string('total'),
9545 $storetotal['hits'],
9546 $storetotal['misses'],
9547 $storetotal['sets'],
9548 $size,
9550 $table->data[] = $row;
9552 $html .= html_writer::table($table);
9554 $info['cachesused'] = "$hits / $misses / $sets";
9555 $info['html'] .= $html;
9556 $info['txt'] .= $text.'. ';
9557 } else {
9558 $info['cachesused'] = '0 / 0 / 0';
9559 $info['html'] .= '<div class="cachesused">Caches used (hits/misses/sets): 0/0/0</div>';
9560 $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 ';
9563 // Display lock information if any.
9564 if (!empty($PERF->locks)) {
9565 $table = new html_table();
9566 $table->attributes['class'] = 'locktimings table table-dark table-sm w-auto table-bordered';
9567 $table->head = ['Lock', 'Waited (s)', 'Obtained', 'Held for (s)'];
9568 $table->align = ['left', 'right', 'center', 'right'];
9569 $table->data = [];
9570 $text = 'Locks (waited/obtained/held):';
9571 foreach ($PERF->locks as $locktiming) {
9572 $row = [];
9573 $row[] = s($locktiming->type . '/' . $locktiming->resource);
9574 $text .= ' ' . $locktiming->type . '/' . $locktiming->resource . ' (';
9576 // The time we had to wait to get the lock.
9577 $roundedtime = number_format($locktiming->wait, 1);
9578 $cell = new html_table_cell($roundedtime);
9579 if ($locktiming->wait > 0.5) {
9580 $cell->attributes = ['class' => 'bg-warning text-dark'];
9582 $row[] = $cell;
9583 $text .= $roundedtime . '/';
9585 // Show a tick or cross for success.
9586 $row[] = $locktiming->success ? '&#x2713;' : '&#x274c;';
9587 $text .= ($locktiming->success ? 'y' : 'n') . '/';
9589 // If applicable, show how long we held the lock before releasing it.
9590 if (property_exists($locktiming, 'held')) {
9591 $roundedtime = number_format($locktiming->held, 1);
9592 $cell = new html_table_cell($roundedtime);
9593 if ($locktiming->held > 0.5) {
9594 $cell->attributes = ['class' => 'bg-warning text-dark'];
9596 $row[] = $cell;
9597 $text .= $roundedtime;
9598 } else {
9599 $row[] = '-';
9600 $text .= '-';
9602 $text .= ')';
9604 $table->data[] = $row;
9606 $info['html'] .= html_writer::table($table);
9607 $info['txt'] .= $text . '. ';
9610 $info['html'] = '<div class="performanceinfo siteinfo container-fluid px-md-0 overflow-auto pt-3">'.$info['html'].'</div>';
9611 return $info;
9615 * Renames a file or directory to a unique name within the same directory.
9617 * This function is designed to avoid any potential race conditions, and select an unused name.
9619 * @param string $filepath Original filepath
9620 * @param string $prefix Prefix to use for the temporary name
9621 * @return string|bool New file path or false if failed
9622 * @since Moodle 3.10
9624 function rename_to_unused_name(string $filepath, string $prefix = '_temp_') {
9625 $dir = dirname($filepath);
9626 $basename = $dir . '/' . $prefix;
9627 $limit = 0;
9628 while ($limit < 100) {
9629 // Select a new name based on a random number.
9630 $newfilepath = $basename . md5(mt_rand());
9632 // Attempt a rename to that new name.
9633 if (@rename($filepath, $newfilepath)) {
9634 return $newfilepath;
9637 // The first time, do some sanity checks, maybe it is failing for a good reason and there
9638 // is no point trying 100 times if so.
9639 if ($limit === 0 && (!file_exists($filepath) || !is_writable($dir))) {
9640 return false;
9642 $limit++;
9644 return false;
9648 * Delete directory or only its content
9650 * @param string $dir directory path
9651 * @param bool $contentonly
9652 * @return bool success, true also if dir does not exist
9654 function remove_dir($dir, $contentonly=false) {
9655 if (!is_dir($dir)) {
9656 // Nothing to do.
9657 return true;
9660 if (!$contentonly) {
9661 // Start by renaming the directory; this will guarantee that other processes don't write to it
9662 // while it is in the process of being deleted.
9663 $tempdir = rename_to_unused_name($dir);
9664 if ($tempdir) {
9665 // If the rename was successful then delete the $tempdir instead.
9666 $dir = $tempdir;
9668 // If the rename fails, we will continue through and attempt to delete the directory
9669 // without renaming it since that is likely to at least delete most of the files.
9672 if (!$handle = opendir($dir)) {
9673 return false;
9675 $result = true;
9676 while (false!==($item = readdir($handle))) {
9677 if ($item != '.' && $item != '..') {
9678 if (is_dir($dir.'/'.$item)) {
9679 $result = remove_dir($dir.'/'.$item) && $result;
9680 } else {
9681 $result = unlink($dir.'/'.$item) && $result;
9685 closedir($handle);
9686 if ($contentonly) {
9687 clearstatcache(); // Make sure file stat cache is properly invalidated.
9688 return $result;
9690 $result = rmdir($dir); // If anything left the result will be false, no need for && $result.
9691 clearstatcache(); // Make sure file stat cache is properly invalidated.
9692 return $result;
9696 * Detect if an object or a class contains a given property
9697 * will take an actual object or the name of a class
9699 * @param mixed $obj Name of class or real object to test
9700 * @param string $property name of property to find
9701 * @return bool true if property exists
9703 function object_property_exists( $obj, $property ) {
9704 if (is_string( $obj )) {
9705 $properties = get_class_vars( $obj );
9706 } else {
9707 $properties = get_object_vars( $obj );
9709 return array_key_exists( $property, $properties );
9713 * Converts an object into an associative array
9715 * This function converts an object into an associative array by iterating
9716 * over its public properties. Because this function uses the foreach
9717 * construct, Iterators are respected. It works recursively on arrays of objects.
9718 * Arrays and simple values are returned as is.
9720 * If class has magic properties, it can implement IteratorAggregate
9721 * and return all available properties in getIterator()
9723 * @param mixed $var
9724 * @return array
9726 function convert_to_array($var) {
9727 $result = array();
9729 // Loop over elements/properties.
9730 foreach ($var as $key => $value) {
9731 // Recursively convert objects.
9732 if (is_object($value) || is_array($value)) {
9733 $result[$key] = convert_to_array($value);
9734 } else {
9735 // Simple values are untouched.
9736 $result[$key] = $value;
9739 return $result;
9743 * Detect a custom script replacement in the data directory that will
9744 * replace an existing moodle script
9746 * @return string|bool full path name if a custom script exists, false if no custom script exists
9748 function custom_script_path() {
9749 global $CFG, $SCRIPT;
9751 if ($SCRIPT === null) {
9752 // Probably some weird external script.
9753 return false;
9756 $scriptpath = $CFG->customscripts . $SCRIPT;
9758 // Check the custom script exists.
9759 if (file_exists($scriptpath) and is_file($scriptpath)) {
9760 return $scriptpath;
9761 } else {
9762 return false;
9767 * Returns whether or not the user object is a remote MNET user. This function
9768 * is in moodlelib because it does not rely on loading any of the MNET code.
9770 * @param object $user A valid user object
9771 * @return bool True if the user is from a remote Moodle.
9773 function is_mnet_remote_user($user) {
9774 global $CFG;
9776 if (!isset($CFG->mnet_localhost_id)) {
9777 include_once($CFG->dirroot . '/mnet/lib.php');
9778 $env = new mnet_environment();
9779 $env->init();
9780 unset($env);
9783 return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id);
9787 * This function will search for browser prefereed languages, setting Moodle
9788 * to use the best one available if $SESSION->lang is undefined
9790 function setup_lang_from_browser() {
9791 global $CFG, $SESSION, $USER;
9793 if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) {
9794 // Lang is defined in session or user profile, nothing to do.
9795 return;
9798 if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do.
9799 return;
9802 // Extract and clean langs from headers.
9803 $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
9804 $rawlangs = str_replace('-', '_', $rawlangs); // We are using underscores.
9805 $rawlangs = explode(',', $rawlangs); // Convert to array.
9806 $langs = array();
9808 $order = 1.0;
9809 foreach ($rawlangs as $lang) {
9810 if (strpos($lang, ';') === false) {
9811 $langs[(string)$order] = $lang;
9812 $order = $order-0.01;
9813 } else {
9814 $parts = explode(';', $lang);
9815 $pos = strpos($parts[1], '=');
9816 $langs[substr($parts[1], $pos+1)] = $parts[0];
9819 krsort($langs, SORT_NUMERIC);
9821 // Look for such langs under standard locations.
9822 foreach ($langs as $lang) {
9823 // Clean it properly for include.
9824 $lang = strtolower(clean_param($lang, PARAM_SAFEDIR));
9825 if (get_string_manager()->translation_exists($lang, false)) {
9826 // If the translation for this language exists then try to set it
9827 // for the rest of the session, if this is a read only session then
9828 // we can only set it temporarily in $CFG.
9829 if (defined('READ_ONLY_SESSION') && !empty($CFG->enable_read_only_sessions)) {
9830 $CFG->lang = $lang;
9831 } else {
9832 $SESSION->lang = $lang;
9834 // We have finished. Go out.
9835 break;
9838 return;
9842 * Check if $url matches anything in proxybypass list
9844 * Any errors just result in the proxy being used (least bad)
9846 * @param string $url url to check
9847 * @return boolean true if we should bypass the proxy
9849 function is_proxybypass( $url ) {
9850 global $CFG;
9852 // Sanity check.
9853 if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) {
9854 return false;
9857 // Get the host part out of the url.
9858 if (!$host = parse_url( $url, PHP_URL_HOST )) {
9859 return false;
9862 // Get the possible bypass hosts into an array.
9863 $matches = explode( ',', $CFG->proxybypass );
9865 // Check for a exact match on the IP or in the domains.
9866 $isdomaininallowedlist = \core\ip_utils::is_domain_in_allowed_list($host, $matches);
9867 $isipinsubnetlist = \core\ip_utils::is_ip_in_subnet_list($host, $CFG->proxybypass, ',');
9869 if ($isdomaininallowedlist || $isipinsubnetlist) {
9870 return true;
9873 // Nothing matched.
9874 return false;
9878 * Check if the passed navigation is of the new style
9880 * @param mixed $navigation
9881 * @return bool true for yes false for no
9883 function is_newnav($navigation) {
9884 if (is_array($navigation) && !empty($navigation['newnav'])) {
9885 return true;
9886 } else {
9887 return false;
9892 * Checks whether the given variable name is defined as a variable within the given object.
9894 * This will NOT work with stdClass objects, which have no class variables.
9896 * @param string $var The variable name
9897 * @param object $object The object to check
9898 * @return boolean
9900 function in_object_vars($var, $object) {
9901 $classvars = get_class_vars(get_class($object));
9902 $classvars = array_keys($classvars);
9903 return in_array($var, $classvars);
9907 * Returns an array without repeated objects.
9908 * This function is similar to array_unique, but for arrays that have objects as values
9910 * @param array $array
9911 * @param bool $keepkeyassoc
9912 * @return array
9914 function object_array_unique($array, $keepkeyassoc = true) {
9915 $duplicatekeys = array();
9916 $tmp = array();
9918 foreach ($array as $key => $val) {
9919 // Convert objects to arrays, in_array() does not support objects.
9920 if (is_object($val)) {
9921 $val = (array)$val;
9924 if (!in_array($val, $tmp)) {
9925 $tmp[] = $val;
9926 } else {
9927 $duplicatekeys[] = $key;
9931 foreach ($duplicatekeys as $key) {
9932 unset($array[$key]);
9935 return $keepkeyassoc ? $array : array_values($array);
9939 * Is a userid the primary administrator?
9941 * @param int $userid int id of user to check
9942 * @return boolean
9944 function is_primary_admin($userid) {
9945 $primaryadmin = get_admin();
9947 if ($userid == $primaryadmin->id) {
9948 return true;
9949 } else {
9950 return false;
9955 * Returns the site identifier
9957 * @return string $CFG->siteidentifier, first making sure it is properly initialised.
9959 function get_site_identifier() {
9960 global $CFG;
9961 // Check to see if it is missing. If so, initialise it.
9962 if (empty($CFG->siteidentifier)) {
9963 set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']);
9965 // Return it.
9966 return $CFG->siteidentifier;
9970 * Check whether the given password has no more than the specified
9971 * number of consecutive identical characters.
9973 * @param string $password password to be checked against the password policy
9974 * @param integer $maxchars maximum number of consecutive identical characters
9975 * @return bool
9977 function check_consecutive_identical_characters($password, $maxchars) {
9979 if ($maxchars < 1) {
9980 return true; // Zero 0 is to disable this check.
9982 if (strlen($password) <= $maxchars) {
9983 return true; // Too short to fail this test.
9986 $previouschar = '';
9987 $consecutivecount = 1;
9988 foreach (str_split($password) as $char) {
9989 if ($char != $previouschar) {
9990 $consecutivecount = 1;
9991 } else {
9992 $consecutivecount++;
9993 if ($consecutivecount > $maxchars) {
9994 return false; // Check failed already.
9998 $previouschar = $char;
10001 return true;
10005 * Helper function to do partial function binding.
10006 * so we can use it for preg_replace_callback, for example
10007 * this works with php functions, user functions, static methods and class methods
10008 * it returns you a callback that you can pass on like so:
10010 * $callback = partial('somefunction', $arg1, $arg2);
10011 * or
10012 * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
10013 * or even
10014 * $obj = new someclass();
10015 * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2);
10017 * and then the arguments that are passed through at calltime are appended to the argument list.
10019 * @param mixed $function a php callback
10020 * @param mixed $arg1,... $argv arguments to partially bind with
10021 * @return array Array callback
10023 function partial() {
10024 if (!class_exists('partial')) {
10026 * Used to manage function binding.
10027 * @copyright 2009 Penny Leach
10028 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10030 class partial{
10031 /** @var array */
10032 public $values = array();
10033 /** @var string The function to call as a callback. */
10034 public $func;
10036 * Constructor
10037 * @param string $func
10038 * @param array $args
10040 public function __construct($func, $args) {
10041 $this->values = $args;
10042 $this->func = $func;
10045 * Calls the callback function.
10046 * @return mixed
10048 public function method() {
10049 $args = func_get_args();
10050 return call_user_func_array($this->func, array_merge($this->values, $args));
10054 $args = func_get_args();
10055 $func = array_shift($args);
10056 $p = new partial($func, $args);
10057 return array($p, 'method');
10061 * helper function to load up and initialise the mnet environment
10062 * this must be called before you use mnet functions.
10064 * @return mnet_environment the equivalent of old $MNET global
10066 function get_mnet_environment() {
10067 global $CFG;
10068 require_once($CFG->dirroot . '/mnet/lib.php');
10069 static $instance = null;
10070 if (empty($instance)) {
10071 $instance = new mnet_environment();
10072 $instance->init();
10074 return $instance;
10078 * during xmlrpc server code execution, any code wishing to access
10079 * information about the remote peer must use this to get it.
10081 * @return mnet_remote_client the equivalent of old $MNETREMOTE_CLIENT global
10083 function get_mnet_remote_client() {
10084 if (!defined('MNET_SERVER')) {
10085 debugging(get_string('notinxmlrpcserver', 'mnet'));
10086 return false;
10088 global $MNET_REMOTE_CLIENT;
10089 if (isset($MNET_REMOTE_CLIENT)) {
10090 return $MNET_REMOTE_CLIENT;
10092 return false;
10096 * during the xmlrpc server code execution, this will be called
10097 * to setup the object returned by {@link get_mnet_remote_client}
10099 * @param mnet_remote_client $client the client to set up
10100 * @throws moodle_exception
10102 function set_mnet_remote_client($client) {
10103 if (!defined('MNET_SERVER')) {
10104 throw new moodle_exception('notinxmlrpcserver', 'mnet');
10106 global $MNET_REMOTE_CLIENT;
10107 $MNET_REMOTE_CLIENT = $client;
10111 * return the jump url for a given remote user
10112 * this is used for rewriting forum post links in emails, etc
10114 * @param stdclass $user the user to get the idp url for
10116 function mnet_get_idp_jump_url($user) {
10117 global $CFG;
10119 static $mnetjumps = array();
10120 if (!array_key_exists($user->mnethostid, $mnetjumps)) {
10121 $idp = mnet_get_peer_host($user->mnethostid);
10122 $idpjumppath = mnet_get_app_jumppath($idp->applicationid);
10123 $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl=';
10125 return $mnetjumps[$user->mnethostid];
10129 * Gets the homepage to use for the current user
10131 * @return int One of HOMEPAGE_*
10133 function get_home_page() {
10134 global $CFG;
10136 if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) {
10137 // If dashboard is disabled, home will be set to default page.
10138 $defaultpage = get_default_home_page();
10139 if ($CFG->defaulthomepage == HOMEPAGE_MY) {
10140 if (!empty($CFG->enabledashboard)) {
10141 return HOMEPAGE_MY;
10142 } else {
10143 return $defaultpage;
10145 } else if ($CFG->defaulthomepage == HOMEPAGE_MYCOURSES) {
10146 return HOMEPAGE_MYCOURSES;
10147 } else {
10148 $userhomepage = (int) get_user_preferences('user_home_page_preference', $defaultpage);
10149 if (empty($CFG->enabledashboard) && $userhomepage == HOMEPAGE_MY) {
10150 // If the user was using the dashboard but it's disabled, return the default home page.
10151 $userhomepage = $defaultpage;
10153 return $userhomepage;
10156 return HOMEPAGE_SITE;
10160 * Returns the default home page to display if current one is not defined or can't be applied.
10161 * The default behaviour is to return Dashboard if it's enabled or My courses page if it isn't.
10163 * @return int The default home page.
10165 function get_default_home_page(): int {
10166 global $CFG;
10168 return (!isset($CFG->enabledashboard) || $CFG->enabledashboard) ? HOMEPAGE_MY : HOMEPAGE_MYCOURSES;
10172 * Gets the name of a course to be displayed when showing a list of courses.
10173 * By default this is just $course->fullname but user can configure it. The
10174 * result of this function should be passed through print_string.
10175 * @param stdClass|core_course_list_element $course Moodle course object
10176 * @return string Display name of course (either fullname or short + fullname)
10178 function get_course_display_name_for_list($course) {
10179 global $CFG;
10180 if (!empty($CFG->courselistshortnames)) {
10181 if (!($course instanceof stdClass)) {
10182 $course = (object)convert_to_array($course);
10184 return get_string('courseextendednamedisplay', '', $course);
10185 } else {
10186 return $course->fullname;
10191 * Safe analogue of unserialize() that can only parse arrays
10193 * Arrays may contain only integers or strings as both keys and values. Nested arrays are allowed.
10195 * @param string $expression
10196 * @return array|bool either parsed array or false if parsing was impossible.
10198 function unserialize_array($expression) {
10200 // Check the expression is an array.
10201 if (!preg_match('/^a:(\d+):/', $expression)) {
10202 return false;
10205 $values = (array) unserialize_object($expression);
10207 // Callback that returns true if the given value is an unserialized object, executes recursively.
10208 $invalidvaluecallback = static function($value) use (&$invalidvaluecallback): bool {
10209 if (is_array($value)) {
10210 return (bool) array_filter($value, $invalidvaluecallback);
10212 return ($value instanceof stdClass) || ($value instanceof __PHP_Incomplete_Class);
10215 // Iterate over the result to ensure there are no stray objects.
10216 if (array_filter($values, $invalidvaluecallback)) {
10217 return false;
10220 return $values;
10224 * Safe method for unserializing given input that is expected to contain only a serialized instance of an stdClass object
10226 * If any class type other than stdClass is included in the input string, it will not be instantiated and will be cast to an
10227 * stdClass object. The initial cast to array, then back to object is to ensure we are always returning the correct type,
10228 * otherwise we would return an instances of {@see __PHP_Incomplete_class} for malformed strings
10230 * @param string $input
10231 * @return stdClass
10233 function unserialize_object(string $input): stdClass {
10234 $instance = (array) unserialize($input, ['allowed_classes' => [stdClass::class]]);
10235 return (object) $instance;
10239 * The lang_string class
10241 * This special class is used to create an object representation of a string request.
10242 * It is special because processing doesn't occur until the object is first used.
10243 * The class was created especially to aid performance in areas where strings were
10244 * required to be generated but were not necessarily used.
10245 * As an example the admin tree when generated uses over 1500 strings, of which
10246 * normally only 1/3 are ever actually printed at any time.
10247 * The performance advantage is achieved by not actually processing strings that
10248 * arn't being used, as such reducing the processing required for the page.
10250 * How to use the lang_string class?
10251 * There are two methods of using the lang_string class, first through the
10252 * forth argument of the get_string function, and secondly directly.
10253 * The following are examples of both.
10254 * 1. Through get_string calls e.g.
10255 * $string = get_string($identifier, $component, $a, true);
10256 * $string = get_string('yes', 'moodle', null, true);
10257 * 2. Direct instantiation
10258 * $string = new lang_string($identifier, $component, $a, $lang);
10259 * $string = new lang_string('yes');
10261 * How do I use a lang_string object?
10262 * The lang_string object makes use of a magic __toString method so that you
10263 * are able to use the object exactly as you would use a string in most cases.
10264 * This means you are able to collect it into a variable and then directly
10265 * echo it, or concatenate it into another string, or similar.
10266 * The other thing you can do is manually get the string by calling the
10267 * lang_strings out method e.g.
10268 * $string = new lang_string('yes');
10269 * $string->out();
10270 * Also worth noting is that the out method can take one argument, $lang which
10271 * allows the developer to change the language on the fly.
10273 * When should I use a lang_string object?
10274 * The lang_string object is designed to be used in any situation where a
10275 * string may not be needed, but needs to be generated.
10276 * The admin tree is a good example of where lang_string objects should be
10277 * used.
10278 * A more practical example would be any class that requries strings that may
10279 * not be printed (after all classes get renderer by renderers and who knows
10280 * what they will do ;))
10282 * When should I not use a lang_string object?
10283 * Don't use lang_strings when you are going to use a string immediately.
10284 * There is no need as it will be processed immediately and there will be no
10285 * advantage, and in fact perhaps a negative hit as a class has to be
10286 * instantiated for a lang_string object, however get_string won't require
10287 * that.
10289 * Limitations:
10290 * 1. You cannot use a lang_string object as an array offset. Doing so will
10291 * result in PHP throwing an error. (You can use it as an object property!)
10293 * @package core
10294 * @category string
10295 * @copyright 2011 Sam Hemelryk
10296 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10298 class lang_string {
10300 /** @var string The strings identifier */
10301 protected $identifier;
10302 /** @var string The strings component. Default '' */
10303 protected $component = '';
10304 /** @var array|stdClass Any arguments required for the string. Default null */
10305 protected $a = null;
10306 /** @var string The language to use when processing the string. Default null */
10307 protected $lang = null;
10309 /** @var string The processed string (once processed) */
10310 protected $string = null;
10313 * A special boolean. If set to true then the object has been woken up and
10314 * cannot be regenerated. If this is set then $this->string MUST be used.
10315 * @var bool
10317 protected $forcedstring = false;
10320 * Constructs a lang_string object
10322 * This function should do as little processing as possible to ensure the best
10323 * performance for strings that won't be used.
10325 * @param string $identifier The strings identifier
10326 * @param string $component The strings component
10327 * @param stdClass|array|mixed $a Any arguments the string requires
10328 * @param string $lang The language to use when processing the string.
10329 * @throws coding_exception
10331 public function __construct($identifier, $component = '', $a = null, $lang = null) {
10332 if (empty($component)) {
10333 $component = 'moodle';
10336 $this->identifier = $identifier;
10337 $this->component = $component;
10338 $this->lang = $lang;
10340 // We MUST duplicate $a to ensure that it if it changes by reference those
10341 // changes are not carried across.
10342 // To do this we always ensure $a or its properties/values are strings
10343 // and that any properties/values that arn't convertable are forgotten.
10344 if ($a !== null) {
10345 if (is_scalar($a)) {
10346 $this->a = $a;
10347 } else if ($a instanceof lang_string) {
10348 $this->a = $a->out();
10349 } else if (is_object($a) or is_array($a)) {
10350 $a = (array)$a;
10351 $this->a = array();
10352 foreach ($a as $key => $value) {
10353 // Make sure conversion errors don't get displayed (results in '').
10354 if (is_array($value)) {
10355 $this->a[$key] = '';
10356 } else if (is_object($value)) {
10357 if (method_exists($value, '__toString')) {
10358 $this->a[$key] = $value->__toString();
10359 } else {
10360 $this->a[$key] = '';
10362 } else {
10363 $this->a[$key] = (string)$value;
10369 if (debugging(false, DEBUG_DEVELOPER)) {
10370 if (clean_param($this->identifier, PARAM_STRINGID) == '') {
10371 throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition');
10373 if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') {
10374 throw new coding_exception('Invalid string compontent. Please check your string definition');
10376 if (!get_string_manager()->string_exists($this->identifier, $this->component)) {
10377 debugging('String does not exist. Please check your string definition for '.$this->identifier.'/'.$this->component, DEBUG_DEVELOPER);
10383 * Processes the string.
10385 * This function actually processes the string, stores it in the string property
10386 * and then returns it.
10387 * You will notice that this function is VERY similar to the get_string method.
10388 * That is because it is pretty much doing the same thing.
10389 * However as this function is an upgrade it isn't as tolerant to backwards
10390 * compatibility.
10392 * @return string
10393 * @throws coding_exception
10395 protected function get_string() {
10396 global $CFG;
10398 // Check if we need to process the string.
10399 if ($this->string === null) {
10400 // Check the quality of the identifier.
10401 if ($CFG->debugdeveloper && clean_param($this->identifier, PARAM_STRINGID) === '') {
10402 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);
10405 // Process the string.
10406 $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang);
10407 // Debugging feature lets you display string identifier and component.
10408 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
10409 $this->string .= ' {' . $this->identifier . '/' . $this->component . '}';
10412 // Return the string.
10413 return $this->string;
10417 * Returns the string
10419 * @param string $lang The langauge to use when processing the string
10420 * @return string
10422 public function out($lang = null) {
10423 if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) {
10424 if ($this->forcedstring) {
10425 debugging('lang_string objects that have been used cannot be printed in another language. ('.$this->lang.' used)', DEBUG_DEVELOPER);
10426 return $this->get_string();
10428 $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang);
10429 return $translatedstring->out();
10431 return $this->get_string();
10435 * Magic __toString method for printing a string
10437 * @return string
10439 public function __toString() {
10440 return $this->get_string();
10444 * Magic __set_state method used for var_export
10446 * @param array $array
10447 * @return self
10449 public static function __set_state(array $array): self {
10450 $tmp = new lang_string($array['identifier'], $array['component'], $array['a'], $array['lang']);
10451 $tmp->string = $array['string'];
10452 $tmp->forcedstring = $array['forcedstring'];
10453 return $tmp;
10457 * Prepares the lang_string for sleep and stores only the forcedstring and
10458 * string properties... the string cannot be regenerated so we need to ensure
10459 * it is generated for this.
10461 * @return string
10463 public function __sleep() {
10464 $this->get_string();
10465 $this->forcedstring = true;
10466 return array('forcedstring', 'string', 'lang');
10470 * Returns the identifier.
10472 * @return string
10474 public function get_identifier() {
10475 return $this->identifier;
10479 * Returns the component.
10481 * @return string
10483 public function get_component() {
10484 return $this->component;
10489 * Get human readable name describing the given callable.
10491 * This performs syntax check only to see if the given param looks like a valid function, method or closure.
10492 * It does not check if the callable actually exists.
10494 * @param callable|string|array $callable
10495 * @return string|bool Human readable name of callable, or false if not a valid callable.
10497 function get_callable_name($callable) {
10499 if (!is_callable($callable, true, $name)) {
10500 return false;
10502 } else {
10503 return $name;
10508 * Tries to guess if $CFG->wwwroot is publicly accessible or not.
10509 * Never put your faith on this function and rely on its accuracy as there might be false positives.
10510 * It just performs some simple checks, and mainly is used for places where we want to hide some options
10511 * such as site registration when $CFG->wwwroot is not publicly accessible.
10512 * Good thing is there is no false negative.
10513 * Note that it's possible to force the result of this check by specifying $CFG->site_is_public in config.php
10515 * @return bool
10517 function site_is_public() {
10518 global $CFG;
10520 // Return early if site admin has forced this setting.
10521 if (isset($CFG->site_is_public)) {
10522 return (bool)$CFG->site_is_public;
10525 $host = parse_url($CFG->wwwroot, PHP_URL_HOST);
10527 if ($host === 'localhost' || preg_match('|^127\.\d+\.\d+\.\d+$|', $host)) {
10528 $ispublic = false;
10529 } else if (\core\ip_utils::is_ip_address($host) && !ip_is_public($host)) {
10530 $ispublic = false;
10531 } else if (($address = \core\ip_utils::get_ip_address($host)) && !ip_is_public($address)) {
10532 $ispublic = false;
10533 } else {
10534 $ispublic = true;
10537 return $ispublic;
10541 * Validates user's password length.
10543 * @param string $password
10544 * @param int $pepperlength The length of the used peppers
10545 * @return bool
10547 function exceeds_password_length(string $password, int $pepperlength = 0): bool {
10548 return (strlen($password) > (MAX_PASSWORD_CHARACTERS + $pepperlength));
10552 * A helper to replace PHP 8.3 usage of array_keys with two args.
10554 * There is an indication that this will become a new method in PHP 8.4, but that has not happened yet.
10555 * Therefore this non-polyfill has been created with a different naming convention.
10556 * In the future it can be deprecated if a core PHP method is created.
10558 * https://wiki.php.net/rfc/deprecate_functions_with_overloaded_signatures#array_keys
10560 * @param array $array
10561 * @param mixed $filter The value to filter on
10562 * @param bool $strict Whether to apply a strit test with the filter
10563 * @return array
10565 function moodle_array_keys_filter(array $array, mixed $filter, bool $strict = false): array {
10566 return array_keys(array_filter(
10567 $array,
10568 function($value, $key) use ($filter, $strict): bool {
10569 if ($strict) {
10570 return $value === $filter;
10572 return $value == $filter;
10574 ARRAY_FILTER_USE_BOTH,