MDL-41197 normalize ascii text conversion
[moodle.git] / lib / moodlelib.php
blob69151fdccb651d1f61e7f9908e00210674322e22
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 /**
75 * PARAM_ALPHA - contains only english ascii letters a-zA-Z.
77 define('PARAM_ALPHA', 'alpha');
79 /**
80 * PARAM_ALPHAEXT the same contents as PARAM_ALPHA plus the chars in quotes: "_-" allowed
81 * NOTE: originally this allowed "/" too, please use PARAM_SAFEPATH if "/" needed
83 define('PARAM_ALPHAEXT', 'alphaext');
85 /**
86 * PARAM_ALPHANUM - expected numbers and letters only.
88 define('PARAM_ALPHANUM', 'alphanum');
90 /**
91 * PARAM_ALPHANUMEXT - expected numbers, letters only and _-.
93 define('PARAM_ALPHANUMEXT', 'alphanumext');
95 /**
96 * PARAM_AUTH - actually checks to make sure the string is a valid auth plugin
98 define('PARAM_AUTH', 'auth');
101 * PARAM_BASE64 - Base 64 encoded format
103 define('PARAM_BASE64', 'base64');
106 * PARAM_BOOL - converts input into 0 or 1, use for switches in forms and urls.
108 define('PARAM_BOOL', 'bool');
111 * PARAM_CAPABILITY - A capability name, like 'moodle/role:manage'. Actually
112 * checked against the list of capabilities in the database.
114 define('PARAM_CAPABILITY', 'capability');
117 * PARAM_CLEANHTML - cleans submitted HTML code. Note that you almost never want
118 * to use this. The normal mode of operation is to use PARAM_RAW when recieving
119 * the input (required/optional_param or formslib) and then sanitse the HTML
120 * using format_text on output. This is for the rare cases when you want to
121 * sanitise the HTML on input. This cleaning may also fix xhtml strictness.
123 define('PARAM_CLEANHTML', 'cleanhtml');
126 * PARAM_EMAIL - an email address following the RFC
128 define('PARAM_EMAIL', 'email');
131 * PARAM_FILE - safe file name, all dangerous chars are stripped, protects against XSS, SQL injections and directory traversals
133 define('PARAM_FILE', 'file');
136 * PARAM_FLOAT - a real/floating point number.
138 * Note that you should not use PARAM_FLOAT for numbers typed in by the user.
139 * It does not work for languages that use , as a decimal separator.
140 * Instead, do something like
141 * $rawvalue = required_param('name', PARAM_RAW);
142 * // ... other code including require_login, which sets current lang ...
143 * $realvalue = unformat_float($rawvalue);
144 * // ... then use $realvalue
146 define('PARAM_FLOAT', 'float');
149 * PARAM_HOST - expected fully qualified domain name (FQDN) or an IPv4 dotted quad (IP address)
151 define('PARAM_HOST', 'host');
154 * PARAM_INT - integers only, use when expecting only numbers.
156 define('PARAM_INT', 'int');
159 * PARAM_LANG - checks to see if the string is a valid installed language in the current site.
161 define('PARAM_LANG', 'lang');
164 * PARAM_LOCALURL - expected properly formatted URL as well as one that refers to the local server itself. (NOT orthogonal to the
165 * others! Implies PARAM_URL!)
167 define('PARAM_LOCALURL', 'localurl');
170 * PARAM_NOTAGS - all html tags are stripped from the text. Do not abuse this type.
172 define('PARAM_NOTAGS', 'notags');
175 * PARAM_PATH - safe relative path name, all dangerous chars are stripped, protects against XSS, SQL injections and directory
176 * traversals note: the leading slash is not removed, window drive letter is not allowed
178 define('PARAM_PATH', 'path');
181 * PARAM_PEM - Privacy Enhanced Mail format
183 define('PARAM_PEM', 'pem');
186 * PARAM_PERMISSION - A permission, one of CAP_INHERIT, CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT.
188 define('PARAM_PERMISSION', 'permission');
191 * PARAM_RAW specifies a parameter that is not cleaned/processed in any way except the discarding of the invalid utf-8 characters
193 define('PARAM_RAW', 'raw');
196 * PARAM_RAW_TRIMMED like PARAM_RAW but leading and trailing whitespace is stripped.
198 define('PARAM_RAW_TRIMMED', 'raw_trimmed');
201 * PARAM_SAFEDIR - safe directory name, suitable for include() and require()
203 define('PARAM_SAFEDIR', 'safedir');
206 * PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths, etc.
208 define('PARAM_SAFEPATH', 'safepath');
211 * PARAM_SEQUENCE - expects a sequence of numbers like 8 to 1,5,6,4,6,8,9. Numbers and comma only.
213 define('PARAM_SEQUENCE', 'sequence');
216 * PARAM_TAG - one tag (interests, blogs, etc.) - mostly international characters and space, <> not supported
218 define('PARAM_TAG', 'tag');
221 * PARAM_TAGLIST - list of tags separated by commas (interests, blogs, etc.)
223 define('PARAM_TAGLIST', 'taglist');
226 * PARAM_TEXT - general plain text compatible with multilang filter, no other html tags. Please note '<', or '>' are allowed here.
228 define('PARAM_TEXT', 'text');
231 * PARAM_THEME - Checks to see if the string is a valid theme name in the current site
233 define('PARAM_THEME', 'theme');
236 * PARAM_URL - expected properly formatted URL. Please note that domain part is required, http://localhost/ is not accepted but
237 * http://localhost.localdomain/ is ok.
239 define('PARAM_URL', 'url');
242 * PARAM_USERNAME - Clean username to only contains allowed characters. This is to be used ONLY when manually creating user
243 * accounts, do NOT use when syncing with external systems!!
245 define('PARAM_USERNAME', 'username');
248 * PARAM_STRINGID - used to check if the given string is valid string identifier for get_string()
250 define('PARAM_STRINGID', 'stringid');
252 // DEPRECATED PARAM TYPES OR ALIASES - DO NOT USE FOR NEW CODE.
254 * PARAM_CLEAN - obsoleted, please use a more specific type of parameter.
255 * It was one of the first types, that is why it is abused so much ;-)
256 * @deprecated since 2.0
258 define('PARAM_CLEAN', 'clean');
261 * PARAM_INTEGER - deprecated alias for PARAM_INT
262 * @deprecated since 2.0
264 define('PARAM_INTEGER', 'int');
267 * PARAM_NUMBER - deprecated alias of PARAM_FLOAT
268 * @deprecated since 2.0
270 define('PARAM_NUMBER', 'float');
273 * PARAM_ACTION - deprecated alias for PARAM_ALPHANUMEXT, use for various actions in forms and urls
274 * NOTE: originally alias for PARAM_APLHA
275 * @deprecated since 2.0
277 define('PARAM_ACTION', 'alphanumext');
280 * PARAM_FORMAT - deprecated alias for PARAM_ALPHANUMEXT, use for names of plugins, formats, etc.
281 * NOTE: originally alias for PARAM_APLHA
282 * @deprecated since 2.0
284 define('PARAM_FORMAT', 'alphanumext');
287 * PARAM_MULTILANG - deprecated alias of PARAM_TEXT.
288 * @deprecated since 2.0
290 define('PARAM_MULTILANG', 'text');
293 * PARAM_TIMEZONE - expected timezone. Timezone can be int +-(0-13) or float +-(0.5-12.5) or
294 * string separated by '/' and can have '-' &/ '_' (eg. America/North_Dakota/New_Salem
295 * America/Port-au-Prince)
297 define('PARAM_TIMEZONE', 'timezone');
300 * PARAM_CLEANFILE - deprecated alias of PARAM_FILE; originally was removing regional chars too
302 define('PARAM_CLEANFILE', 'file');
305 * PARAM_COMPONENT is used for full component names (aka frankenstyle) such as 'mod_forum', 'core_rating', 'auth_ldap'.
306 * Short legacy subsystem names and module names are accepted too ex: 'forum', 'rating', 'user'.
307 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
308 * NOTE: numbers and underscores are strongly discouraged in plugin names!
310 define('PARAM_COMPONENT', 'component');
313 * PARAM_AREA is a name of area used when addressing files, comments, ratings, etc.
314 * It is usually used together with context id and component.
315 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
317 define('PARAM_AREA', 'area');
320 * PARAM_PLUGIN is used for plugin names such as 'forum', 'glossary', 'ldap', 'radius', 'paypal', 'completionstatus'.
321 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
322 * NOTE: numbers and underscores are strongly discouraged in plugin names! Underscores are forbidden in module names.
324 define('PARAM_PLUGIN', 'plugin');
327 // Web Services.
330 * VALUE_REQUIRED - if the parameter is not supplied, there is an error
332 define('VALUE_REQUIRED', 1);
335 * VALUE_OPTIONAL - if the parameter is not supplied, then the param has no value
337 define('VALUE_OPTIONAL', 2);
340 * VALUE_DEFAULT - if the parameter is not supplied, then the default value is used
342 define('VALUE_DEFAULT', 0);
345 * NULL_NOT_ALLOWED - the parameter can not be set to null in the database
347 define('NULL_NOT_ALLOWED', false);
350 * NULL_ALLOWED - the parameter can be set to null in the database
352 define('NULL_ALLOWED', true);
354 // Page types.
357 * PAGE_COURSE_VIEW is a definition of a page type. For more information on the page class see moodle/lib/pagelib.php.
359 define('PAGE_COURSE_VIEW', 'course-view');
361 /** Get remote addr constant */
362 define('GETREMOTEADDR_SKIP_HTTP_CLIENT_IP', '1');
363 /** Get remote addr constant */
364 define('GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR', '2');
366 // Blog access level constant declaration.
367 define ('BLOG_USER_LEVEL', 1);
368 define ('BLOG_GROUP_LEVEL', 2);
369 define ('BLOG_COURSE_LEVEL', 3);
370 define ('BLOG_SITE_LEVEL', 4);
371 define ('BLOG_GLOBAL_LEVEL', 5);
374 // Tag constants.
376 * To prevent problems with multibytes strings,Flag updating in nav not working on the review page. this should not exceed the
377 * length of "varchar(255) / 3 (bytes / utf-8 character) = 85".
378 * TODO: this is not correct, varchar(255) are 255 unicode chars ;-)
380 * @todo define(TAG_MAX_LENGTH) this is not correct, varchar(255) are 255 unicode chars ;-)
382 define('TAG_MAX_LENGTH', 50);
384 // Password policy constants.
385 define ('PASSWORD_LOWER', 'abcdefghijklmnopqrstuvwxyz');
386 define ('PASSWORD_UPPER', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
387 define ('PASSWORD_DIGITS', '0123456789');
388 define ('PASSWORD_NONALPHANUM', '.,;:!?_-+/*@#&$');
390 // Feature constants.
391 // Used for plugin_supports() to report features that are, or are not, supported by a module.
393 /** True if module can provide a grade */
394 define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade');
395 /** True if module supports outcomes */
396 define('FEATURE_GRADE_OUTCOMES', 'outcomes');
397 /** True if module supports advanced grading methods */
398 define('FEATURE_ADVANCED_GRADING', 'grade_advanced_grading');
399 /** True if module controls the grade visibility over the gradebook */
400 define('FEATURE_CONTROLS_GRADE_VISIBILITY', 'controlsgradevisbility');
401 /** True if module supports plagiarism plugins */
402 define('FEATURE_PLAGIARISM', 'plagiarism');
404 /** True if module has code to track whether somebody viewed it */
405 define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views');
406 /** True if module has custom completion rules */
407 define('FEATURE_COMPLETION_HAS_RULES', 'completion_has_rules');
409 /** True if module has no 'view' page (like label) */
410 define('FEATURE_NO_VIEW_LINK', 'viewlink');
411 /** True if module supports outcomes */
412 define('FEATURE_IDNUMBER', 'idnumber');
413 /** True if module supports groups */
414 define('FEATURE_GROUPS', 'groups');
415 /** True if module supports groupings */
416 define('FEATURE_GROUPINGS', 'groupings');
417 /** True if module supports groupmembersonly */
418 define('FEATURE_GROUPMEMBERSONLY', 'groupmembersonly');
420 /** Type of module */
421 define('FEATURE_MOD_ARCHETYPE', 'mod_archetype');
422 /** True if module supports intro editor */
423 define('FEATURE_MOD_INTRO', 'mod_intro');
424 /** True if module has default completion */
425 define('FEATURE_MODEDIT_DEFAULT_COMPLETION', 'modedit_default_completion');
427 define('FEATURE_COMMENT', 'comment');
429 define('FEATURE_RATE', 'rate');
430 /** True if module supports backup/restore of moodle2 format */
431 define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2');
433 /** True if module can show description on course main page */
434 define('FEATURE_SHOW_DESCRIPTION', 'showdescription');
436 /** True if module uses the question bank */
437 define('FEATURE_USES_QUESTIONS', 'usesquestions');
439 /** Unspecified module archetype */
440 define('MOD_ARCHETYPE_OTHER', 0);
441 /** Resource-like type module */
442 define('MOD_ARCHETYPE_RESOURCE', 1);
443 /** Assignment module archetype */
444 define('MOD_ARCHETYPE_ASSIGNMENT', 2);
445 /** System (not user-addable) module archetype */
446 define('MOD_ARCHETYPE_SYSTEM', 3);
448 /** Return this from modname_get_types callback to use default display in activity chooser */
449 define('MOD_SUBTYPE_NO_CHILDREN', 'modsubtypenochildren');
452 * Security token used for allowing access
453 * from external application such as web services.
454 * Scripts do not use any session, performance is relatively
455 * low because we need to load access info in each request.
456 * Scripts are executed in parallel.
458 define('EXTERNAL_TOKEN_PERMANENT', 0);
461 * Security token used for allowing access
462 * of embedded applications, the code is executed in the
463 * active user session. Token is invalidated after user logs out.
464 * Scripts are executed serially - normal session locking is used.
466 define('EXTERNAL_TOKEN_EMBEDDED', 1);
469 * The home page should be the site home
471 define('HOMEPAGE_SITE', 0);
473 * The home page should be the users my page
475 define('HOMEPAGE_MY', 1);
477 * The home page can be chosen by the user
479 define('HOMEPAGE_USER', 2);
482 * Hub directory url (should be moodle.org)
484 define('HUB_HUBDIRECTORYURL', "http://hubdirectory.moodle.org");
488 * Moodle.org url (should be moodle.org)
490 define('HUB_MOODLEORGHUBURL', "http://hub.moodle.org");
493 * Moodle mobile app service name
495 define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app');
498 * Indicates the user has the capabilities required to ignore activity and course file size restrictions
500 define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1);
503 * Course display settings: display all sections on one page.
505 define('COURSE_DISPLAY_SINGLEPAGE', 0);
507 * Course display settings: split pages into a page per section.
509 define('COURSE_DISPLAY_MULTIPAGE', 1);
512 * Authentication constant: String used in password field when password is not stored.
514 define('AUTH_PASSWORD_NOT_CACHED', 'not cached');
516 // PARAMETER HANDLING.
519 * Returns a particular value for the named variable, taken from
520 * POST or GET. If the parameter doesn't exist then an error is
521 * thrown because we require this variable.
523 * This function should be used to initialise all required values
524 * in a script that are based on parameters. Usually it will be
525 * used like this:
526 * $id = required_param('id', PARAM_INT);
528 * Please note the $type parameter is now required and the value can not be array.
530 * @param string $parname the name of the page parameter we want
531 * @param string $type expected type of parameter
532 * @return mixed
533 * @throws coding_exception
535 function required_param($parname, $type) {
536 if (func_num_args() != 2 or empty($parname) or empty($type)) {
537 throw new coding_exception('required_param() requires $parname and $type to be specified (parameter: '.$parname.')');
539 // POST has precedence.
540 if (isset($_POST[$parname])) {
541 $param = $_POST[$parname];
542 } else if (isset($_GET[$parname])) {
543 $param = $_GET[$parname];
544 } else {
545 print_error('missingparam', '', '', $parname);
548 if (is_array($param)) {
549 debugging('Invalid array parameter detected in required_param(): '.$parname);
550 // TODO: switch to fatal error in Moodle 2.3.
551 return required_param_array($parname, $type);
554 return clean_param($param, $type);
558 * Returns a particular array value for the named variable, taken from
559 * POST or GET. If the parameter doesn't exist then an error is
560 * thrown because we require this variable.
562 * This function should be used to initialise all required values
563 * in a script that are based on parameters. Usually it will be
564 * used like this:
565 * $ids = required_param_array('ids', PARAM_INT);
567 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
569 * @param string $parname the name of the page parameter we want
570 * @param string $type expected type of parameter
571 * @return array
572 * @throws coding_exception
574 function required_param_array($parname, $type) {
575 if (func_num_args() != 2 or empty($parname) or empty($type)) {
576 throw new coding_exception('required_param_array() requires $parname and $type to be specified (parameter: '.$parname.')');
578 // POST has precedence.
579 if (isset($_POST[$parname])) {
580 $param = $_POST[$parname];
581 } else if (isset($_GET[$parname])) {
582 $param = $_GET[$parname];
583 } else {
584 print_error('missingparam', '', '', $parname);
586 if (!is_array($param)) {
587 print_error('missingparam', '', '', $parname);
590 $result = array();
591 foreach ($param as $key => $value) {
592 if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
593 debugging('Invalid key name in required_param_array() detected: '.$key.', parameter: '.$parname);
594 continue;
596 $result[$key] = clean_param($value, $type);
599 return $result;
603 * Returns a particular value for the named variable, taken from
604 * POST or GET, otherwise returning a given default.
606 * This function should be used to initialise all optional values
607 * in a script that are based on parameters. Usually it will be
608 * used like this:
609 * $name = optional_param('name', 'Fred', PARAM_TEXT);
611 * Please note the $type parameter is now required and the value can not be array.
613 * @param string $parname the name of the page parameter we want
614 * @param mixed $default the default value to return if nothing is found
615 * @param string $type expected type of parameter
616 * @return mixed
617 * @throws coding_exception
619 function optional_param($parname, $default, $type) {
620 if (func_num_args() != 3 or empty($parname) or empty($type)) {
621 throw new coding_exception('optional_param requires $parname, $default + $type to be specified (parameter: '.$parname.')');
623 if (!isset($default)) {
624 $default = null;
627 // POST has precedence.
628 if (isset($_POST[$parname])) {
629 $param = $_POST[$parname];
630 } else if (isset($_GET[$parname])) {
631 $param = $_GET[$parname];
632 } else {
633 return $default;
636 if (is_array($param)) {
637 debugging('Invalid array parameter detected in required_param(): '.$parname);
638 // TODO: switch to $default in Moodle 2.3.
639 return optional_param_array($parname, $default, $type);
642 return clean_param($param, $type);
646 * Returns a particular array value for the named variable, taken from
647 * POST or GET, otherwise returning a given default.
649 * This function should be used to initialise all optional values
650 * in a script that are based on parameters. Usually it will be
651 * used like this:
652 * $ids = optional_param('id', array(), PARAM_INT);
654 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
656 * @param string $parname the name of the page parameter we want
657 * @param mixed $default the default value to return if nothing is found
658 * @param string $type expected type of parameter
659 * @return array
660 * @throws coding_exception
662 function optional_param_array($parname, $default, $type) {
663 if (func_num_args() != 3 or empty($parname) or empty($type)) {
664 throw new coding_exception('optional_param_array requires $parname, $default + $type to be specified (parameter: '.$parname.')');
667 // POST has precedence.
668 if (isset($_POST[$parname])) {
669 $param = $_POST[$parname];
670 } else if (isset($_GET[$parname])) {
671 $param = $_GET[$parname];
672 } else {
673 return $default;
675 if (!is_array($param)) {
676 debugging('optional_param_array() expects array parameters only: '.$parname);
677 return $default;
680 $result = array();
681 foreach ($param as $key => $value) {
682 if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
683 debugging('Invalid key name in optional_param_array() detected: '.$key.', parameter: '.$parname);
684 continue;
686 $result[$key] = clean_param($value, $type);
689 return $result;
693 * Strict validation of parameter values, the values are only converted
694 * to requested PHP type. Internally it is using clean_param, the values
695 * before and after cleaning must be equal - otherwise
696 * an invalid_parameter_exception is thrown.
697 * Objects and classes are not accepted.
699 * @param mixed $param
700 * @param string $type PARAM_ constant
701 * @param bool $allownull are nulls valid value?
702 * @param string $debuginfo optional debug information
703 * @return mixed the $param value converted to PHP type
704 * @throws invalid_parameter_exception if $param is not of given type
706 function validate_param($param, $type, $allownull=NULL_NOT_ALLOWED, $debuginfo='') {
707 if (is_null($param)) {
708 if ($allownull == NULL_ALLOWED) {
709 return null;
710 } else {
711 throw new invalid_parameter_exception($debuginfo);
714 if (is_array($param) or is_object($param)) {
715 throw new invalid_parameter_exception($debuginfo);
718 $cleaned = clean_param($param, $type);
720 if ($type == PARAM_FLOAT) {
721 // Do not detect precision loss here.
722 if (is_float($param) or is_int($param)) {
723 // These always fit.
724 } else if (!is_numeric($param) or !preg_match('/^[\+-]?[0-9]*\.?[0-9]*(e[-+]?[0-9]+)?$/i', (string)$param)) {
725 throw new invalid_parameter_exception($debuginfo);
727 } else if ((string)$param !== (string)$cleaned) {
728 // Conversion to string is usually lossless.
729 throw new invalid_parameter_exception($debuginfo);
732 return $cleaned;
736 * Makes sure array contains only the allowed types, this function does not validate array key names!
738 * <code>
739 * $options = clean_param($options, PARAM_INT);
740 * </code>
742 * @param array $param the variable array we are cleaning
743 * @param string $type expected format of param after cleaning.
744 * @param bool $recursive clean recursive arrays
745 * @return array
746 * @throws coding_exception
748 function clean_param_array(array $param = null, $type, $recursive = false) {
749 // Convert null to empty array.
750 $param = (array)$param;
751 foreach ($param as $key => $value) {
752 if (is_array($value)) {
753 if ($recursive) {
754 $param[$key] = clean_param_array($value, $type, true);
755 } else {
756 throw new coding_exception('clean_param_array can not process multidimensional arrays when $recursive is false.');
758 } else {
759 $param[$key] = clean_param($value, $type);
762 return $param;
766 * Used by {@link optional_param()} and {@link required_param()} to
767 * clean the variables and/or cast to specific types, based on
768 * an options field.
769 * <code>
770 * $course->format = clean_param($course->format, PARAM_ALPHA);
771 * $selectedgradeitem = clean_param($selectedgradeitem, PARAM_INT);
772 * </code>
774 * @param mixed $param the variable we are cleaning
775 * @param string $type expected format of param after cleaning.
776 * @return mixed
777 * @throws coding_exception
779 function clean_param($param, $type) {
780 global $CFG;
782 if (is_array($param)) {
783 throw new coding_exception('clean_param() can not process arrays, please use clean_param_array() instead.');
784 } else if (is_object($param)) {
785 if (method_exists($param, '__toString')) {
786 $param = $param->__toString();
787 } else {
788 throw new coding_exception('clean_param() can not process objects, please use clean_param_array() instead.');
792 switch ($type) {
793 case PARAM_RAW:
794 // No cleaning at all.
795 $param = fix_utf8($param);
796 return $param;
798 case PARAM_RAW_TRIMMED:
799 // No cleaning, but strip leading and trailing whitespace.
800 $param = fix_utf8($param);
801 return trim($param);
803 case PARAM_CLEAN:
804 // General HTML cleaning, try to use more specific type if possible this is deprecated!
805 // Please use more specific type instead.
806 if (is_numeric($param)) {
807 return $param;
809 $param = fix_utf8($param);
810 // Sweep for scripts, etc.
811 return clean_text($param);
813 case PARAM_CLEANHTML:
814 // Clean html fragment.
815 $param = fix_utf8($param);
816 // Sweep for scripts, etc.
817 $param = clean_text($param, FORMAT_HTML);
818 return trim($param);
820 case PARAM_INT:
821 // Convert to integer.
822 return (int)$param;
824 case PARAM_FLOAT:
825 // Convert to float.
826 return (float)$param;
828 case PARAM_ALPHA:
829 // Remove everything not `a-z`.
830 return preg_replace('/[^a-zA-Z]/i', '', $param);
832 case PARAM_ALPHAEXT:
833 // Remove everything not `a-zA-Z_-` (originally allowed "/" too).
834 return preg_replace('/[^a-zA-Z_-]/i', '', $param);
836 case PARAM_ALPHANUM:
837 // Remove everything not `a-zA-Z0-9`.
838 return preg_replace('/[^A-Za-z0-9]/i', '', $param);
840 case PARAM_ALPHANUMEXT:
841 // Remove everything not `a-zA-Z0-9_-`.
842 return preg_replace('/[^A-Za-z0-9_-]/i', '', $param);
844 case PARAM_SEQUENCE:
845 // Remove everything not `0-9,`.
846 return preg_replace('/[^0-9,]/i', '', $param);
848 case PARAM_BOOL:
849 // Convert to 1 or 0.
850 $tempstr = strtolower($param);
851 if ($tempstr === 'on' or $tempstr === 'yes' or $tempstr === 'true') {
852 $param = 1;
853 } else if ($tempstr === 'off' or $tempstr === 'no' or $tempstr === 'false') {
854 $param = 0;
855 } else {
856 $param = empty($param) ? 0 : 1;
858 return $param;
860 case PARAM_NOTAGS:
861 // Strip all tags.
862 $param = fix_utf8($param);
863 return strip_tags($param);
865 case PARAM_TEXT:
866 // Leave only tags needed for multilang.
867 $param = fix_utf8($param);
868 // If the multilang syntax is not correct we strip all tags because it would break xhtml strict which is required
869 // for accessibility standards please note this cleaning does not strip unbalanced '>' for BC compatibility reasons.
870 do {
871 if (strpos($param, '</lang>') !== false) {
872 // Old and future mutilang syntax.
873 $param = strip_tags($param, '<lang>');
874 if (!preg_match_all('/<.*>/suU', $param, $matches)) {
875 break;
877 $open = false;
878 foreach ($matches[0] as $match) {
879 if ($match === '</lang>') {
880 if ($open) {
881 $open = false;
882 continue;
883 } else {
884 break 2;
887 if (!preg_match('/^<lang lang="[a-zA-Z0-9_-]+"\s*>$/u', $match)) {
888 break 2;
889 } else {
890 $open = true;
893 if ($open) {
894 break;
896 return $param;
898 } else if (strpos($param, '</span>') !== false) {
899 // Current problematic multilang syntax.
900 $param = strip_tags($param, '<span>');
901 if (!preg_match_all('/<.*>/suU', $param, $matches)) {
902 break;
904 $open = false;
905 foreach ($matches[0] as $match) {
906 if ($match === '</span>') {
907 if ($open) {
908 $open = false;
909 continue;
910 } else {
911 break 2;
914 if (!preg_match('/^<span(\s+lang="[a-zA-Z0-9_-]+"|\s+class="multilang"){2}\s*>$/u', $match)) {
915 break 2;
916 } else {
917 $open = true;
920 if ($open) {
921 break;
923 return $param;
925 } while (false);
926 // Easy, just strip all tags, if we ever want to fix orphaned '&' we have to do that in format_string().
927 return strip_tags($param);
929 case PARAM_COMPONENT:
930 // We do not want any guessing here, either the name is correct or not
931 // please note only normalised component names are accepted.
932 if (!preg_match('/^[a-z]+(_[a-z][a-z0-9_]*)?[a-z0-9]$/', $param)) {
933 return '';
935 if (strpos($param, '__') !== false) {
936 return '';
938 if (strpos($param, 'mod_') === 0) {
939 // Module names must not contain underscores because we need to differentiate them from invalid plugin types.
940 if (substr_count($param, '_') != 1) {
941 return '';
944 return $param;
946 case PARAM_PLUGIN:
947 case PARAM_AREA:
948 // We do not want any guessing here, either the name is correct or not.
949 if (!is_valid_plugin_name($param)) {
950 return '';
952 return $param;
954 case PARAM_SAFEDIR:
955 // Remove everything not a-zA-Z0-9_- .
956 return preg_replace('/[^a-zA-Z0-9_-]/i', '', $param);
958 case PARAM_SAFEPATH:
959 // Remove everything not a-zA-Z0-9/_- .
960 return preg_replace('/[^a-zA-Z0-9\/_-]/i', '', $param);
962 case PARAM_FILE:
963 // Strip all suspicious characters from filename.
964 $param = fix_utf8($param);
965 $param = preg_replace('~[[:cntrl:]]|[&<>"`\|\':\\\\/]~u', '', $param);
966 if ($param === '.' || $param === '..') {
967 $param = '';
969 return $param;
971 case PARAM_PATH:
972 // Strip all suspicious characters from file path.
973 $param = fix_utf8($param);
974 $param = str_replace('\\', '/', $param);
976 // Explode the path and clean each element using the PARAM_FILE rules.
977 $breadcrumb = explode('/', $param);
978 foreach ($breadcrumb as $key => $crumb) {
979 if ($crumb === '.' && $key === 0) {
980 // Special condition to allow for relative current path such as ./currentdirfile.txt.
981 } else {
982 $crumb = clean_param($crumb, PARAM_FILE);
984 $breadcrumb[$key] = $crumb;
986 $param = implode('/', $breadcrumb);
988 // Remove multiple current path (./././) and multiple slashes (///).
989 $param = preg_replace('~//+~', '/', $param);
990 $param = preg_replace('~/(\./)+~', '/', $param);
991 return $param;
993 case PARAM_HOST:
994 // Allow FQDN or IPv4 dotted quad.
995 $param = preg_replace('/[^\.\d\w-]/', '', $param );
996 // Match ipv4 dotted quad.
997 if (preg_match('/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/', $param, $match)) {
998 // Confirm values are ok.
999 if ( $match[0] > 255
1000 || $match[1] > 255
1001 || $match[3] > 255
1002 || $match[4] > 255 ) {
1003 // Hmmm, what kind of dotted quad is this?
1004 $param = '';
1006 } else if ( preg_match('/^[\w\d\.-]+$/', $param) // Dots, hyphens, numbers.
1007 && !preg_match('/^[\.-]/', $param) // No leading dots/hyphens.
1008 && !preg_match('/[\.-]$/', $param) // No trailing dots/hyphens.
1010 // All is ok - $param is respected.
1011 } else {
1012 // All is not ok...
1013 $param='';
1015 return $param;
1017 case PARAM_URL: // Allow safe ftp, http, mailto urls.
1018 $param = fix_utf8($param);
1019 include_once($CFG->dirroot . '/lib/validateurlsyntax.php');
1020 if (!empty($param) && validateUrlSyntax($param, 's?H?S?F?E?u-P-a?I?p?f?q?r?')) {
1021 // All is ok, param is respected.
1022 } else {
1023 // Not really ok.
1024 $param ='';
1026 return $param;
1028 case PARAM_LOCALURL:
1029 // Allow http absolute, root relative and relative URLs within wwwroot.
1030 $param = clean_param($param, PARAM_URL);
1031 if (!empty($param)) {
1032 if (preg_match(':^/:', $param)) {
1033 // Root-relative, ok!
1034 } else if (preg_match('/^'.preg_quote($CFG->wwwroot, '/').'/i', $param)) {
1035 // Absolute, and matches our wwwroot.
1036 } else {
1037 // Relative - let's make sure there are no tricks.
1038 if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?')) {
1039 // Looks ok.
1040 } else {
1041 $param = '';
1045 return $param;
1047 case PARAM_PEM:
1048 $param = trim($param);
1049 // PEM formatted strings may contain letters/numbers and the symbols:
1050 // forward slash: /
1051 // plus sign: +
1052 // equal sign: =
1053 // , surrounded by BEGIN and END CERTIFICATE prefix and suffixes.
1054 if (preg_match('/^-----BEGIN CERTIFICATE-----([\s\w\/\+=]+)-----END CERTIFICATE-----$/', trim($param), $matches)) {
1055 list($wholething, $body) = $matches;
1056 unset($wholething, $matches);
1057 $b64 = clean_param($body, PARAM_BASE64);
1058 if (!empty($b64)) {
1059 return "-----BEGIN CERTIFICATE-----\n$b64\n-----END CERTIFICATE-----\n";
1060 } else {
1061 return '';
1064 return '';
1066 case PARAM_BASE64:
1067 if (!empty($param)) {
1068 // PEM formatted strings may contain letters/numbers and the symbols
1069 // forward slash: /
1070 // plus sign: +
1071 // equal sign: =.
1072 if (0 >= preg_match('/^([\s\w\/\+=]+)$/', trim($param))) {
1073 return '';
1075 $lines = preg_split('/[\s]+/', $param, -1, PREG_SPLIT_NO_EMPTY);
1076 // Each line of base64 encoded data must be 64 characters in length, except for the last line which may be less
1077 // than (or equal to) 64 characters long.
1078 for ($i=0, $j=count($lines); $i < $j; $i++) {
1079 if ($i + 1 == $j) {
1080 if (64 < strlen($lines[$i])) {
1081 return '';
1083 continue;
1086 if (64 != strlen($lines[$i])) {
1087 return '';
1090 return implode("\n", $lines);
1091 } else {
1092 return '';
1095 case PARAM_TAG:
1096 $param = fix_utf8($param);
1097 // Please note it is not safe to use the tag name directly anywhere,
1098 // it must be processed with s(), urlencode() before embedding anywhere.
1099 // Remove some nasties.
1100 $param = preg_replace('~[[:cntrl:]]|[<>`]~u', '', $param);
1101 // Convert many whitespace chars into one.
1102 $param = preg_replace('/\s+/', ' ', $param);
1103 $param = core_text::substr(trim($param), 0, TAG_MAX_LENGTH);
1104 return $param;
1106 case PARAM_TAGLIST:
1107 $param = fix_utf8($param);
1108 $tags = explode(',', $param);
1109 $result = array();
1110 foreach ($tags as $tag) {
1111 $res = clean_param($tag, PARAM_TAG);
1112 if ($res !== '') {
1113 $result[] = $res;
1116 if ($result) {
1117 return implode(',', $result);
1118 } else {
1119 return '';
1122 case PARAM_CAPABILITY:
1123 if (get_capability_info($param)) {
1124 return $param;
1125 } else {
1126 return '';
1129 case PARAM_PERMISSION:
1130 $param = (int)$param;
1131 if (in_array($param, array(CAP_INHERIT, CAP_ALLOW, CAP_PREVENT, CAP_PROHIBIT))) {
1132 return $param;
1133 } else {
1134 return CAP_INHERIT;
1137 case PARAM_AUTH:
1138 $param = clean_param($param, PARAM_PLUGIN);
1139 if (empty($param)) {
1140 return '';
1141 } else if (exists_auth_plugin($param)) {
1142 return $param;
1143 } else {
1144 return '';
1147 case PARAM_LANG:
1148 $param = clean_param($param, PARAM_SAFEDIR);
1149 if (get_string_manager()->translation_exists($param)) {
1150 return $param;
1151 } else {
1152 // Specified language is not installed or param malformed.
1153 return '';
1156 case PARAM_THEME:
1157 $param = clean_param($param, PARAM_PLUGIN);
1158 if (empty($param)) {
1159 return '';
1160 } else if (file_exists("$CFG->dirroot/theme/$param/config.php")) {
1161 return $param;
1162 } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$param/config.php")) {
1163 return $param;
1164 } else {
1165 // Specified theme is not installed.
1166 return '';
1169 case PARAM_USERNAME:
1170 $param = fix_utf8($param);
1171 $param = str_replace(" " , "", $param);
1172 // Convert uppercase to lowercase MDL-16919.
1173 $param = core_text::strtolower($param);
1174 if (empty($CFG->extendedusernamechars)) {
1175 // Regular expression, eliminate all chars EXCEPT:
1176 // alphanum, dash (-), underscore (_), at sign (@) and period (.) characters.
1177 $param = preg_replace('/[^-\.@_a-z0-9]/', '', $param);
1179 return $param;
1181 case PARAM_EMAIL:
1182 $param = fix_utf8($param);
1183 if (validate_email($param)) {
1184 return $param;
1185 } else {
1186 return '';
1189 case PARAM_STRINGID:
1190 if (preg_match('|^[a-zA-Z][a-zA-Z0-9\.:/_-]*$|', $param)) {
1191 return $param;
1192 } else {
1193 return '';
1196 case PARAM_TIMEZONE:
1197 // Can be int, float(with .5 or .0) or string seperated by '/' and can have '-_'.
1198 $param = fix_utf8($param);
1199 $timezonepattern = '/^(([+-]?(0?[0-9](\.[5|0])?|1[0-3](\.0)?|1[0-2]\.5))|(99)|[[:alnum:]]+(\/?[[:alpha:]_-])+)$/';
1200 if (preg_match($timezonepattern, $param)) {
1201 return $param;
1202 } else {
1203 return '';
1206 default:
1207 // Doh! throw error, switched parameters in optional_param or another serious problem.
1208 print_error("unknownparamtype", '', '', $type);
1213 * Makes sure the data is using valid utf8, invalid characters are discarded.
1215 * Note: this function is not intended for full objects with methods and private properties.
1217 * @param mixed $value
1218 * @return mixed with proper utf-8 encoding
1220 function fix_utf8($value) {
1221 if (is_null($value) or $value === '') {
1222 return $value;
1224 } else if (is_string($value)) {
1225 if ((string)(int)$value === $value) {
1226 // Shortcut.
1227 return $value;
1229 // No null bytes expected in our data, so let's remove it.
1230 $value = str_replace("\0", '', $value);
1232 // Lower error reporting because glibc throws bogus notices.
1233 $olderror = error_reporting();
1234 if ($olderror & E_NOTICE) {
1235 error_reporting($olderror ^ E_NOTICE);
1238 // Note: this duplicates min_fix_utf8() intentionally.
1239 static $buggyiconv = null;
1240 if ($buggyiconv === null) {
1241 $buggyiconv = (!function_exists('iconv') or iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€');
1244 if ($buggyiconv) {
1245 if (function_exists('mb_convert_encoding')) {
1246 $subst = mb_substitute_character();
1247 mb_substitute_character('');
1248 $result = mb_convert_encoding($value, 'utf-8', 'utf-8');
1249 mb_substitute_character($subst);
1251 } else {
1252 // Warn admins on admin/index.php page.
1253 $result = $value;
1256 } else {
1257 $result = iconv('UTF-8', 'UTF-8//IGNORE', $value);
1260 if ($olderror & E_NOTICE) {
1261 error_reporting($olderror);
1264 return $result;
1266 } else if (is_array($value)) {
1267 foreach ($value as $k => $v) {
1268 $value[$k] = fix_utf8($v);
1270 return $value;
1272 } else if (is_object($value)) {
1273 // Do not modify original.
1274 $value = clone($value);
1275 foreach ($value as $k => $v) {
1276 $value->$k = fix_utf8($v);
1278 return $value;
1280 } else {
1281 // This is some other type, no utf-8 here.
1282 return $value;
1287 * Return true if given value is integer or string with integer value
1289 * @param mixed $value String or Int
1290 * @return bool true if number, false if not
1292 function is_number($value) {
1293 if (is_int($value)) {
1294 return true;
1295 } else if (is_string($value)) {
1296 return ((string)(int)$value) === $value;
1297 } else {
1298 return false;
1303 * Returns host part from url.
1305 * @param string $url full url
1306 * @return string host, null if not found
1308 function get_host_from_url($url) {
1309 preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches);
1310 if ($matches) {
1311 return $matches[1];
1313 return null;
1317 * Tests whether anything was returned by text editor
1319 * This function is useful for testing whether something you got back from
1320 * the HTML editor actually contains anything. Sometimes the HTML editor
1321 * appear to be empty, but actually you get back a <br> tag or something.
1323 * @param string $string a string containing HTML.
1324 * @return boolean does the string contain any actual content - that is text,
1325 * images, objects, etc.
1327 function html_is_blank($string) {
1328 return trim(strip_tags($string, '<img><object><applet><input><select><textarea><hr>')) == '';
1332 * Set a key in global configuration
1334 * Set a key/value pair in both this session's {@link $CFG} global variable
1335 * and in the 'config' database table for future sessions.
1337 * Can also be used to update keys for plugin-scoped configs in config_plugin table.
1338 * In that case it doesn't affect $CFG.
1340 * A NULL value will delete the entry.
1342 * @param string $name the key to set
1343 * @param string $value the value to set (without magic quotes)
1344 * @param string $plugin (optional) the plugin scope, default null
1345 * @return bool true or exception
1347 function set_config($name, $value, $plugin=null) {
1348 global $CFG, $DB;
1350 if (empty($plugin)) {
1351 if (!array_key_exists($name, $CFG->config_php_settings)) {
1352 // So it's defined for this invocation at least.
1353 if (is_null($value)) {
1354 unset($CFG->$name);
1355 } else {
1356 // Settings from db are always strings.
1357 $CFG->$name = (string)$value;
1361 if ($DB->get_field('config', 'name', array('name' => $name))) {
1362 if ($value === null) {
1363 $DB->delete_records('config', array('name' => $name));
1364 } else {
1365 $DB->set_field('config', 'value', $value, array('name' => $name));
1367 } else {
1368 if ($value !== null) {
1369 $config = new stdClass();
1370 $config->name = $name;
1371 $config->value = $value;
1372 $DB->insert_record('config', $config, false);
1375 if ($name === 'siteidentifier') {
1376 cache_helper::update_site_identifier($value);
1378 cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1379 } else {
1380 // Plugin scope.
1381 if ($id = $DB->get_field('config_plugins', 'id', array('name' => $name, 'plugin' => $plugin))) {
1382 if ($value===null) {
1383 $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
1384 } else {
1385 $DB->set_field('config_plugins', 'value', $value, array('id' => $id));
1387 } else {
1388 if ($value !== null) {
1389 $config = new stdClass();
1390 $config->plugin = $plugin;
1391 $config->name = $name;
1392 $config->value = $value;
1393 $DB->insert_record('config_plugins', $config, false);
1396 cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1399 return true;
1403 * Get configuration values from the global config table
1404 * or the config_plugins table.
1406 * If called with one parameter, it will load all the config
1407 * variables for one plugin, and return them as an object.
1409 * If called with 2 parameters it will return a string single
1410 * value or false if the value is not found.
1412 * @static string|false $siteidentifier The site identifier is not cached. We use this static cache so
1413 * that we need only fetch it once per request.
1414 * @param string $plugin full component name
1415 * @param string $name default null
1416 * @return mixed hash-like object or single value, return false no config found
1417 * @throws dml_exception
1419 function get_config($plugin, $name = null) {
1420 global $CFG, $DB;
1422 static $siteidentifier = null;
1424 if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) {
1425 $forced =& $CFG->config_php_settings;
1426 $iscore = true;
1427 $plugin = 'core';
1428 } else {
1429 if (array_key_exists($plugin, $CFG->forced_plugin_settings)) {
1430 $forced =& $CFG->forced_plugin_settings[$plugin];
1431 } else {
1432 $forced = array();
1434 $iscore = false;
1437 if ($siteidentifier === null) {
1438 try {
1439 // This may fail during installation.
1440 // If you have a look at {@link initialise_cfg()} you will see that this is how we detect the need to
1441 // install the database.
1442 $siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
1443 } catch (dml_exception $ex) {
1444 // Set siteidentifier to false. We don't want to trip this continually.
1445 $siteidentifier = false;
1446 throw $ex;
1450 if (!empty($name)) {
1451 if (array_key_exists($name, $forced)) {
1452 return (string)$forced[$name];
1453 } else if ($name === 'siteidentifier' && $plugin == 'core') {
1454 return $siteidentifier;
1458 $cache = cache::make('core', 'config');
1459 $result = $cache->get($plugin);
1460 if ($result === false) {
1461 // The user is after a recordset.
1462 if (!$iscore) {
1463 $result = $DB->get_records_menu('config_plugins', array('plugin' => $plugin), '', 'name,value');
1464 } else {
1465 // This part is not really used any more, but anyway...
1466 $result = $DB->get_records_menu('config', array(), '', 'name,value');;
1468 $cache->set($plugin, $result);
1471 if (!empty($name)) {
1472 if (array_key_exists($name, $result)) {
1473 return $result[$name];
1475 return false;
1478 if ($plugin === 'core') {
1479 $result['siteidentifier'] = $siteidentifier;
1482 foreach ($forced as $key => $value) {
1483 if (is_null($value) or is_array($value) or is_object($value)) {
1484 // We do not want any extra mess here, just real settings that could be saved in db.
1485 unset($result[$key]);
1486 } else {
1487 // Convert to string as if it went through the DB.
1488 $result[$key] = (string)$value;
1492 return (object)$result;
1496 * Removes a key from global configuration.
1498 * @param string $name the key to set
1499 * @param string $plugin (optional) the plugin scope
1500 * @return boolean whether the operation succeeded.
1502 function unset_config($name, $plugin=null) {
1503 global $CFG, $DB;
1505 if (empty($plugin)) {
1506 unset($CFG->$name);
1507 $DB->delete_records('config', array('name' => $name));
1508 cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1509 } else {
1510 $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
1511 cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1514 return true;
1518 * Remove all the config variables for a given plugin.
1520 * NOTE: this function is called from lib/db/upgrade.php
1522 * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice';
1523 * @return boolean whether the operation succeeded.
1525 function unset_all_config_for_plugin($plugin) {
1526 global $DB;
1527 // Delete from the obvious config_plugins first.
1528 $DB->delete_records('config_plugins', array('plugin' => $plugin));
1529 // Next delete any suspect settings from config.
1530 $like = $DB->sql_like('name', '?', true, true, false, '|');
1531 $params = array($DB->sql_like_escape($plugin.'_', '|') . '%');
1532 $DB->delete_records_select('config', $like, $params);
1533 // Finally clear both the plugin cache and the core cache (suspect settings now removed from core).
1534 cache_helper::invalidate_by_definition('core', 'config', array(), array('core', $plugin));
1536 return true;
1540 * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability.
1542 * All users are verified if they still have the necessary capability.
1544 * @param string $value the value of the config setting.
1545 * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor.
1546 * @param bool $includeadmins include administrators.
1547 * @return array of user objects.
1549 function get_users_from_config($value, $capability, $includeadmins = true) {
1550 if (empty($value) or $value === '$@NONE@$') {
1551 return array();
1554 // We have to make sure that users still have the necessary capability,
1555 // it should be faster to fetch them all first and then test if they are present
1556 // instead of validating them one-by-one.
1557 $users = get_users_by_capability(context_system::instance(), $capability);
1558 if ($includeadmins) {
1559 $admins = get_admins();
1560 foreach ($admins as $admin) {
1561 $users[$admin->id] = $admin;
1565 if ($value === '$@ALL@$') {
1566 return $users;
1569 $result = array(); // Result in correct order.
1570 $allowed = explode(',', $value);
1571 foreach ($allowed as $uid) {
1572 if (isset($users[$uid])) {
1573 $user = $users[$uid];
1574 $result[$user->id] = $user;
1578 return $result;
1583 * Invalidates browser caches and cached data in temp.
1585 * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
1586 * {@link phpunit_util::reset_dataroot()}
1588 * @return void
1590 function purge_all_caches() {
1591 global $CFG, $DB;
1593 reset_text_filters_cache();
1594 js_reset_all_caches();
1595 theme_reset_all_caches();
1596 get_string_manager()->reset_caches();
1597 core_text::reset_caches();
1598 if (class_exists('plugin_manager')) {
1599 plugin_manager::reset_caches();
1602 // Bump up cacherev field for all courses.
1603 try {
1604 increment_revision_number('course', 'cacherev', '');
1605 } catch (moodle_exception $e) {
1606 // Ignore exception since this function is also called before upgrade script when field course.cacherev does not exist yet.
1609 $DB->reset_caches();
1610 cache_helper::purge_all();
1612 // Purge all other caches: rss, simplepie, etc.
1613 remove_dir($CFG->cachedir.'', true);
1615 // Make sure cache dir is writable, throws exception if not.
1616 make_cache_directory('');
1618 // This is the only place where we purge local caches, we are only adding files there.
1619 // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes.
1620 remove_dir($CFG->localcachedir, true);
1621 set_config('localcachedirpurged', time());
1622 make_localcache_directory('', true);
1626 * Get volatile flags
1628 * @param string $type
1629 * @param int $changedsince default null
1630 * @return array records array
1632 function get_cache_flags($type, $changedsince = null) {
1633 global $DB;
1635 $params = array('type' => $type, 'expiry' => time());
1636 $sqlwhere = "flagtype = :type AND expiry >= :expiry";
1637 if ($changedsince !== null) {
1638 $params['changedsince'] = $changedsince;
1639 $sqlwhere .= " AND timemodified > :changedsince";
1641 $cf = array();
1642 if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) {
1643 foreach ($flags as $flag) {
1644 $cf[$flag->name] = $flag->value;
1647 return $cf;
1651 * Get volatile flags
1653 * @param string $type
1654 * @param string $name
1655 * @param int $changedsince default null
1656 * @return string|false The cache flag value or false
1658 function get_cache_flag($type, $name, $changedsince=null) {
1659 global $DB;
1661 $params = array('type' => $type, 'name' => $name, 'expiry' => time());
1663 $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry";
1664 if ($changedsince !== null) {
1665 $params['changedsince'] = $changedsince;
1666 $sqlwhere .= " AND timemodified > :changedsince";
1669 return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params);
1673 * Set a volatile flag
1675 * @param string $type the "type" namespace for the key
1676 * @param string $name the key to set
1677 * @param string $value the value to set (without magic quotes) - null will remove the flag
1678 * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs
1679 * @return bool Always returns true
1681 function set_cache_flag($type, $name, $value, $expiry = null) {
1682 global $DB;
1684 $timemodified = time();
1685 if ($expiry === null || $expiry < $timemodified) {
1686 $expiry = $timemodified + 24 * 60 * 60;
1687 } else {
1688 $expiry = (int)$expiry;
1691 if ($value === null) {
1692 unset_cache_flag($type, $name);
1693 return true;
1696 if ($f = $DB->get_record('cache_flags', array('name' => $name, 'flagtype' => $type), '*', IGNORE_MULTIPLE)) {
1697 // This is a potential problem in DEBUG_DEVELOPER.
1698 if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) {
1699 return true; // No need to update.
1701 $f->value = $value;
1702 $f->expiry = $expiry;
1703 $f->timemodified = $timemodified;
1704 $DB->update_record('cache_flags', $f);
1705 } else {
1706 $f = new stdClass();
1707 $f->flagtype = $type;
1708 $f->name = $name;
1709 $f->value = $value;
1710 $f->expiry = $expiry;
1711 $f->timemodified = $timemodified;
1712 $DB->insert_record('cache_flags', $f);
1714 return true;
1718 * Removes a single volatile flag
1720 * @param string $type the "type" namespace for the key
1721 * @param string $name the key to set
1722 * @return bool
1724 function unset_cache_flag($type, $name) {
1725 global $DB;
1726 $DB->delete_records('cache_flags', array('name' => $name, 'flagtype' => $type));
1727 return true;
1731 * Garbage-collect volatile flags
1733 * @return bool Always returns true
1735 function gc_cache_flags() {
1736 global $DB;
1737 $DB->delete_records_select('cache_flags', 'expiry < ?', array(time()));
1738 return true;
1741 // USER PREFERENCE API.
1744 * Refresh user preference cache. This is used most often for $USER
1745 * object that is stored in session, but it also helps with performance in cron script.
1747 * Preferences for each user are loaded on first use on every page, then again after the timeout expires.
1749 * @package core
1750 * @category preference
1751 * @access public
1752 * @param stdClass $user User object. Preferences are preloaded into 'preference' property
1753 * @param int $cachelifetime Cache life time on the current page (in seconds)
1754 * @throws coding_exception
1755 * @return null
1757 function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120) {
1758 global $DB;
1759 // Static cache, we need to check on each page load, not only every 2 minutes.
1760 static $loadedusers = array();
1762 if (!isset($user->id)) {
1763 throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field');
1766 if (empty($user->id) or isguestuser($user->id)) {
1767 // No permanent storage for not-logged-in users and guest.
1768 if (!isset($user->preference)) {
1769 $user->preference = array();
1771 return;
1774 $timenow = time();
1776 if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) {
1777 // Already loaded at least once on this page. Are we up to date?
1778 if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) {
1779 // No need to reload - we are on the same page and we loaded prefs just a moment ago.
1780 return;
1782 } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) {
1783 // No change since the lastcheck on this page.
1784 $user->preference['_lastloaded'] = $timenow;
1785 return;
1789 // OK, so we have to reload all preferences.
1790 $loadedusers[$user->id] = true;
1791 $user->preference = $DB->get_records_menu('user_preferences', array('userid' => $user->id), '', 'name,value'); // All values.
1792 $user->preference['_lastloaded'] = $timenow;
1796 * Called from set/unset_user_preferences, so that the prefs can be correctly reloaded in different sessions.
1798 * NOTE: internal function, do not call from other code.
1800 * @package core
1801 * @access private
1802 * @param integer $userid the user whose prefs were changed.
1804 function mark_user_preferences_changed($userid) {
1805 global $CFG;
1807 if (empty($userid) or isguestuser($userid)) {
1808 // No cache flags for guest and not-logged-in users.
1809 return;
1812 set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout);
1816 * Sets a preference for the specified user.
1818 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1820 * @package core
1821 * @category preference
1822 * @access public
1823 * @param string $name The key to set as preference for the specified user
1824 * @param string $value The value to set for the $name key in the specified user's
1825 * record, null means delete current value.
1826 * @param stdClass|int|null $user A moodle user object or id, null means current user
1827 * @throws coding_exception
1828 * @return bool Always true or exception
1830 function set_user_preference($name, $value, $user = null) {
1831 global $USER, $DB;
1833 if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1834 throw new coding_exception('Invalid preference name in set_user_preference() call');
1837 if (is_null($value)) {
1838 // Null means delete current.
1839 return unset_user_preference($name, $user);
1840 } else if (is_object($value)) {
1841 throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed');
1842 } else if (is_array($value)) {
1843 throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed');
1845 // Value column maximum length is 1333 characters.
1846 $value = (string)$value;
1847 if (core_text::strlen($value) > 1333) {
1848 throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column');
1851 if (is_null($user)) {
1852 $user = $USER;
1853 } else if (isset($user->id)) {
1854 // It is a valid object.
1855 } else if (is_numeric($user)) {
1856 $user = (object)array('id' => (int)$user);
1857 } else {
1858 throw new coding_exception('Invalid $user parameter in set_user_preference() call');
1861 check_user_preferences_loaded($user);
1863 if (empty($user->id) or isguestuser($user->id)) {
1864 // No permanent storage for not-logged-in users and guest.
1865 $user->preference[$name] = $value;
1866 return true;
1869 if ($preference = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => $name))) {
1870 if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) {
1871 // Preference already set to this value.
1872 return true;
1874 $DB->set_field('user_preferences', 'value', $value, array('id' => $preference->id));
1876 } else {
1877 $preference = new stdClass();
1878 $preference->userid = $user->id;
1879 $preference->name = $name;
1880 $preference->value = $value;
1881 $DB->insert_record('user_preferences', $preference);
1884 // Update value in cache.
1885 $user->preference[$name] = $value;
1887 // Set reload flag for other sessions.
1888 mark_user_preferences_changed($user->id);
1890 return true;
1894 * Sets a whole array of preferences for the current user
1896 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1898 * @package core
1899 * @category preference
1900 * @access public
1901 * @param array $prefarray An array of key/value pairs to be set
1902 * @param stdClass|int|null $user A moodle user object or id, null means current user
1903 * @return bool Always true or exception
1905 function set_user_preferences(array $prefarray, $user = null) {
1906 foreach ($prefarray as $name => $value) {
1907 set_user_preference($name, $value, $user);
1909 return true;
1913 * Unsets a preference completely by deleting it from the database
1915 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1917 * @package core
1918 * @category preference
1919 * @access public
1920 * @param string $name The key to unset as preference for the specified user
1921 * @param stdClass|int|null $user A moodle user object or id, null means current user
1922 * @throws coding_exception
1923 * @return bool Always true or exception
1925 function unset_user_preference($name, $user = null) {
1926 global $USER, $DB;
1928 if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1929 throw new coding_exception('Invalid preference name in unset_user_preference() call');
1932 if (is_null($user)) {
1933 $user = $USER;
1934 } else if (isset($user->id)) {
1935 // It is a valid object.
1936 } else if (is_numeric($user)) {
1937 $user = (object)array('id' => (int)$user);
1938 } else {
1939 throw new coding_exception('Invalid $user parameter in unset_user_preference() call');
1942 check_user_preferences_loaded($user);
1944 if (empty($user->id) or isguestuser($user->id)) {
1945 // No permanent storage for not-logged-in user and guest.
1946 unset($user->preference[$name]);
1947 return true;
1950 // Delete from DB.
1951 $DB->delete_records('user_preferences', array('userid' => $user->id, 'name' => $name));
1953 // Delete the preference from cache.
1954 unset($user->preference[$name]);
1956 // Set reload flag for other sessions.
1957 mark_user_preferences_changed($user->id);
1959 return true;
1963 * Used to fetch user preference(s)
1965 * If no arguments are supplied this function will return
1966 * all of the current user preferences as an array.
1968 * If a name is specified then this function
1969 * attempts to return that particular preference value. If
1970 * none is found, then the optional value $default is returned,
1971 * otherwise null.
1973 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1975 * @package core
1976 * @category preference
1977 * @access public
1978 * @param string $name Name of the key to use in finding a preference value
1979 * @param mixed|null $default Value to be returned if the $name key is not set in the user preferences
1980 * @param stdClass|int|null $user A moodle user object or id, null means current user
1981 * @throws coding_exception
1982 * @return string|mixed|null A string containing the value of a single preference. An
1983 * array with all of the preferences or null
1985 function get_user_preferences($name = null, $default = null, $user = null) {
1986 global $USER;
1988 if (is_null($name)) {
1989 // All prefs.
1990 } else if (is_numeric($name) or $name === '_lastloaded') {
1991 throw new coding_exception('Invalid preference name in get_user_preferences() call');
1994 if (is_null($user)) {
1995 $user = $USER;
1996 } else if (isset($user->id)) {
1997 // Is a valid object.
1998 } else if (is_numeric($user)) {
1999 $user = (object)array('id' => (int)$user);
2000 } else {
2001 throw new coding_exception('Invalid $user parameter in get_user_preferences() call');
2004 check_user_preferences_loaded($user);
2006 if (empty($name)) {
2007 // All values.
2008 return $user->preference;
2009 } else if (isset($user->preference[$name])) {
2010 // The single string value.
2011 return $user->preference[$name];
2012 } else {
2013 // Default value (null if not specified).
2014 return $default;
2018 // FUNCTIONS FOR HANDLING TIME.
2021 * Given date parts in user time produce a GMT timestamp.
2023 * @package core
2024 * @category time
2025 * @param int $year The year part to create timestamp of
2026 * @param int $month The month part to create timestamp of
2027 * @param int $day The day part to create timestamp of
2028 * @param int $hour The hour part to create timestamp of
2029 * @param int $minute The minute part to create timestamp of
2030 * @param int $second The second part to create timestamp of
2031 * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset.
2032 * if 99 then default user's timezone is used {@link http://docs.moodle.org/dev/Time_API#Timezone}
2033 * @param bool $applydst Toggle Daylight Saving Time, default true, will be
2034 * applied only if timezone is 99 or string.
2035 * @return int GMT timestamp
2037 function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) {
2039 // Save input timezone, required for dst offset check.
2040 $passedtimezone = $timezone;
2042 $timezone = get_user_timezone_offset($timezone);
2044 if (abs($timezone) > 13) {
2045 // Server time.
2046 $time = mktime((int)$hour, (int)$minute, (int)$second, (int)$month, (int)$day, (int)$year);
2047 } else {
2048 $time = gmmktime((int)$hour, (int)$minute, (int)$second, (int)$month, (int)$day, (int)$year);
2049 $time = usertime($time, $timezone);
2051 // Apply dst for string timezones or if 99 then try dst offset with user's default timezone.
2052 if ($applydst && ((99 == $passedtimezone) || !is_numeric($passedtimezone))) {
2053 $time -= dst_offset_on($time, $passedtimezone);
2057 return $time;
2062 * Format a date/time (seconds) as weeks, days, hours etc as needed
2064 * Given an amount of time in seconds, returns string
2065 * formatted nicely as weeks, days, hours etc as needed
2067 * @package core
2068 * @category time
2069 * @uses MINSECS
2070 * @uses HOURSECS
2071 * @uses DAYSECS
2072 * @uses YEARSECS
2073 * @param int $totalsecs Time in seconds
2074 * @param stdClass $str Should be a time object
2075 * @return string A nicely formatted date/time string
2077 function format_time($totalsecs, $str = null) {
2079 $totalsecs = abs($totalsecs);
2081 if (!$str) {
2082 // Create the str structure the slow way.
2083 $str = new stdClass();
2084 $str->day = get_string('day');
2085 $str->days = get_string('days');
2086 $str->hour = get_string('hour');
2087 $str->hours = get_string('hours');
2088 $str->min = get_string('min');
2089 $str->mins = get_string('mins');
2090 $str->sec = get_string('sec');
2091 $str->secs = get_string('secs');
2092 $str->year = get_string('year');
2093 $str->years = get_string('years');
2096 $years = floor($totalsecs/YEARSECS);
2097 $remainder = $totalsecs - ($years*YEARSECS);
2098 $days = floor($remainder/DAYSECS);
2099 $remainder = $totalsecs - ($days*DAYSECS);
2100 $hours = floor($remainder/HOURSECS);
2101 $remainder = $remainder - ($hours*HOURSECS);
2102 $mins = floor($remainder/MINSECS);
2103 $secs = $remainder - ($mins*MINSECS);
2105 $ss = ($secs == 1) ? $str->sec : $str->secs;
2106 $sm = ($mins == 1) ? $str->min : $str->mins;
2107 $sh = ($hours == 1) ? $str->hour : $str->hours;
2108 $sd = ($days == 1) ? $str->day : $str->days;
2109 $sy = ($years == 1) ? $str->year : $str->years;
2111 $oyears = '';
2112 $odays = '';
2113 $ohours = '';
2114 $omins = '';
2115 $osecs = '';
2117 if ($years) {
2118 $oyears = $years .' '. $sy;
2120 if ($days) {
2121 $odays = $days .' '. $sd;
2123 if ($hours) {
2124 $ohours = $hours .' '. $sh;
2126 if ($mins) {
2127 $omins = $mins .' '. $sm;
2129 if ($secs) {
2130 $osecs = $secs .' '. $ss;
2133 if ($years) {
2134 return trim($oyears .' '. $odays);
2136 if ($days) {
2137 return trim($odays .' '. $ohours);
2139 if ($hours) {
2140 return trim($ohours .' '. $omins);
2142 if ($mins) {
2143 return trim($omins .' '. $osecs);
2145 if ($secs) {
2146 return $osecs;
2148 return get_string('now');
2152 * Returns a formatted string that represents a date in user time.
2154 * @package core
2155 * @category time
2156 * @param int $date the timestamp in UTC, as obtained from the database.
2157 * @param string $format strftime format. You should probably get this using
2158 * get_string('strftime...', 'langconfig');
2159 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
2160 * not 99 then daylight saving will not be added.
2161 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2162 * @param bool $fixday If true (default) then the leading zero from %d is removed.
2163 * If false then the leading zero is maintained.
2164 * @param bool $fixhour If true (default) then the leading zero from %I is removed.
2165 * @return string the formatted date/time.
2167 function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
2168 $calendartype = \core_calendar\type_factory::get_calendar_instance();
2169 return $calendartype->timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour);
2173 * Returns a formatted date ensuring it is UTF-8.
2175 * If we are running under Windows convert to Windows encoding and then back to UTF-8
2176 * (because it's impossible to specify UTF-8 to fetch locale info in Win32).
2178 * This function does not do any calculation regarding the user preferences and should
2179 * therefore receive the final date timestamp, format and timezone. Timezone being only used
2180 * to differentiate the use of server time or not (strftime() against gmstrftime()).
2182 * @param int $date the timestamp.
2183 * @param string $format strftime format.
2184 * @param int|float $tz the numerical timezone, typically returned by {@link get_user_timezone_offset()}.
2185 * @return string the formatted date/time.
2186 * @since 2.3.3
2188 function date_format_string($date, $format, $tz = 99) {
2189 global $CFG;
2190 if (abs($tz) > 13) {
2191 if ($CFG->ostype == 'WINDOWS' and $localewincharset = get_string('localewincharset', 'langconfig')) {
2192 $format = core_text::convert($format, 'utf-8', $localewincharset);
2193 $datestring = strftime($format, $date);
2194 $datestring = core_text::convert($datestring, $localewincharset, 'utf-8');
2195 } else {
2196 $datestring = strftime($format, $date);
2198 } else {
2199 if ($CFG->ostype == 'WINDOWS' and $localewincharset = get_string('localewincharset', 'langconfig')) {
2200 $format = core_text::convert($format, 'utf-8', $localewincharset);
2201 $datestring = gmstrftime($format, $date);
2202 $datestring = core_text::convert($datestring, $localewincharset, 'utf-8');
2203 } else {
2204 $datestring = gmstrftime($format, $date);
2207 return $datestring;
2211 * Given a $time timestamp in GMT (seconds since epoch),
2212 * returns an array that represents the date in user time
2214 * @package core
2215 * @category time
2216 * @uses HOURSECS
2217 * @param int $time Timestamp in GMT
2218 * @param float|int|string $timezone offset's time with timezone, if float and not 99, then no
2219 * dst offset is applied {@link http://docs.moodle.org/dev/Time_API#Timezone}
2220 * @return array An array that represents the date in user time
2222 function usergetdate($time, $timezone=99) {
2224 // Save input timezone, required for dst offset check.
2225 $passedtimezone = $timezone;
2227 $timezone = get_user_timezone_offset($timezone);
2229 if (abs($timezone) > 13) {
2230 // Server time.
2231 return getdate($time);
2234 // Add daylight saving offset for string timezones only, as we can't get dst for
2235 // float values. if timezone is 99 (user default timezone), then try update dst.
2236 if ($passedtimezone == 99 || !is_numeric($passedtimezone)) {
2237 $time += dst_offset_on($time, $passedtimezone);
2240 $time += intval((float)$timezone * HOURSECS);
2242 $datestring = gmstrftime('%B_%A_%j_%Y_%m_%w_%d_%H_%M_%S', $time);
2244 // Be careful to ensure the returned array matches that produced by getdate() above.
2245 list(
2246 $getdate['month'],
2247 $getdate['weekday'],
2248 $getdate['yday'],
2249 $getdate['year'],
2250 $getdate['mon'],
2251 $getdate['wday'],
2252 $getdate['mday'],
2253 $getdate['hours'],
2254 $getdate['minutes'],
2255 $getdate['seconds']
2256 ) = explode('_', $datestring);
2258 // Set correct datatype to match with getdate().
2259 $getdate['seconds'] = (int)$getdate['seconds'];
2260 $getdate['yday'] = (int)$getdate['yday'] - 1; // The function gmstrftime returns 0 through 365.
2261 $getdate['year'] = (int)$getdate['year'];
2262 $getdate['mon'] = (int)$getdate['mon'];
2263 $getdate['wday'] = (int)$getdate['wday'];
2264 $getdate['mday'] = (int)$getdate['mday'];
2265 $getdate['hours'] = (int)$getdate['hours'];
2266 $getdate['minutes'] = (int)$getdate['minutes'];
2267 return $getdate;
2271 * Given a GMT timestamp (seconds since epoch), offsets it by
2272 * the timezone. eg 3pm in India is 3pm GMT - 7 * 3600 seconds
2274 * @package core
2275 * @category time
2276 * @uses HOURSECS
2277 * @param int $date Timestamp in GMT
2278 * @param float|int|string $timezone timezone to calculate GMT time offset before
2279 * calculating user time, 99 is default user timezone
2280 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2281 * @return int
2283 function usertime($date, $timezone=99) {
2285 $timezone = get_user_timezone_offset($timezone);
2287 if (abs($timezone) > 13) {
2288 return $date;
2290 return $date - (int)($timezone * HOURSECS);
2294 * Given a time, return the GMT timestamp of the most recent midnight
2295 * for the current user.
2297 * @package core
2298 * @category time
2299 * @param int $date Timestamp in GMT
2300 * @param float|int|string $timezone timezone to calculate GMT time offset before
2301 * calculating user midnight time, 99 is default user timezone
2302 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2303 * @return int Returns a GMT timestamp
2305 function usergetmidnight($date, $timezone=99) {
2307 $userdate = usergetdate($date, $timezone);
2309 // Time of midnight of this user's day, in GMT.
2310 return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone);
2315 * Returns a string that prints the user's timezone
2317 * @package core
2318 * @category time
2319 * @param float|int|string $timezone timezone to calculate GMT time offset before
2320 * calculating user timezone, 99 is default user timezone
2321 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2322 * @return string
2324 function usertimezone($timezone=99) {
2326 $tz = get_user_timezone($timezone);
2328 if (!is_float($tz)) {
2329 return $tz;
2332 if (abs($tz) > 13) {
2333 // Server time.
2334 return get_string('serverlocaltime');
2337 if ($tz == intval($tz)) {
2338 // Don't show .0 for whole hours.
2339 $tz = intval($tz);
2342 if ($tz == 0) {
2343 return 'UTC';
2344 } else if ($tz > 0) {
2345 return 'UTC+'.$tz;
2346 } else {
2347 return 'UTC'.$tz;
2353 * Returns a float which represents the user's timezone difference from GMT in hours
2354 * Checks various settings and picks the most dominant of those which have a value
2356 * @package core
2357 * @category time
2358 * @param float|int|string $tz timezone to calculate GMT time offset for user,
2359 * 99 is default user timezone
2360 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2361 * @return float
2363 function get_user_timezone_offset($tz = 99) {
2364 $tz = get_user_timezone($tz);
2366 if (is_float($tz)) {
2367 return $tz;
2368 } else {
2369 $tzrecord = get_timezone_record($tz);
2370 if (empty($tzrecord)) {
2371 return 99.0;
2373 return (float)$tzrecord->gmtoff / HOURMINS;
2378 * Returns an int which represents the systems's timezone difference from GMT in seconds
2380 * @package core
2381 * @category time
2382 * @param float|int|string $tz timezone for which offset is required.
2383 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2384 * @return int|bool if found, false is timezone 99 or error
2386 function get_timezone_offset($tz) {
2387 if ($tz == 99) {
2388 return false;
2391 if (is_numeric($tz)) {
2392 return intval($tz * 60*60);
2395 if (!$tzrecord = get_timezone_record($tz)) {
2396 return false;
2398 return intval($tzrecord->gmtoff * 60);
2402 * Returns a float or a string which denotes the user's timezone
2403 * 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)
2404 * means that for this timezone there are also DST rules to be taken into account
2405 * Checks various settings and picks the most dominant of those which have a value
2407 * @package core
2408 * @category time
2409 * @param float|int|string $tz timezone to calculate GMT time offset before
2410 * calculating user timezone, 99 is default user timezone
2411 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2412 * @return float|string
2414 function get_user_timezone($tz = 99) {
2415 global $USER, $CFG;
2417 $timezones = array(
2418 $tz,
2419 isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99,
2420 isset($USER->timezone) ? $USER->timezone : 99,
2421 isset($CFG->timezone) ? $CFG->timezone : 99,
2424 $tz = 99;
2426 // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array.
2427 while (((empty($tz) && !is_numeric($tz)) || $tz == 99) && $next = each($timezones)) {
2428 $tz = $next['value'];
2430 return is_numeric($tz) ? (float) $tz : $tz;
2434 * Returns cached timezone record for given $timezonename
2436 * @package core
2437 * @param string $timezonename name of the timezone
2438 * @return stdClass|bool timezonerecord or false
2440 function get_timezone_record($timezonename) {
2441 global $DB;
2442 static $cache = null;
2444 if ($cache === null) {
2445 $cache = array();
2448 if (isset($cache[$timezonename])) {
2449 return $cache[$timezonename];
2452 return $cache[$timezonename] = $DB->get_record_sql('SELECT * FROM {timezone}
2453 WHERE name = ? ORDER BY year DESC', array($timezonename), IGNORE_MULTIPLE);
2457 * Build and store the users Daylight Saving Time (DST) table
2459 * @package core
2460 * @param int $fromyear Start year for the table, defaults to 1971
2461 * @param int $toyear End year for the table, defaults to 2035
2462 * @param int|float|string $strtimezone timezone to check if dst should be applied.
2463 * @return bool
2465 function calculate_user_dst_table($fromyear = null, $toyear = null, $strtimezone = null) {
2466 global $SESSION, $DB;
2468 $usertz = get_user_timezone($strtimezone);
2470 if (is_float($usertz)) {
2471 // Trivial timezone, no DST.
2472 return false;
2475 if (!empty($SESSION->dst_offsettz) && $SESSION->dst_offsettz != $usertz) {
2476 // We have pre-calculated values, but the user's effective TZ has changed in the meantime, so reset.
2477 unset($SESSION->dst_offsets);
2478 unset($SESSION->dst_range);
2481 if (!empty($SESSION->dst_offsets) && empty($fromyear) && empty($toyear)) {
2482 // Repeat calls which do not request specific year ranges stop here, we have already calculated the table.
2483 // This will be the return path most of the time, pretty light computationally.
2484 return true;
2487 // Reaching here means we either need to extend our table or create it from scratch.
2489 // Remember which TZ we calculated these changes for.
2490 $SESSION->dst_offsettz = $usertz;
2492 if (empty($SESSION->dst_offsets)) {
2493 // If we 're creating from scratch, put the two guard elements in there.
2494 $SESSION->dst_offsets = array(1 => null, 0 => null);
2496 if (empty($SESSION->dst_range)) {
2497 // If creating from scratch.
2498 $from = max((empty($fromyear) ? intval(date('Y')) - 3 : $fromyear), 1971);
2499 $to = min((empty($toyear) ? intval(date('Y')) + 3 : $toyear), 2035);
2501 // Fill in the array with the extra years we need to process.
2502 $yearstoprocess = array();
2503 for ($i = $from; $i <= $to; ++$i) {
2504 $yearstoprocess[] = $i;
2507 // Take note of which years we have processed for future calls.
2508 $SESSION->dst_range = array($from, $to);
2509 } else {
2510 // If needing to extend the table, do the same.
2511 $yearstoprocess = array();
2513 $from = max((empty($fromyear) ? $SESSION->dst_range[0] : $fromyear), 1971);
2514 $to = min((empty($toyear) ? $SESSION->dst_range[1] : $toyear), 2035);
2516 if ($from < $SESSION->dst_range[0]) {
2517 // Take note of which years we need to process and then note that we have processed them for future calls.
2518 for ($i = $from; $i < $SESSION->dst_range[0]; ++$i) {
2519 $yearstoprocess[] = $i;
2521 $SESSION->dst_range[0] = $from;
2523 if ($to > $SESSION->dst_range[1]) {
2524 // Take note of which years we need to process and then note that we have processed them for future calls.
2525 for ($i = $SESSION->dst_range[1] + 1; $i <= $to; ++$i) {
2526 $yearstoprocess[] = $i;
2528 $SESSION->dst_range[1] = $to;
2532 if (empty($yearstoprocess)) {
2533 // This means that there was a call requesting a SMALLER range than we have already calculated.
2534 return true;
2537 // From now on, we know that the array has at least the two guard elements, and $yearstoprocess has the years we need
2538 // Also, the array is sorted in descending timestamp order!
2540 // Get DB data.
2542 static $presetscache = array();
2543 if (!isset($presetscache[$usertz])) {
2544 $presetscache[$usertz] = $DB->get_records('timezone', array('name' => $usertz),
2545 'year DESC', 'year, gmtoff, dstoff, dst_month, dst_startday, dst_weekday, dst_skipweeks, dst_time, std_month, '.
2546 'std_startday, std_weekday, std_skipweeks, std_time');
2548 if (empty($presetscache[$usertz])) {
2549 return false;
2552 // Remove ending guard (first element of the array).
2553 reset($SESSION->dst_offsets);
2554 unset($SESSION->dst_offsets[key($SESSION->dst_offsets)]);
2556 // Add all required change timestamps.
2557 foreach ($yearstoprocess as $y) {
2558 // Find the record which is in effect for the year $y.
2559 foreach ($presetscache[$usertz] as $year => $preset) {
2560 if ($year <= $y) {
2561 break;
2565 $changes = dst_changes_for_year($y, $preset);
2567 if ($changes === null) {
2568 continue;
2570 if ($changes['dst'] != 0) {
2571 $SESSION->dst_offsets[$changes['dst']] = $preset->dstoff * MINSECS;
2573 if ($changes['std'] != 0) {
2574 $SESSION->dst_offsets[$changes['std']] = 0;
2578 // Put in a guard element at the top.
2579 $maxtimestamp = max(array_keys($SESSION->dst_offsets));
2580 $SESSION->dst_offsets[($maxtimestamp + DAYSECS)] = null; // DAYSECS is arbitrary, any "small" number will do.
2582 // Sort again.
2583 krsort($SESSION->dst_offsets);
2585 return true;
2589 * Calculates the required DST change and returns a Timestamp Array
2591 * @package core
2592 * @category time
2593 * @uses HOURSECS
2594 * @uses MINSECS
2595 * @param int|string $year Int or String Year to focus on
2596 * @param object $timezone Instatiated Timezone object
2597 * @return array|null Array dst => xx, 0 => xx, std => yy, 1 => yy or null
2599 function dst_changes_for_year($year, $timezone) {
2601 if ($timezone->dst_startday == 0 && $timezone->dst_weekday == 0 &&
2602 $timezone->std_startday == 0 && $timezone->std_weekday == 0) {
2603 return null;
2606 $monthdaydst = find_day_in_month($timezone->dst_startday, $timezone->dst_weekday, $timezone->dst_month, $year);
2607 $monthdaystd = find_day_in_month($timezone->std_startday, $timezone->std_weekday, $timezone->std_month, $year);
2609 list($dsthour, $dstmin) = explode(':', $timezone->dst_time);
2610 list($stdhour, $stdmin) = explode(':', $timezone->std_time);
2612 $timedst = make_timestamp($year, $timezone->dst_month, $monthdaydst, 0, 0, 0, 99, false);
2613 $timestd = make_timestamp($year, $timezone->std_month, $monthdaystd, 0, 0, 0, 99, false);
2615 // Instead of putting hour and minute in make_timestamp(), we add them afterwards.
2616 // This has the advantage of being able to have negative values for hour, i.e. for timezones
2617 // where GMT time would be in the PREVIOUS day than the local one on which DST changes.
2619 $timedst += $dsthour * HOURSECS + $dstmin * MINSECS;
2620 $timestd += $stdhour * HOURSECS + $stdmin * MINSECS;
2622 return array('dst' => $timedst, 0 => $timedst, 'std' => $timestd, 1 => $timestd);
2626 * Calculates the Daylight Saving Offset for a given date/time (timestamp)
2627 * - Note: Daylight saving only works for string timezones and not for float.
2629 * @package core
2630 * @category time
2631 * @param int $time must NOT be compensated at all, it has to be a pure timestamp
2632 * @param int|float|string $strtimezone timezone for which offset is expected, if 99 or null
2633 * then user's default timezone is used. {@link http://docs.moodle.org/dev/Time_API#Timezone}
2634 * @return int
2636 function dst_offset_on($time, $strtimezone = null) {
2637 global $SESSION;
2639 if (!calculate_user_dst_table(null, null, $strtimezone) || empty($SESSION->dst_offsets)) {
2640 return 0;
2643 reset($SESSION->dst_offsets);
2644 while (list($from, $offset) = each($SESSION->dst_offsets)) {
2645 if ($from <= $time) {
2646 break;
2650 // This is the normal return path.
2651 if ($offset !== null) {
2652 return $offset;
2655 // Reaching this point means we haven't calculated far enough, do it now:
2656 // Calculate extra DST changes if needed and recurse. The recursion always
2657 // moves toward the stopping condition, so will always end.
2659 if ($from == 0) {
2660 // We need a year smaller than $SESSION->dst_range[0].
2661 if ($SESSION->dst_range[0] == 1971) {
2662 return 0;
2664 calculate_user_dst_table($SESSION->dst_range[0] - 5, null, $strtimezone);
2665 return dst_offset_on($time, $strtimezone);
2666 } else {
2667 // We need a year larger than $SESSION->dst_range[1].
2668 if ($SESSION->dst_range[1] == 2035) {
2669 return 0;
2671 calculate_user_dst_table(null, $SESSION->dst_range[1] + 5, $strtimezone);
2672 return dst_offset_on($time, $strtimezone);
2677 * Calculates when the day appears in specific month
2679 * @package core
2680 * @category time
2681 * @param int $startday starting day of the month
2682 * @param int $weekday The day when week starts (normally taken from user preferences)
2683 * @param int $month The month whose day is sought
2684 * @param int $year The year of the month whose day is sought
2685 * @return int
2687 function find_day_in_month($startday, $weekday, $month, $year) {
2689 $daysinmonth = days_in_month($month, $year);
2691 if ($weekday == -1) {
2692 // Don't care about weekday, so return:
2693 // abs($startday) if $startday != -1
2694 // $daysinmonth otherwise.
2695 return ($startday == -1) ? $daysinmonth : abs($startday);
2698 // From now on we 're looking for a specific weekday.
2700 // Give "end of month" its actual value, since we know it.
2701 if ($startday == -1) {
2702 $startday = -1 * $daysinmonth;
2705 // Starting from day $startday, the sign is the direction.
2707 if ($startday < 1) {
2709 $startday = abs($startday);
2710 $lastmonthweekday = strftime('%w', mktime(12, 0, 0, $month, $daysinmonth, $year));
2712 // This is the last such weekday of the month.
2713 $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday;
2714 if ($lastinmonth > $daysinmonth) {
2715 $lastinmonth -= 7;
2718 // Find the first such weekday <= $startday.
2719 while ($lastinmonth > $startday) {
2720 $lastinmonth -= 7;
2723 return $lastinmonth;
2725 } else {
2727 $indexweekday = strftime('%w', mktime(12, 0, 0, $month, $startday, $year));
2729 $diff = $weekday - $indexweekday;
2730 if ($diff < 0) {
2731 $diff += 7;
2734 // This is the first such weekday of the month equal to or after $startday.
2735 $firstfromindex = $startday + $diff;
2737 return $firstfromindex;
2743 * Calculate the number of days in a given month
2745 * @package core
2746 * @category time
2747 * @param int $month The month whose day count is sought
2748 * @param int $year The year of the month whose day count is sought
2749 * @return int
2751 function days_in_month($month, $year) {
2752 return intval(date('t', mktime(12, 0, 0, $month, 1, $year)));
2756 * Calculate the position in the week of a specific calendar day
2758 * @package core
2759 * @category time
2760 * @param int $day The day of the date whose position in the week is sought
2761 * @param int $month The month of the date whose position in the week is sought
2762 * @param int $year The year of the date whose position in the week is sought
2763 * @return int
2765 function dayofweek($day, $month, $year) {
2766 // I wonder if this is any different from strftime('%w', mktime(12, 0, 0, $month, $daysinmonth, $year, 0));.
2767 return intval(date('w', mktime(12, 0, 0, $month, $day, $year)));
2770 // USER AUTHENTICATION AND LOGIN.
2773 * Returns full login url.
2775 * @return string login url
2777 function get_login_url() {
2778 global $CFG;
2780 $url = "$CFG->wwwroot/login/index.php";
2782 if (!empty($CFG->loginhttps)) {
2783 $url = str_replace('http:', 'https:', $url);
2786 return $url;
2790 * This function checks that the current user is logged in and has the
2791 * required privileges
2793 * This function checks that the current user is logged in, and optionally
2794 * whether they are allowed to be in a particular course and view a particular
2795 * course module.
2796 * If they are not logged in, then it redirects them to the site login unless
2797 * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which
2798 * case they are automatically logged in as guests.
2799 * If $courseid is given and the user is not enrolled in that course then the
2800 * user is redirected to the course enrolment page.
2801 * If $cm is given and the course module is hidden and the user is not a teacher
2802 * in the course then the user is redirected to the course home page.
2804 * When $cm parameter specified, this function sets page layout to 'module'.
2805 * You need to change it manually later if some other layout needed.
2807 * @package core_access
2808 * @category access
2810 * @param mixed $courseorid id of the course or course object
2811 * @param bool $autologinguest default true
2812 * @param object $cm course module object
2813 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2814 * true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2815 * in order to keep redirects working properly. MDL-14495
2816 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2817 * @return mixed Void, exit, and die depending on path
2818 * @throws coding_exception
2819 * @throws require_login_exception
2821 function require_login($courseorid = null, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
2822 global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT;
2824 // Must not redirect when byteserving already started.
2825 if (!empty($_SERVER['HTTP_RANGE'])) {
2826 $preventredirect = true;
2829 // Setup global $COURSE, themes, language and locale.
2830 if (!empty($courseorid)) {
2831 if (is_object($courseorid)) {
2832 $course = $courseorid;
2833 } else if ($courseorid == SITEID) {
2834 $course = clone($SITE);
2835 } else {
2836 $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST);
2838 if ($cm) {
2839 if ($cm->course != $course->id) {
2840 throw new coding_exception('course and cm parameters in require_login() call do not match!!');
2842 // Make sure we have a $cm from get_fast_modinfo as this contains activity access details.
2843 if (!($cm instanceof cm_info)) {
2844 // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
2845 // db queries so this is not really a performance concern, however it is obviously
2846 // better if you use get_fast_modinfo to get the cm before calling this.
2847 $modinfo = get_fast_modinfo($course);
2848 $cm = $modinfo->get_cm($cm->id);
2850 $PAGE->set_cm($cm, $course); // Set's up global $COURSE.
2851 $PAGE->set_pagelayout('incourse');
2852 } else {
2853 $PAGE->set_course($course); // Set's up global $COURSE.
2855 } else {
2856 // Do not touch global $COURSE via $PAGE->set_course(),
2857 // the reasons is we need to be able to call require_login() at any time!!
2858 $course = $SITE;
2859 if ($cm) {
2860 throw new coding_exception('cm parameter in require_login() requires valid course parameter!');
2864 // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false.
2865 // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future
2866 // risk leading the user back to the AJAX request URL.
2867 if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) {
2868 $setwantsurltome = false;
2871 // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour.
2872 if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !$preventredirect && !empty($CFG->dbsessions)) {
2873 if ($setwantsurltome) {
2874 $SESSION->wantsurl = qualified_me();
2876 redirect(get_login_url());
2879 // If the user is not even logged in yet then make sure they are.
2880 if (!isloggedin()) {
2881 if ($autologinguest and !empty($CFG->guestloginbutton) and !empty($CFG->autologinguests)) {
2882 if (!$guest = get_complete_user_data('id', $CFG->siteguest)) {
2883 // Misconfigured site guest, just redirect to login page.
2884 redirect(get_login_url());
2885 exit; // Never reached.
2887 $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang;
2888 complete_user_login($guest);
2889 $USER->autologinguest = true;
2890 $SESSION->lang = $lang;
2891 } else {
2892 // NOTE: $USER->site check was obsoleted by session test cookie, $USER->confirmed test is in login/index.php.
2893 if ($preventredirect) {
2894 throw new require_login_exception('You are not logged in');
2897 if ($setwantsurltome) {
2898 $SESSION->wantsurl = qualified_me();
2900 if (!empty($_SERVER['HTTP_REFERER'])) {
2901 $SESSION->fromurl = $_SERVER['HTTP_REFERER'];
2903 redirect(get_login_url());
2904 exit; // Never reached.
2908 // Loginas as redirection if needed.
2909 if ($course->id != SITEID and \core\session\manager::is_loggedinas()) {
2910 if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) {
2911 if ($USER->loginascontext->instanceid != $course->id) {
2912 print_error('loginasonecourse', '', $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid);
2917 // Check whether the user should be changing password (but only if it is REALLY them).
2918 if (get_user_preferences('auth_forcepasswordchange') && !\core\session\manager::is_loggedinas()) {
2919 $userauth = get_auth_plugin($USER->auth);
2920 if ($userauth->can_change_password() and !$preventredirect) {
2921 if ($setwantsurltome) {
2922 $SESSION->wantsurl = qualified_me();
2924 if ($changeurl = $userauth->change_password_url()) {
2925 // Use plugin custom url.
2926 redirect($changeurl);
2927 } else {
2928 // Use moodle internal method.
2929 if (empty($CFG->loginhttps)) {
2930 redirect($CFG->wwwroot .'/login/change_password.php');
2931 } else {
2932 $wwwroot = str_replace('http:', 'https:', $CFG->wwwroot);
2933 redirect($wwwroot .'/login/change_password.php');
2936 } else {
2937 print_error('nopasswordchangeforced', 'auth');
2941 // Check that the user account is properly set up.
2942 if (user_not_fully_set_up($USER)) {
2943 if ($preventredirect) {
2944 throw new require_login_exception('User not fully set-up');
2946 if ($setwantsurltome) {
2947 $SESSION->wantsurl = qualified_me();
2949 redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&amp;course='. SITEID);
2952 // Make sure the USER has a sesskey set up. Used for CSRF protection.
2953 sesskey();
2955 // Do not bother admins with any formalities.
2956 if (is_siteadmin()) {
2957 // Set accesstime or the user will appear offline which messes up messaging.
2958 user_accesstime_log($course->id);
2959 return;
2962 // Check that the user has agreed to a site policy if there is one - do not test in case of admins.
2963 if (!$USER->policyagreed and !is_siteadmin()) {
2964 if (!empty($CFG->sitepolicy) and !isguestuser()) {
2965 if ($preventredirect) {
2966 throw new require_login_exception('Policy not agreed');
2968 if ($setwantsurltome) {
2969 $SESSION->wantsurl = qualified_me();
2971 redirect($CFG->wwwroot .'/user/policy.php');
2972 } else if (!empty($CFG->sitepolicyguest) and isguestuser()) {
2973 if ($preventredirect) {
2974 throw new require_login_exception('Policy not agreed');
2976 if ($setwantsurltome) {
2977 $SESSION->wantsurl = qualified_me();
2979 redirect($CFG->wwwroot .'/user/policy.php');
2983 // Fetch the system context, the course context, and prefetch its child contexts.
2984 $sysctx = context_system::instance();
2985 $coursecontext = context_course::instance($course->id, MUST_EXIST);
2986 if ($cm) {
2987 $cmcontext = context_module::instance($cm->id, MUST_EXIST);
2988 } else {
2989 $cmcontext = null;
2992 // If the site is currently under maintenance, then print a message.
2993 if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:config', $sysctx)) {
2994 if ($preventredirect) {
2995 throw new require_login_exception('Maintenance in progress');
2998 print_maintenance_message();
3001 // Make sure the course itself is not hidden.
3002 if ($course->id == SITEID) {
3003 // Frontpage can not be hidden.
3004 } else {
3005 if (is_role_switched($course->id)) {
3006 // When switching roles ignore the hidden flag - user had to be in course to do the switch.
3007 } else {
3008 if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
3009 // Originally there was also test of parent category visibility, BUT is was very slow in complex queries
3010 // involving "my courses" now it is also possible to simply hide all courses user is not enrolled in :-).
3011 if ($preventredirect) {
3012 throw new require_login_exception('Course is hidden');
3014 // We need to override the navigation URL as the course won't have been added to the navigation and thus
3015 // the navigation will mess up when trying to find it.
3016 navigation_node::override_active_url(new moodle_url('/'));
3017 notice(get_string('coursehidden'), $CFG->wwwroot .'/');
3022 // Is the user enrolled?
3023 if ($course->id == SITEID) {
3024 // Everybody is enrolled on the frontpage.
3025 } else {
3026 if (\core\session\manager::is_loggedinas()) {
3027 // Make sure the REAL person can access this course first.
3028 $realuser = \core\session\manager::get_realuser();
3029 if (!is_enrolled($coursecontext, $realuser->id, '', true) and
3030 !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) {
3031 if ($preventredirect) {
3032 throw new require_login_exception('Invalid course login-as access');
3034 echo $OUTPUT->header();
3035 notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/');
3039 $access = false;
3041 if (is_role_switched($course->id)) {
3042 // Ok, user had to be inside this course before the switch.
3043 $access = true;
3045 } else if (is_viewing($coursecontext, $USER)) {
3046 // Ok, no need to mess with enrol.
3047 $access = true;
3049 } else {
3050 if (isset($USER->enrol['enrolled'][$course->id])) {
3051 if ($USER->enrol['enrolled'][$course->id] > time()) {
3052 $access = true;
3053 if (isset($USER->enrol['tempguest'][$course->id])) {
3054 unset($USER->enrol['tempguest'][$course->id]);
3055 remove_temp_course_roles($coursecontext);
3057 } else {
3058 // Expired.
3059 unset($USER->enrol['enrolled'][$course->id]);
3062 if (isset($USER->enrol['tempguest'][$course->id])) {
3063 if ($USER->enrol['tempguest'][$course->id] == 0) {
3064 $access = true;
3065 } else if ($USER->enrol['tempguest'][$course->id] > time()) {
3066 $access = true;
3067 } else {
3068 // Expired.
3069 unset($USER->enrol['tempguest'][$course->id]);
3070 remove_temp_course_roles($coursecontext);
3074 if (!$access) {
3075 // Cache not ok.
3076 $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id);
3077 if ($until !== false) {
3078 // Active participants may always access, a timestamp in the future, 0 (always) or false.
3079 if ($until == 0) {
3080 $until = ENROL_MAX_TIMESTAMP;
3082 $USER->enrol['enrolled'][$course->id] = $until;
3083 $access = true;
3085 } else {
3086 $params = array('courseid' => $course->id, 'status' => ENROL_INSTANCE_ENABLED);
3087 $instances = $DB->get_records('enrol', $params, 'sortorder, id ASC');
3088 $enrols = enrol_get_plugins(true);
3089 // First ask all enabled enrol instances in course if they want to auto enrol user.
3090 foreach ($instances as $instance) {
3091 if (!isset($enrols[$instance->enrol])) {
3092 continue;
3094 // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false.
3095 $until = $enrols[$instance->enrol]->try_autoenrol($instance);
3096 if ($until !== false) {
3097 if ($until == 0) {
3098 $until = ENROL_MAX_TIMESTAMP;
3100 $USER->enrol['enrolled'][$course->id] = $until;
3101 $access = true;
3102 break;
3105 // If not enrolled yet try to gain temporary guest access.
3106 if (!$access) {
3107 foreach ($instances as $instance) {
3108 if (!isset($enrols[$instance->enrol])) {
3109 continue;
3111 // Get a duration for the guest access, a timestamp in the future or false.
3112 $until = $enrols[$instance->enrol]->try_guestaccess($instance);
3113 if ($until !== false and $until > time()) {
3114 $USER->enrol['tempguest'][$course->id] = $until;
3115 $access = true;
3116 break;
3124 if (!$access) {
3125 if ($preventredirect) {
3126 throw new require_login_exception('Not enrolled');
3128 if ($setwantsurltome) {
3129 $SESSION->wantsurl = qualified_me();
3131 redirect($CFG->wwwroot .'/enrol/index.php?id='. $course->id);
3135 // Check visibility of activity to current user; includes visible flag, groupmembersonly, conditional availability, etc.
3136 if ($cm && !$cm->uservisible) {
3137 if ($preventredirect) {
3138 throw new require_login_exception('Activity is hidden');
3140 if ($course->id != SITEID) {
3141 $url = new moodle_url('/course/view.php', array('id' => $course->id));
3142 } else {
3143 $url = new moodle_url('/');
3145 redirect($url, get_string('activityiscurrentlyhidden'));
3148 // Finally access granted, update lastaccess times.
3149 user_accesstime_log($course->id);
3154 * This function just makes sure a user is logged out.
3156 * @package core_access
3157 * @category access
3159 function require_logout() {
3160 global $USER, $DB;
3162 if (!isloggedin()) {
3163 // This should not happen often, no need for hooks or events here.
3164 \core\session\manager::terminate_current();
3165 return;
3168 // Execute hooks before action.
3169 $authsequence = get_enabled_auth_plugins();
3170 foreach ($authsequence as $authname) {
3171 $authplugin = get_auth_plugin($authname);
3172 $authplugin->prelogout_hook();
3175 // Store info that gets removed during logout.
3176 $sid = session_id();
3177 $event = \core\event\user_loggedout::create(
3178 array(
3179 'userid' => $USER->id,
3180 'objectid' => $USER->id,
3181 'other' => array('sessionid' => $sid),
3184 if ($session = $DB->get_record('sessions', array('sid'=>$sid))) {
3185 $event->add_record_snapshot('sessions', $session);
3188 // Delete session record and drop $_SESSION content.
3189 \core\session\manager::terminate_current();
3191 // Trigger event AFTER action.
3192 $event->trigger();
3196 * Weaker version of require_login()
3198 * This is a weaker version of {@link require_login()} which only requires login
3199 * when called from within a course rather than the site page, unless
3200 * the forcelogin option is turned on.
3201 * @see require_login()
3203 * @package core_access
3204 * @category access
3206 * @param mixed $courseorid The course object or id in question
3207 * @param bool $autologinguest Allow autologin guests if that is wanted
3208 * @param object $cm Course activity module if known
3209 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
3210 * true. Used to avoid (=false) some scripts (file.php...) to set that variable,
3211 * in order to keep redirects working properly. MDL-14495
3212 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
3213 * @return void
3214 * @throws coding_exception
3216 function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
3217 global $CFG, $PAGE, $SITE;
3218 $issite = (is_object($courseorid) and $courseorid->id == SITEID)
3219 or (!is_object($courseorid) and $courseorid == SITEID);
3220 if ($issite && !empty($cm) && !($cm instanceof cm_info)) {
3221 // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
3222 // db queries so this is not really a performance concern, however it is obviously
3223 // better if you use get_fast_modinfo to get the cm before calling this.
3224 if (is_object($courseorid)) {
3225 $course = $courseorid;
3226 } else {
3227 $course = clone($SITE);
3229 $modinfo = get_fast_modinfo($course);
3230 $cm = $modinfo->get_cm($cm->id);
3232 if (!empty($CFG->forcelogin)) {
3233 // Login required for both SITE and courses.
3234 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3236 } else if ($issite && !empty($cm) and !$cm->uservisible) {
3237 // Always login for hidden activities.
3238 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3240 } else if ($issite) {
3241 // Login for SITE not required.
3242 if ($cm and empty($cm->visible)) {
3243 // Hidden activities are not accessible without login.
3244 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3245 } else if ($cm and !empty($CFG->enablegroupmembersonly) and $cm->groupmembersonly) {
3246 // Not-logged-in users do not have any group membership.
3247 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3248 } else {
3249 // We still need to instatiate PAGE vars properly so that things that rely on it like navigation function correctly.
3250 if (!empty($courseorid)) {
3251 if (is_object($courseorid)) {
3252 $course = $courseorid;
3253 } else {
3254 $course = clone($SITE);
3256 if ($cm) {
3257 if ($cm->course != $course->id) {
3258 throw new coding_exception('course and cm parameters in require_course_login() call do not match!!');
3260 $PAGE->set_cm($cm, $course);
3261 $PAGE->set_pagelayout('incourse');
3262 } else {
3263 $PAGE->set_course($course);
3265 } else {
3266 // If $PAGE->course, and hence $PAGE->context, have not already been set up properly, set them up now.
3267 $PAGE->set_course($PAGE->course);
3269 // TODO: verify conditional activities here.
3270 user_accesstime_log(SITEID);
3271 return;
3274 } else {
3275 // Course login always required.
3276 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3281 * Require key login. Function terminates with error if key not found or incorrect.
3283 * @uses NO_MOODLE_COOKIES
3284 * @uses PARAM_ALPHANUM
3285 * @param string $script unique script identifier
3286 * @param int $instance optional instance id
3287 * @return int Instance ID
3289 function require_user_key_login($script, $instance=null) {
3290 global $DB;
3292 if (!NO_MOODLE_COOKIES) {
3293 print_error('sessioncookiesdisable');
3296 // Extra safety.
3297 \core\session\manager::write_close();
3299 $keyvalue = required_param('key', PARAM_ALPHANUM);
3301 if (!$key = $DB->get_record('user_private_key', array('script' => $script, 'value' => $keyvalue, 'instance' => $instance))) {
3302 print_error('invalidkey');
3305 if (!empty($key->validuntil) and $key->validuntil < time()) {
3306 print_error('expiredkey');
3309 if ($key->iprestriction) {
3310 $remoteaddr = getremoteaddr(null);
3311 if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) {
3312 print_error('ipmismatch');
3316 if (!$user = $DB->get_record('user', array('id' => $key->userid))) {
3317 print_error('invaliduserid');
3320 // Emulate normal session.
3321 enrol_check_plugins($user);
3322 \core\session\manager::set_user($user);
3324 // Note we are not using normal login.
3325 if (!defined('USER_KEY_LOGIN')) {
3326 define('USER_KEY_LOGIN', true);
3329 // Return instance id - it might be empty.
3330 return $key->instance;
3334 * Creates a new private user access key.
3336 * @param string $script unique target identifier
3337 * @param int $userid
3338 * @param int $instance optional instance id
3339 * @param string $iprestriction optional ip restricted access
3340 * @param timestamp $validuntil key valid only until given data
3341 * @return string access key value
3343 function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3344 global $DB;
3346 $key = new stdClass();
3347 $key->script = $script;
3348 $key->userid = $userid;
3349 $key->instance = $instance;
3350 $key->iprestriction = $iprestriction;
3351 $key->validuntil = $validuntil;
3352 $key->timecreated = time();
3354 // Something long and unique.
3355 $key->value = md5($userid.'_'.time().random_string(40));
3356 while ($DB->record_exists('user_private_key', array('value' => $key->value))) {
3357 // Must be unique.
3358 $key->value = md5($userid.'_'.time().random_string(40));
3360 $DB->insert_record('user_private_key', $key);
3361 return $key->value;
3365 * Delete the user's new private user access keys for a particular script.
3367 * @param string $script unique target identifier
3368 * @param int $userid
3369 * @return void
3371 function delete_user_key($script, $userid) {
3372 global $DB;
3373 $DB->delete_records('user_private_key', array('script' => $script, 'userid' => $userid));
3377 * Gets a private user access key (and creates one if one doesn't exist).
3379 * @param string $script unique target identifier
3380 * @param int $userid
3381 * @param int $instance optional instance id
3382 * @param string $iprestriction optional ip restricted access
3383 * @param timestamp $validuntil key valid only until given data
3384 * @return string access key value
3386 function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3387 global $DB;
3389 if ($key = $DB->get_record('user_private_key', array('script' => $script, 'userid' => $userid,
3390 'instance' => $instance, 'iprestriction' => $iprestriction,
3391 'validuntil' => $validuntil))) {
3392 return $key->value;
3393 } else {
3394 return create_user_key($script, $userid, $instance, $iprestriction, $validuntil);
3400 * Modify the user table by setting the currently logged in user's last login to now.
3402 * @return bool Always returns true
3404 function update_user_login_times() {
3405 global $USER, $DB, $CFG;
3407 require_once($CFG->dirroot.'/user/lib.php');
3409 if (isguestuser()) {
3410 // Do not update guest access times/ips for performance.
3411 return true;
3414 $now = time();
3416 $user = new stdClass();
3417 $user->id = $USER->id;
3419 // Make sure all users that logged in have some firstaccess.
3420 if ($USER->firstaccess == 0) {
3421 $USER->firstaccess = $user->firstaccess = $now;
3424 // Store the previous current as lastlogin.
3425 $USER->lastlogin = $user->lastlogin = $USER->currentlogin;
3427 $USER->currentlogin = $user->currentlogin = $now;
3429 // Function user_accesstime_log() may not update immediately, better do it here.
3430 $USER->lastaccess = $user->lastaccess = $now;
3431 $USER->lastip = $user->lastip = getremoteaddr();
3433 user_update_user($user, false);
3434 return true;
3438 * Determines if a user has completed setting up their account.
3440 * @param stdClass $user A {@link $USER} object to test for the existence of a valid name and email
3441 * @return bool
3443 function user_not_fully_set_up($user) {
3444 if (isguestuser($user)) {
3445 return false;
3447 return (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user));
3451 * Check whether the user has exceeded the bounce threshold
3453 * @param stdClass $user A {@link $USER} object
3454 * @return bool true => User has exceeded bounce threshold
3456 function over_bounce_threshold($user) {
3457 global $CFG, $DB;
3459 if (empty($CFG->handlebounces)) {
3460 return false;
3463 if (empty($user->id)) {
3464 // No real (DB) user, nothing to do here.
3465 return false;
3468 // Set sensible defaults.
3469 if (empty($CFG->minbounces)) {
3470 $CFG->minbounces = 10;
3472 if (empty($CFG->bounceratio)) {
3473 $CFG->bounceratio = .20;
3475 $bouncecount = 0;
3476 $sendcount = 0;
3477 if ($bounce = $DB->get_record('user_preferences', array ('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3478 $bouncecount = $bounce->value;
3480 if ($send = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3481 $sendcount = $send->value;
3483 return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio);
3487 * Used to increment or reset email sent count
3489 * @param stdClass $user object containing an id
3490 * @param bool $reset will reset the count to 0
3491 * @return void
3493 function set_send_count($user, $reset=false) {
3494 global $DB;
3496 if (empty($user->id)) {
3497 // No real (DB) user, nothing to do here.
3498 return;
3501 if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3502 $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3503 $DB->update_record('user_preferences', $pref);
3504 } else if (!empty($reset)) {
3505 // If it's not there and we're resetting, don't bother. Make a new one.
3506 $pref = new stdClass();
3507 $pref->name = 'email_send_count';
3508 $pref->value = 1;
3509 $pref->userid = $user->id;
3510 $DB->insert_record('user_preferences', $pref, false);
3515 * Increment or reset user's email bounce count
3517 * @param stdClass $user object containing an id
3518 * @param bool $reset will reset the count to 0
3520 function set_bounce_count($user, $reset=false) {
3521 global $DB;
3523 if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3524 $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3525 $DB->update_record('user_preferences', $pref);
3526 } else if (!empty($reset)) {
3527 // If it's not there and we're resetting, don't bother. Make a new one.
3528 $pref = new stdClass();
3529 $pref->name = 'email_bounce_count';
3530 $pref->value = 1;
3531 $pref->userid = $user->id;
3532 $DB->insert_record('user_preferences', $pref, false);
3537 * Determines if the logged in user is currently moving an activity
3539 * @param int $courseid The id of the course being tested
3540 * @return bool
3542 function ismoving($courseid) {
3543 global $USER;
3545 if (!empty($USER->activitycopy)) {
3546 return ($USER->activitycopycourse == $courseid);
3548 return false;
3552 * Returns a persons full name
3554 * Given an object containing all of the users name values, this function returns a string with the full name of the person.
3555 * The result may depend on system settings or language. 'override' will force both names to be used even if system settings
3556 * specify one.
3558 * @param stdClass $user A {@link $USER} object to get full name of.
3559 * @param bool $override If true then the name will be firstname followed by lastname rather than adhering to fullnamedisplay.
3560 * @return string
3562 function fullname($user, $override=false) {
3563 global $CFG, $SESSION;
3565 if (!isset($user->firstname) and !isset($user->lastname)) {
3566 return '';
3569 if (!$override) {
3570 if (!empty($CFG->forcefirstname)) {
3571 $user->firstname = $CFG->forcefirstname;
3573 if (!empty($CFG->forcelastname)) {
3574 $user->lastname = $CFG->forcelastname;
3578 if (!empty($SESSION->fullnamedisplay)) {
3579 $CFG->fullnamedisplay = $SESSION->fullnamedisplay;
3582 $template = null;
3583 // If the fullnamedisplay setting is available, set the template to that.
3584 if (isset($CFG->fullnamedisplay)) {
3585 $template = $CFG->fullnamedisplay;
3587 // If the template is empty, or set to language, or $override is set, return the language string.
3588 if (empty($template) || $template == 'language' || $override) {
3589 return get_string('fullnamedisplay', null, $user);
3592 // Get all of the name fields.
3593 $allnames = get_all_user_name_fields();
3594 $requirednames = array();
3595 // With each name, see if it is in the display name template, and add it to the required names array if it is.
3596 foreach ($allnames as $allname) {
3597 if (strpos($template, $allname) !== false) {
3598 $requirednames[] = $allname;
3599 // If the field is in the template but not set in the user object then notify the programmer that it needs to be fixed.
3600 if (!array_key_exists($allname, $user)) {
3601 debugging('You need to update your sql to include additional name fields in the user object.', DEBUG_DEVELOPER);
3606 $displayname = $template;
3607 // Switch in the actual data into the template.
3608 foreach ($requirednames as $altname) {
3609 if (isset($user->$altname)) {
3610 // Using empty() on the below if statement causes breakages.
3611 if ((string)$user->$altname == '') {
3612 $displayname = str_replace($altname, 'EMPTY', $displayname);
3613 } else {
3614 $displayname = str_replace($altname, $user->$altname, $displayname);
3616 } else {
3617 $displayname = str_replace($altname, 'EMPTY', $displayname);
3620 // Tidy up any misc. characters (Not perfect, but gets most characters).
3621 // Don't remove the "u" at the end of the first expression unless you want garbled characters when combining hiragana or
3622 // katakana and parenthesis.
3623 $patterns = array();
3624 // This regular expression replacement is to fix problems such as 'James () Kirk' Where 'Tiberius' (middlename) has not been
3625 // filled in by a user.
3626 // The special characters are Japanese brackets that are common enough to make allowances for them (not covered by :punct:).
3627 $patterns[] = '/[[:punct:]「」]*EMPTY[[:punct:]「」]*/u';
3628 // This regular expression is to remove any double spaces in the display name.
3629 $patterns[] = '/\s{2,}/';
3630 foreach ($patterns as $pattern) {
3631 $displayname = preg_replace($pattern, ' ', $displayname);
3634 // Trimming $displayname will help the next check to ensure that we don't have a display name with spaces.
3635 $displayname = trim($displayname);
3636 if (empty($displayname)) {
3637 // Going with just the first name if no alternate fields are filled out. May be changed later depending on what
3638 // people in general feel is a good setting to fall back on.
3639 $displayname = $user->firstname;
3641 return $displayname;
3645 * A centralised location for the all name fields. Returns an array / sql string snippet.
3647 * @param bool $returnsql True for an sql select field snippet.
3648 * @param string $alias table alias to use in front of each field.
3649 * @return array|string All name fields.
3651 function get_all_user_name_fields($returnsql = false, $alias = null) {
3652 $alternatenames = array('firstnamephonetic',
3653 'lastnamephonetic',
3654 'middlename',
3655 'alternatename',
3656 'firstname',
3657 'lastname');
3658 if ($returnsql) {
3659 if ($alias) {
3660 foreach ($alternatenames as $key => $altname) {
3661 $alternatenames[$key] = "$alias.$altname";
3664 $alternatenames = implode(',', $alternatenames);
3666 return $alternatenames;
3670 * Returns an array of values in order of occurance in a provided string.
3671 * The key in the result is the character postion in the string.
3673 * @param array $values Values to be found in the string format
3674 * @param string $stringformat The string which may contain values being searched for.
3675 * @return array An array of values in order according to placement in the string format.
3677 function order_in_string($values, $stringformat) {
3678 $valuearray = array();
3679 foreach ($values as $value) {
3680 $pattern = "/$value\b/";
3681 // Using preg_match as strpos() may match values that are similar e.g. firstname and firstnamephonetic.
3682 if (preg_match($pattern, $stringformat)) {
3683 $replacement = "thing";
3684 // Replace the value with something more unique to ensure we get the right position when using strpos().
3685 $newformat = preg_replace($pattern, $replacement, $stringformat);
3686 $position = strpos($newformat, $replacement);
3687 $valuearray[$position] = $value;
3690 ksort($valuearray);
3691 return $valuearray;
3695 * Checks if current user is shown any extra fields when listing users.
3697 * @param object $context Context
3698 * @param array $already Array of fields that we're going to show anyway
3699 * so don't bother listing them
3700 * @return array Array of field names from user table, not including anything
3701 * listed in $already
3703 function get_extra_user_fields($context, $already = array()) {
3704 global $CFG;
3706 // Only users with permission get the extra fields.
3707 if (!has_capability('moodle/site:viewuseridentity', $context)) {
3708 return array();
3711 // Split showuseridentity on comma.
3712 if (empty($CFG->showuseridentity)) {
3713 // Explode gives wrong result with empty string.
3714 $extra = array();
3715 } else {
3716 $extra = explode(',', $CFG->showuseridentity);
3718 $renumber = false;
3719 foreach ($extra as $key => $field) {
3720 if (in_array($field, $already)) {
3721 unset($extra[$key]);
3722 $renumber = true;
3725 if ($renumber) {
3726 // For consistency, if entries are removed from array, renumber it
3727 // so they are numbered as you would expect.
3728 $extra = array_merge($extra);
3730 return $extra;
3734 * If the current user is to be shown extra user fields when listing or
3735 * selecting users, returns a string suitable for including in an SQL select
3736 * clause to retrieve those fields.
3738 * @param context $context Context
3739 * @param string $alias Alias of user table, e.g. 'u' (default none)
3740 * @param string $prefix Prefix for field names using AS, e.g. 'u_' (default none)
3741 * @param array $already Array of fields that we're going to include anyway so don't list them (default none)
3742 * @return string Partial SQL select clause, beginning with comma, for example ',u.idnumber,u.department' unless it is blank
3744 function get_extra_user_fields_sql($context, $alias='', $prefix='', $already = array()) {
3745 $fields = get_extra_user_fields($context, $already);
3746 $result = '';
3747 // Add punctuation for alias.
3748 if ($alias !== '') {
3749 $alias .= '.';
3751 foreach ($fields as $field) {
3752 $result .= ', ' . $alias . $field;
3753 if ($prefix) {
3754 $result .= ' AS ' . $prefix . $field;
3757 return $result;
3761 * Returns the display name of a field in the user table. Works for most fields that are commonly displayed to users.
3762 * @param string $field Field name, e.g. 'phone1'
3763 * @return string Text description taken from language file, e.g. 'Phone number'
3765 function get_user_field_name($field) {
3766 // Some fields have language strings which are not the same as field name.
3767 switch ($field) {
3768 case 'phone1' : {
3769 return get_string('phone');
3771 case 'url' : {
3772 return get_string('webpage');
3774 case 'icq' : {
3775 return get_string('icqnumber');
3777 case 'skype' : {
3778 return get_string('skypeid');
3780 case 'aim' : {
3781 return get_string('aimid');
3783 case 'yahoo' : {
3784 return get_string('yahooid');
3786 case 'msn' : {
3787 return get_string('msnid');
3790 // Otherwise just use the same lang string.
3791 return get_string($field);
3795 * Returns whether a given authentication plugin exists.
3797 * @param string $auth Form of authentication to check for. Defaults to the global setting in {@link $CFG}.
3798 * @return boolean Whether the plugin is available.
3800 function exists_auth_plugin($auth) {
3801 global $CFG;
3803 if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) {
3804 return is_readable("{$CFG->dirroot}/auth/$auth/auth.php");
3806 return false;
3810 * Checks if a given plugin is in the list of enabled authentication plugins.
3812 * @param string $auth Authentication plugin.
3813 * @return boolean Whether the plugin is enabled.
3815 function is_enabled_auth($auth) {
3816 if (empty($auth)) {
3817 return false;
3820 $enabled = get_enabled_auth_plugins();
3822 return in_array($auth, $enabled);
3826 * Returns an authentication plugin instance.
3828 * @param string $auth name of authentication plugin
3829 * @return auth_plugin_base An instance of the required authentication plugin.
3831 function get_auth_plugin($auth) {
3832 global $CFG;
3834 // Check the plugin exists first.
3835 if (! exists_auth_plugin($auth)) {
3836 print_error('authpluginnotfound', 'debug', '', $auth);
3839 // Return auth plugin instance.
3840 require_once("{$CFG->dirroot}/auth/$auth/auth.php");
3841 $class = "auth_plugin_$auth";
3842 return new $class;
3846 * Returns array of active auth plugins.
3848 * @param bool $fix fix $CFG->auth if needed
3849 * @return array
3851 function get_enabled_auth_plugins($fix=false) {
3852 global $CFG;
3854 $default = array('manual', 'nologin');
3856 if (empty($CFG->auth)) {
3857 $auths = array();
3858 } else {
3859 $auths = explode(',', $CFG->auth);
3862 if ($fix) {
3863 $auths = array_unique($auths);
3864 foreach ($auths as $k => $authname) {
3865 if (!exists_auth_plugin($authname) or in_array($authname, $default)) {
3866 unset($auths[$k]);
3869 $newconfig = implode(',', $auths);
3870 if (!isset($CFG->auth) or $newconfig != $CFG->auth) {
3871 set_config('auth', $newconfig);
3875 return (array_merge($default, $auths));
3879 * Returns true if an internal authentication method is being used.
3880 * if method not specified then, global default is assumed
3882 * @param string $auth Form of authentication required
3883 * @return bool
3885 function is_internal_auth($auth) {
3886 // Throws error if bad $auth.
3887 $authplugin = get_auth_plugin($auth);
3888 return $authplugin->is_internal();
3892 * Returns true if the user is a 'restored' one.
3894 * Used in the login process to inform the user and allow him/her to reset the password
3896 * @param string $username username to be checked
3897 * @return bool
3899 function is_restored_user($username) {
3900 global $CFG, $DB;
3902 return $DB->record_exists('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'password' => 'restored'));
3906 * Returns an array of user fields
3908 * @return array User field/column names
3910 function get_user_fieldnames() {
3911 global $DB;
3913 $fieldarray = $DB->get_columns('user');
3914 unset($fieldarray['id']);
3915 $fieldarray = array_keys($fieldarray);
3917 return $fieldarray;
3921 * Creates a bare-bones user record
3923 * @todo Outline auth types and provide code example
3925 * @param string $username New user's username to add to record
3926 * @param string $password New user's password to add to record
3927 * @param string $auth Form of authentication required
3928 * @return stdClass A complete user object
3930 function create_user_record($username, $password, $auth = 'manual') {
3931 global $CFG, $DB;
3932 require_once($CFG->dirroot.'/user/profile/lib.php');
3933 require_once($CFG->dirroot.'/user/lib.php');
3935 // Just in case check text case.
3936 $username = trim(core_text::strtolower($username));
3938 $authplugin = get_auth_plugin($auth);
3939 $customfields = $authplugin->get_custom_user_profile_fields();
3940 $newuser = new stdClass();
3941 if ($newinfo = $authplugin->get_userinfo($username)) {
3942 $newinfo = truncate_userinfo($newinfo);
3943 foreach ($newinfo as $key => $value) {
3944 if (in_array($key, $authplugin->userfields) || (in_array($key, $customfields))) {
3945 $newuser->$key = $value;
3950 if (!empty($newuser->email)) {
3951 if (email_is_not_allowed($newuser->email)) {
3952 unset($newuser->email);
3956 if (!isset($newuser->city)) {
3957 $newuser->city = '';
3960 $newuser->auth = $auth;
3961 $newuser->username = $username;
3963 // Fix for MDL-8480
3964 // user CFG lang for user if $newuser->lang is empty
3965 // or $user->lang is not an installed language.
3966 if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) {
3967 $newuser->lang = $CFG->lang;
3969 $newuser->confirmed = 1;
3970 $newuser->lastip = getremoteaddr();
3971 $newuser->timecreated = time();
3972 $newuser->timemodified = $newuser->timecreated;
3973 $newuser->mnethostid = $CFG->mnet_localhost_id;
3975 $newuser->id = user_create_user($newuser, false);
3977 // Save user profile data.
3978 profile_save_data($newuser);
3980 $user = get_complete_user_data('id', $newuser->id);
3981 if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})) {
3982 set_user_preference('auth_forcepasswordchange', 1, $user);
3984 // Set the password.
3985 update_internal_user_password($user, $password);
3987 return $user;
3991 * Will update a local user record from an external source (MNET users can not be updated using this method!).
3993 * @param string $username user's username to update the record
3994 * @return stdClass A complete user object
3996 function update_user_record($username) {
3997 global $DB, $CFG;
3998 require_once($CFG->dirroot."/user/profile/lib.php");
3999 require_once($CFG->dirroot.'/user/lib.php');
4000 // Just in case check text case.
4001 $username = trim(core_text::strtolower($username));
4003 $oldinfo = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id), '*', MUST_EXIST);
4004 $newuser = array();
4005 $userauth = get_auth_plugin($oldinfo->auth);
4007 if ($newinfo = $userauth->get_userinfo($username)) {
4008 $newinfo = truncate_userinfo($newinfo);
4009 $customfields = $userauth->get_custom_user_profile_fields();
4011 foreach ($newinfo as $key => $value) {
4012 $key = strtolower($key);
4013 $iscustom = in_array($key, $customfields);
4014 if ((!property_exists($oldinfo, $key) && !$iscustom) or $key === 'username' or $key === 'id'
4015 or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') {
4016 // Unknown or must not be changed.
4017 continue;
4019 $confval = $userauth->config->{'field_updatelocal_' . $key};
4020 $lockval = $userauth->config->{'field_lock_' . $key};
4021 if (empty($confval) || empty($lockval)) {
4022 continue;
4024 if ($confval === 'onlogin') {
4025 // MDL-4207 Don't overwrite modified user profile values with
4026 // empty LDAP values when 'unlocked if empty' is set. The purpose
4027 // of the setting 'unlocked if empty' is to allow the user to fill
4028 // in a value for the selected field _if LDAP is giving
4029 // nothing_ for this field. Thus it makes sense to let this value
4030 // stand in until LDAP is giving a value for this field.
4031 if (!(empty($value) && $lockval === 'unlockedifempty')) {
4032 if ($iscustom || (in_array($key, $userauth->userfields) &&
4033 ((string)$oldinfo->$key !== (string)$value))) {
4034 $newuser[$key] = (string)$value;
4039 if ($newuser) {
4040 $newuser['id'] = $oldinfo->id;
4041 $newuser['timemodified'] = time();
4042 user_update_user((object) $newuser, false);
4044 // Save user profile data.
4045 profile_save_data((object) $newuser);
4049 return get_complete_user_data('id', $oldinfo->id);
4053 * Will truncate userinfo as it comes from auth_get_userinfo (from external auth) which may have large fields.
4055 * @param array $info Array of user properties to truncate if needed
4056 * @return array The now truncated information that was passed in
4058 function truncate_userinfo(array $info) {
4059 // Define the limits.
4060 $limit = array(
4061 'username' => 100,
4062 'idnumber' => 255,
4063 'firstname' => 100,
4064 'lastname' => 100,
4065 'email' => 100,
4066 'icq' => 15,
4067 'phone1' => 20,
4068 'phone2' => 20,
4069 'institution' => 40,
4070 'department' => 30,
4071 'address' => 70,
4072 'city' => 120,
4073 'country' => 2,
4074 'url' => 255,
4077 // Apply where needed.
4078 foreach (array_keys($info) as $key) {
4079 if (!empty($limit[$key])) {
4080 $info[$key] = trim(core_text::substr($info[$key], 0, $limit[$key]));
4084 return $info;
4088 * Marks user deleted in internal user database and notifies the auth plugin.
4089 * Also unenrols user from all roles and does other cleanup.
4091 * Any plugin that needs to purge user data should register the 'user_deleted' event.
4093 * @param stdClass $user full user object before delete
4094 * @return boolean success
4095 * @throws coding_exception if invalid $user parameter detected
4097 function delete_user(stdClass $user) {
4098 global $CFG, $DB;
4099 require_once($CFG->libdir.'/grouplib.php');
4100 require_once($CFG->libdir.'/gradelib.php');
4101 require_once($CFG->dirroot.'/message/lib.php');
4102 require_once($CFG->dirroot.'/tag/lib.php');
4103 require_once($CFG->dirroot.'/user/lib.php');
4105 // Make sure nobody sends bogus record type as parameter.
4106 if (!property_exists($user, 'id') or !property_exists($user, 'username')) {
4107 throw new coding_exception('Invalid $user parameter in delete_user() detected');
4110 // Better not trust the parameter and fetch the latest info this will be very expensive anyway.
4111 if (!$user = $DB->get_record('user', array('id' => $user->id))) {
4112 debugging('Attempt to delete unknown user account.');
4113 return false;
4116 // There must be always exactly one guest record, originally the guest account was identified by username only,
4117 // now we use $CFG->siteguest for performance reasons.
4118 if ($user->username === 'guest' or isguestuser($user)) {
4119 debugging('Guest user account can not be deleted.');
4120 return false;
4123 // Admin can be theoretically from different auth plugin, but we want to prevent deletion of internal accoutns only,
4124 // if anything goes wrong ppl may force somebody to be admin via config.php setting $CFG->siteadmins.
4125 if ($user->auth === 'manual' and is_siteadmin($user)) {
4126 debugging('Local administrator accounts can not be deleted.');
4127 return false;
4130 // Keep a copy of user context, we need it for event.
4131 $usercontext = context_user::instance($user->id);
4133 // Delete all grades - backup is kept in grade_grades_history table.
4134 grade_user_delete($user->id);
4136 // Move unread messages from this user to read.
4137 message_move_userfrom_unread2read($user->id);
4139 // TODO: remove from cohorts using standard API here.
4141 // Remove user tags.
4142 tag_set('user', $user->id, array());
4144 // Unconditionally unenrol from all courses.
4145 enrol_user_delete($user);
4147 // Unenrol from all roles in all contexts.
4148 // This might be slow but it is really needed - modules might do some extra cleanup!
4149 role_unassign_all(array('userid' => $user->id));
4151 // Now do a brute force cleanup.
4153 // Remove from all cohorts.
4154 $DB->delete_records('cohort_members', array('userid' => $user->id));
4156 // Remove from all groups.
4157 $DB->delete_records('groups_members', array('userid' => $user->id));
4159 // Brute force unenrol from all courses.
4160 $DB->delete_records('user_enrolments', array('userid' => $user->id));
4162 // Purge user preferences.
4163 $DB->delete_records('user_preferences', array('userid' => $user->id));
4165 // Purge user extra profile info.
4166 $DB->delete_records('user_info_data', array('userid' => $user->id));
4168 // Last course access not necessary either.
4169 $DB->delete_records('user_lastaccess', array('userid' => $user->id));
4170 // Remove all user tokens.
4171 $DB->delete_records('external_tokens', array('userid' => $user->id));
4173 // Unauthorise the user for all services.
4174 $DB->delete_records('external_services_users', array('userid' => $user->id));
4176 // Remove users private keys.
4177 $DB->delete_records('user_private_key', array('userid' => $user->id));
4179 // Force logout - may fail if file based sessions used, sorry.
4180 \core\session\manager::kill_user_sessions($user->id);
4182 // Workaround for bulk deletes of users with the same email address.
4183 $delname = "$user->email.".time();
4184 while ($DB->record_exists('user', array('username' => $delname))) { // No need to use mnethostid here.
4185 $delname++;
4188 // Mark internal user record as "deleted".
4189 $updateuser = new stdClass();
4190 $updateuser->id = $user->id;
4191 $updateuser->deleted = 1;
4192 $updateuser->username = $delname; // Remember it just in case.
4193 $updateuser->email = md5($user->username);// Store hash of username, useful importing/restoring users.
4194 $updateuser->idnumber = ''; // Clear this field to free it up.
4195 $updateuser->picture = 0;
4196 $updateuser->timemodified = time();
4198 user_update_user($updateuser, false);
4200 // Now do a final accesslib cleanup - removes all role assignments in user context and context itself.
4201 context_helper::delete_instance(CONTEXT_USER, $user->id);
4203 // Any plugin that needs to cleanup should register this event.
4204 // Trigger event.
4205 $event = \core\event\user_deleted::create(
4206 array(
4207 'objectid' => $user->id,
4208 'context' => $usercontext,
4209 'other' => array('user' => (array)clone $user)
4212 $event->add_record_snapshot('user', $updateuser);
4213 $event->trigger();
4215 // We will update the user's timemodified, as it will be passed to the user_deleted event, which
4216 // should know about this updated property persisted to the user's table.
4217 $user->timemodified = $updateuser->timemodified;
4219 // Notify auth plugin - do not block the delete even when plugin fails.
4220 $authplugin = get_auth_plugin($user->auth);
4221 $authplugin->user_delete($user);
4223 return true;
4227 * Retrieve the guest user object.
4229 * @return stdClass A {@link $USER} object
4231 function guest_user() {
4232 global $CFG, $DB;
4234 if ($newuser = $DB->get_record('user', array('id' => $CFG->siteguest))) {
4235 $newuser->confirmed = 1;
4236 $newuser->lang = $CFG->lang;
4237 $newuser->lastip = getremoteaddr();
4240 return $newuser;
4244 * Authenticates a user against the chosen authentication mechanism
4246 * Given a username and password, this function looks them
4247 * up using the currently selected authentication mechanism,
4248 * and if the authentication is successful, it returns a
4249 * valid $user object from the 'user' table.
4251 * Uses auth_ functions from the currently active auth module
4253 * After authenticate_user_login() returns success, you will need to
4254 * log that the user has logged in, and call complete_user_login() to set
4255 * the session up.
4257 * Note: this function works only with non-mnet accounts!
4259 * @param string $username User's username
4260 * @param string $password User's password
4261 * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO
4262 * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists)
4263 * @return stdClass|false A {@link $USER} object or false if error
4265 function authenticate_user_login($username, $password, $ignorelockout=false, &$failurereason=null) {
4266 global $CFG, $DB;
4267 require_once("$CFG->libdir/authlib.php");
4269 $authsenabled = get_enabled_auth_plugins();
4271 if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) {
4272 // Use manual if auth not set.
4273 $auth = empty($user->auth) ? 'manual' : $user->auth;
4274 if (!empty($user->suspended)) {
4275 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4276 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4277 $failurereason = AUTH_LOGIN_SUSPENDED;
4278 return false;
4280 if ($auth=='nologin' or !is_enabled_auth($auth)) {
4281 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4282 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Disabled Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4283 // Legacy way to suspend user.
4284 $failurereason = AUTH_LOGIN_SUSPENDED;
4285 return false;
4287 $auths = array($auth);
4289 } else {
4290 // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user().
4291 if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'deleted' => 1))) {
4292 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Deleted Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4293 $failurereason = AUTH_LOGIN_NOUSER;
4294 return false;
4297 // Do not try to authenticate non-existent accounts when user creation is not disabled.
4298 if (!empty($CFG->authpreventaccountcreation)) {
4299 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4300 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Unknown user, can not create new accounts: $username ".$_SERVER['HTTP_USER_AGENT']);
4301 $failurereason = AUTH_LOGIN_NOUSER;
4302 return false;
4305 // User does not exist.
4306 $auths = $authsenabled;
4307 $user = new stdClass();
4308 $user->id = 0;
4311 if ($ignorelockout) {
4312 // Some other mechanism protects against brute force password guessing, for example login form might include reCAPTCHA
4313 // or this function is called from a SSO script.
4314 } else if ($user->id) {
4315 // Verify login lockout after other ways that may prevent user login.
4316 if (login_is_lockedout($user)) {
4317 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4318 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Login lockout: $username ".$_SERVER['HTTP_USER_AGENT']);
4319 $failurereason = AUTH_LOGIN_LOCKOUT;
4320 return false;
4322 } else {
4323 // We can not lockout non-existing accounts.
4326 foreach ($auths as $auth) {
4327 $authplugin = get_auth_plugin($auth);
4329 // On auth fail fall through to the next plugin.
4330 if (!$authplugin->user_login($username, $password)) {
4331 continue;
4334 // Successful authentication.
4335 if ($user->id) {
4336 // User already exists in database.
4337 if (empty($user->auth)) {
4338 // For some reason auth isn't set yet.
4339 $DB->set_field('user', 'auth', $auth, array('username' => $username));
4340 $user->auth = $auth;
4343 // If the existing hash is using an out-of-date algorithm (or the legacy md5 algorithm), then we should update to
4344 // the current hash algorithm while we have access to the user's password.
4345 update_internal_user_password($user, $password);
4347 if ($authplugin->is_synchronised_with_external()) {
4348 // Update user record from external DB.
4349 $user = update_user_record($username);
4351 } else {
4352 // Create account, we verified above that user creation is allowed.
4353 $user = create_user_record($username, $password, $auth);
4356 $authplugin->sync_roles($user);
4358 foreach ($authsenabled as $hau) {
4359 $hauth = get_auth_plugin($hau);
4360 $hauth->user_authenticated_hook($user, $username, $password);
4363 if (empty($user->id)) {
4364 $failurereason = AUTH_LOGIN_NOUSER;
4365 return false;
4368 if (!empty($user->suspended)) {
4369 // Just in case some auth plugin suspended account.
4370 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4371 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4372 $failurereason = AUTH_LOGIN_SUSPENDED;
4373 return false;
4376 login_attempt_valid($user);
4377 $failurereason = AUTH_LOGIN_OK;
4378 return $user;
4381 // Failed if all the plugins have failed.
4382 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4383 if (debugging('', DEBUG_ALL)) {
4384 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Failed Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4387 if ($user->id) {
4388 login_attempt_failed($user);
4389 $failurereason = AUTH_LOGIN_FAILED;
4390 } else {
4391 $failurereason = AUTH_LOGIN_NOUSER;
4394 return false;
4398 * Call to complete the user login process after authenticate_user_login()
4399 * has succeeded. It will setup the $USER variable and other required bits
4400 * and pieces.
4402 * NOTE:
4403 * - It will NOT log anything -- up to the caller to decide what to log.
4404 * - this function does not set any cookies any more!
4406 * @param stdClass $user
4407 * @return stdClass A {@link $USER} object - BC only, do not use
4409 function complete_user_login($user) {
4410 global $CFG, $USER;
4412 \core\session\manager::login_user($user);
4414 // Reload preferences from DB.
4415 unset($USER->preference);
4416 check_user_preferences_loaded($USER);
4418 // Update login times.
4419 update_user_login_times();
4421 // Extra session prefs init.
4422 set_login_session_preferences();
4424 // Trigger login event.
4425 $event = \core\event\user_loggedin::create(
4426 array(
4427 'userid' => $USER->id,
4428 'objectid' => $USER->id,
4429 'other' => array('username' => $USER->username),
4432 $event->add_record_snapshot('user', $user);
4433 $event->trigger();
4435 if (isguestuser()) {
4436 // No need to continue when user is THE guest.
4437 return $USER;
4440 if (CLI_SCRIPT) {
4441 // We can redirect to password change URL only in browser.
4442 return $USER;
4445 // Select password change url.
4446 $userauth = get_auth_plugin($USER->auth);
4448 // Check whether the user should be changing password.
4449 if (get_user_preferences('auth_forcepasswordchange', false)) {
4450 if ($userauth->can_change_password()) {
4451 if ($changeurl = $userauth->change_password_url()) {
4452 redirect($changeurl);
4453 } else {
4454 redirect($CFG->httpswwwroot.'/login/change_password.php');
4456 } else {
4457 print_error('nopasswordchangeforced', 'auth');
4460 return $USER;
4464 * Check a password hash to see if it was hashed using the legacy hash algorithm (md5).
4466 * @param string $password String to check.
4467 * @return boolean True if the $password matches the format of an md5 sum.
4469 function password_is_legacy_hash($password) {
4470 return (bool) preg_match('/^[0-9a-f]{32}$/', $password);
4474 * Checks whether the password compatibility library will work with the current
4475 * version of PHP. This cannot be done using PHP version numbers since the fix
4476 * has been backported to earlier versions in some distributions.
4478 * See https://github.com/ircmaxell/password_compat/issues/10 for more details.
4480 * @return bool True if the library is NOT supported.
4482 function password_compat_not_supported() {
4484 $hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG';
4486 // Create a one off application cache to store bcrypt support status as
4487 // the support status doesn't change and crypt() is slow.
4488 $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'password_compat');
4490 if (!$bcryptsupport = $cache->get('bcryptsupport')) {
4491 $test = crypt('password', $hash);
4492 // Cache string instead of boolean to avoid MDL-37472.
4493 if ($test == $hash) {
4494 $bcryptsupport = 'supported';
4495 } else {
4496 $bcryptsupport = 'not supported';
4498 $cache->set('bcryptsupport', $bcryptsupport);
4501 // Return true if bcrypt *not* supported.
4502 return ($bcryptsupport !== 'supported');
4506 * Compare password against hash stored in user object to determine if it is valid.
4508 * If necessary it also updates the stored hash to the current format.
4510 * @param stdClass $user (Password property may be updated).
4511 * @param string $password Plain text password.
4512 * @return bool True if password is valid.
4514 function validate_internal_user_password($user, $password) {
4515 global $CFG;
4516 require_once($CFG->libdir.'/password_compat/lib/password.php');
4518 if ($user->password === AUTH_PASSWORD_NOT_CACHED) {
4519 // Internal password is not used at all, it can not validate.
4520 return false;
4523 // If hash isn't a legacy (md5) hash, validate using the library function.
4524 if (!password_is_legacy_hash($user->password)) {
4525 return password_verify($password, $user->password);
4528 // Otherwise we need to check for a legacy (md5) hash instead. If the hash
4529 // is valid we can then update it to the new algorithm.
4531 $sitesalt = isset($CFG->passwordsaltmain) ? $CFG->passwordsaltmain : '';
4532 $validated = false;
4534 if ($user->password === md5($password.$sitesalt)
4535 or $user->password === md5($password)
4536 or $user->password === md5(addslashes($password).$sitesalt)
4537 or $user->password === md5(addslashes($password))) {
4538 // Note: we are intentionally using the addslashes() here because we
4539 // need to accept old password hashes of passwords with magic quotes.
4540 $validated = true;
4542 } else {
4543 for ($i=1; $i<=20; $i++) { // 20 alternative salts should be enough, right?
4544 $alt = 'passwordsaltalt'.$i;
4545 if (!empty($CFG->$alt)) {
4546 if ($user->password === md5($password.$CFG->$alt) or $user->password === md5(addslashes($password).$CFG->$alt)) {
4547 $validated = true;
4548 break;
4554 if ($validated) {
4555 // If the password matches the existing md5 hash, update to the
4556 // current hash algorithm while we have access to the user's password.
4557 update_internal_user_password($user, $password);
4560 return $validated;
4564 * Calculate hash for a plain text password.
4566 * @param string $password Plain text password to be hashed.
4567 * @param bool $fasthash If true, use a low cost factor when generating the hash
4568 * This is much faster to generate but makes the hash
4569 * less secure. It is used when lots of hashes need to
4570 * be generated quickly.
4571 * @return string The hashed password.
4573 * @throws moodle_exception If a problem occurs while generating the hash.
4575 function hash_internal_user_password($password, $fasthash = false) {
4576 global $CFG;
4577 require_once($CFG->libdir.'/password_compat/lib/password.php');
4579 // Use the legacy hashing algorithm (md5) if PHP is not new enough to support bcrypt properly.
4580 if (password_compat_not_supported()) {
4581 if (isset($CFG->passwordsaltmain)) {
4582 return md5($password.$CFG->passwordsaltmain);
4583 } else {
4584 return md5($password);
4588 // Set the cost factor to 4 for fast hashing, otherwise use default cost.
4589 $options = ($fasthash) ? array('cost' => 4) : array();
4591 $generatedhash = password_hash($password, PASSWORD_DEFAULT, $options);
4593 if ($generatedhash === false || $generatedhash === null) {
4594 throw new moodle_exception('Failed to generate password hash.');
4597 return $generatedhash;
4601 * Update password hash in user object (if necessary).
4603 * The password is updated if:
4604 * 1. The password has changed (the hash of $user->password is different
4605 * to the hash of $password).
4606 * 2. The existing hash is using an out-of-date algorithm (or the legacy
4607 * md5 algorithm).
4609 * Updating the password will modify the $user object and the database
4610 * record to use the current hashing algorithm.
4612 * @param stdClass $user User object (password property may be updated).
4613 * @param string $password Plain text password.
4614 * @return bool Always returns true.
4616 function update_internal_user_password($user, $password) {
4617 global $CFG, $DB;
4618 require_once($CFG->libdir.'/password_compat/lib/password.php');
4620 // Use the legacy hashing algorithm (md5) if PHP doesn't support bcrypt properly.
4621 $legacyhash = password_compat_not_supported();
4623 // Figure out what the hashed password should be.
4624 $authplugin = get_auth_plugin($user->auth);
4625 if ($authplugin->prevent_local_passwords()) {
4626 $hashedpassword = AUTH_PASSWORD_NOT_CACHED;
4627 } else {
4628 $hashedpassword = hash_internal_user_password($password);
4631 if ($legacyhash) {
4632 $passwordchanged = ($user->password !== $hashedpassword);
4633 $algorithmchanged = false;
4634 } else {
4635 // If verification fails then it means the password has changed.
4636 $passwordchanged = !password_verify($password, $user->password);
4637 $algorithmchanged = password_needs_rehash($user->password, PASSWORD_DEFAULT);
4640 if ($passwordchanged || $algorithmchanged) {
4641 $DB->set_field('user', 'password', $hashedpassword, array('id' => $user->id));
4642 $user->password = $hashedpassword;
4645 return true;
4649 * Get a complete user record, which includes all the info in the user record.
4651 * Intended for setting as $USER session variable
4653 * @param string $field The user field to be checked for a given value.
4654 * @param string $value The value to match for $field.
4655 * @param int $mnethostid
4656 * @return mixed False, or A {@link $USER} object.
4658 function get_complete_user_data($field, $value, $mnethostid = null) {
4659 global $CFG, $DB;
4661 if (!$field || !$value) {
4662 return false;
4665 // Build the WHERE clause for an SQL query.
4666 $params = array('fieldval' => $value);
4667 $constraints = "$field = :fieldval AND deleted <> 1";
4669 // If we are loading user data based on anything other than id,
4670 // we must also restrict our search based on mnet host.
4671 if ($field != 'id') {
4672 if (empty($mnethostid)) {
4673 // If empty, we restrict to local users.
4674 $mnethostid = $CFG->mnet_localhost_id;
4677 if (!empty($mnethostid)) {
4678 $params['mnethostid'] = $mnethostid;
4679 $constraints .= " AND mnethostid = :mnethostid";
4682 // Get all the basic user data.
4683 if (! $user = $DB->get_record_select('user', $constraints, $params)) {
4684 return false;
4687 // Get various settings and preferences.
4689 // Preload preference cache.
4690 check_user_preferences_loaded($user);
4692 // Load course enrolment related stuff.
4693 $user->lastcourseaccess = array(); // During last session.
4694 $user->currentcourseaccess = array(); // During current session.
4695 if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid' => $user->id))) {
4696 foreach ($lastaccesses as $lastaccess) {
4697 $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess;
4701 $sql = "SELECT g.id, g.courseid
4702 FROM {groups} g, {groups_members} gm
4703 WHERE gm.groupid=g.id AND gm.userid=?";
4705 // This is a special hack to speedup calendar display.
4706 $user->groupmember = array();
4707 if (!isguestuser($user)) {
4708 if ($groups = $DB->get_records_sql($sql, array($user->id))) {
4709 foreach ($groups as $group) {
4710 if (!array_key_exists($group->courseid, $user->groupmember)) {
4711 $user->groupmember[$group->courseid] = array();
4713 $user->groupmember[$group->courseid][$group->id] = $group->id;
4718 // Add the custom profile fields to the user record.
4719 $user->profile = array();
4720 if (!isguestuser($user)) {
4721 require_once($CFG->dirroot.'/user/profile/lib.php');
4722 profile_load_custom_fields($user);
4725 // Rewrite some variables if necessary.
4726 if (!empty($user->description)) {
4727 // No need to cart all of it around.
4728 $user->description = true;
4730 if (isguestuser($user)) {
4731 // Guest language always same as site.
4732 $user->lang = $CFG->lang;
4733 // Name always in current language.
4734 $user->firstname = get_string('guestuser');
4735 $user->lastname = ' ';
4738 return $user;
4742 * Validate a password against the configured password policy
4744 * @param string $password the password to be checked against the password policy
4745 * @param string $errmsg the error message to display when the password doesn't comply with the policy.
4746 * @return bool true if the password is valid according to the policy. false otherwise.
4748 function check_password_policy($password, &$errmsg) {
4749 global $CFG;
4751 if (empty($CFG->passwordpolicy)) {
4752 return true;
4755 $errmsg = '';
4756 if (core_text::strlen($password) < $CFG->minpasswordlength) {
4757 $errmsg .= '<div>'. get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength) .'</div>';
4760 if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) {
4761 $errmsg .= '<div>'. get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits) .'</div>';
4764 if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) {
4765 $errmsg .= '<div>'. get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower) .'</div>';
4768 if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) {
4769 $errmsg .= '<div>'. get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper) .'</div>';
4772 if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) {
4773 $errmsg .= '<div>'. get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum) .'</div>';
4775 if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) {
4776 $errmsg .= '<div>'. get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars) .'</div>';
4779 if ($errmsg == '') {
4780 return true;
4781 } else {
4782 return false;
4788 * When logging in, this function is run to set certain preferences for the current SESSION.
4790 function set_login_session_preferences() {
4791 global $SESSION;
4793 $SESSION->justloggedin = true;
4795 unset($SESSION->lang);
4800 * Delete a course, including all related data from the database, and any associated files.
4802 * @param mixed $courseorid The id of the course or course object to delete.
4803 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4804 * @return bool true if all the removals succeeded. false if there were any failures. If this
4805 * method returns false, some of the removals will probably have succeeded, and others
4806 * failed, but you have no way of knowing which.
4808 function delete_course($courseorid, $showfeedback = true) {
4809 global $DB;
4811 if (is_object($courseorid)) {
4812 $courseid = $courseorid->id;
4813 $course = $courseorid;
4814 } else {
4815 $courseid = $courseorid;
4816 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
4817 return false;
4820 $context = context_course::instance($courseid);
4822 // Frontpage course can not be deleted!!
4823 if ($courseid == SITEID) {
4824 return false;
4827 // Make the course completely empty.
4828 remove_course_contents($courseid, $showfeedback);
4830 // Delete the course and related context instance.
4831 context_helper::delete_instance(CONTEXT_COURSE, $courseid);
4833 // We will update the course's timemodified, as it will be passed to the course_deleted event,
4834 // which should know about this updated property, as this event is meant to pass the full course record.
4835 $course->timemodified = time();
4837 $DB->delete_records("course", array("id" => $courseid));
4838 $DB->delete_records("course_format_options", array("courseid" => $courseid));
4840 // Trigger a course deleted event.
4841 $event = \core\event\course_deleted::create(array(
4842 'objectid' => $course->id,
4843 'context' => $context,
4844 'other' => array('shortname' => $course->shortname,
4845 'fullname' => $course->fullname)
4847 $event->add_record_snapshot('course', $course);
4848 $event->trigger();
4850 return true;
4854 * Clear a course out completely, deleting all content but don't delete the course itself.
4856 * This function does not verify any permissions.
4858 * Please note this function also deletes all user enrolments,
4859 * enrolment instances and role assignments by default.
4861 * $options:
4862 * - 'keep_roles_and_enrolments' - false by default
4863 * - 'keep_groups_and_groupings' - false by default
4865 * @param int $courseid The id of the course that is being deleted
4866 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4867 * @param array $options extra options
4868 * @return bool true if all the removals succeeded. false if there were any failures. If this
4869 * method returns false, some of the removals will probably have succeeded, and others
4870 * failed, but you have no way of knowing which.
4872 function remove_course_contents($courseid, $showfeedback = true, array $options = null) {
4873 global $CFG, $DB, $OUTPUT;
4875 require_once($CFG->libdir.'/badgeslib.php');
4876 require_once($CFG->libdir.'/completionlib.php');
4877 require_once($CFG->libdir.'/questionlib.php');
4878 require_once($CFG->libdir.'/gradelib.php');
4879 require_once($CFG->dirroot.'/group/lib.php');
4880 require_once($CFG->dirroot.'/tag/coursetagslib.php');
4881 require_once($CFG->dirroot.'/comment/lib.php');
4882 require_once($CFG->dirroot.'/rating/lib.php');
4884 // Handle course badges.
4885 badges_handle_course_deletion($courseid);
4887 // NOTE: these concatenated strings are suboptimal, but it is just extra info...
4888 $strdeleted = get_string('deleted').' - ';
4890 // Some crazy wishlist of stuff we should skip during purging of course content.
4891 $options = (array)$options;
4893 $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
4894 $coursecontext = context_course::instance($courseid);
4895 $fs = get_file_storage();
4897 // Delete course completion information, this has to be done before grades and enrols.
4898 $cc = new completion_info($course);
4899 $cc->clear_criteria();
4900 if ($showfeedback) {
4901 echo $OUTPUT->notification($strdeleted.get_string('completion', 'completion'), 'notifysuccess');
4904 // Remove all data from gradebook - this needs to be done before course modules
4905 // because while deleting this information, the system may need to reference
4906 // the course modules that own the grades.
4907 remove_course_grades($courseid, $showfeedback);
4908 remove_grade_letters($coursecontext, $showfeedback);
4910 // Delete course blocks in any all child contexts,
4911 // they may depend on modules so delete them first.
4912 $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
4913 foreach ($childcontexts as $childcontext) {
4914 blocks_delete_all_for_context($childcontext->id);
4916 unset($childcontexts);
4917 blocks_delete_all_for_context($coursecontext->id);
4918 if ($showfeedback) {
4919 echo $OUTPUT->notification($strdeleted.get_string('type_block_plural', 'plugin'), 'notifysuccess');
4922 // Delete every instance of every module,
4923 // this has to be done before deleting of course level stuff.
4924 $locations = core_component::get_plugin_list('mod');
4925 foreach ($locations as $modname => $moddir) {
4926 if ($modname === 'NEWMODULE') {
4927 continue;
4929 if ($module = $DB->get_record('modules', array('name' => $modname))) {
4930 include_once("$moddir/lib.php"); // Shows php warning only if plugin defective.
4931 $moddelete = $modname .'_delete_instance'; // Delete everything connected to an instance.
4932 $moddeletecourse = $modname .'_delete_course'; // Delete other stray stuff (uncommon).
4934 if ($instances = $DB->get_records($modname, array('course' => $course->id))) {
4935 foreach ($instances as $instance) {
4936 if ($cm = get_coursemodule_from_instance($modname, $instance->id, $course->id)) {
4937 // Delete activity context questions and question categories.
4938 question_delete_activity($cm, $showfeedback);
4940 if (function_exists($moddelete)) {
4941 // This purges all module data in related tables, extra user prefs, settings, etc.
4942 $moddelete($instance->id);
4943 } else {
4944 // NOTE: we should not allow installation of modules with missing delete support!
4945 debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!");
4946 $DB->delete_records($modname, array('id' => $instance->id));
4949 if ($cm) {
4950 // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition.
4951 context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4952 $DB->delete_records('course_modules', array('id' => $cm->id));
4956 if (function_exists($moddeletecourse)) {
4957 // Execute ptional course cleanup callback.
4958 $moddeletecourse($course, $showfeedback);
4960 if ($instances and $showfeedback) {
4961 echo $OUTPUT->notification($strdeleted.get_string('pluginname', $modname), 'notifysuccess');
4963 } else {
4964 // Ooops, this module is not properly installed, force-delete it in the next block.
4968 // We have tried to delete everything the nice way - now let's force-delete any remaining module data.
4970 // Remove all data from availability and completion tables that is associated
4971 // with course-modules belonging to this course. Note this is done even if the
4972 // features are not enabled now, in case they were enabled previously.
4973 $DB->delete_records_select('course_modules_completion',
4974 'coursemoduleid IN (SELECT id from {course_modules} WHERE course=?)',
4975 array($courseid));
4976 $DB->delete_records_select('course_modules_availability',
4977 'coursemoduleid IN (SELECT id from {course_modules} WHERE course=?)',
4978 array($courseid));
4979 $DB->delete_records_select('course_modules_avail_fields',
4980 'coursemoduleid IN (SELECT id from {course_modules} WHERE course=?)',
4981 array($courseid));
4983 // Remove course-module data.
4984 $cms = $DB->get_records('course_modules', array('course' => $course->id));
4985 foreach ($cms as $cm) {
4986 if ($module = $DB->get_record('modules', array('id' => $cm->module))) {
4987 try {
4988 $DB->delete_records($module->name, array('id' => $cm->instance));
4989 } catch (Exception $e) {
4990 // Ignore weird or missing table problems.
4993 context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4994 $DB->delete_records('course_modules', array('id' => $cm->id));
4997 if ($showfeedback) {
4998 echo $OUTPUT->notification($strdeleted.get_string('type_mod_plural', 'plugin'), 'notifysuccess');
5001 // Cleanup the rest of plugins.
5002 $cleanuplugintypes = array('report', 'coursereport', 'format');
5003 foreach ($cleanuplugintypes as $type) {
5004 $plugins = get_plugin_list_with_function($type, 'delete_course', 'lib.php');
5005 foreach ($plugins as $plugin => $pluginfunction) {
5006 $pluginfunction($course->id, $showfeedback);
5008 if ($showfeedback) {
5009 echo $OUTPUT->notification($strdeleted.get_string('type_'.$type.'_plural', 'plugin'), 'notifysuccess');
5013 // Delete questions and question categories.
5014 question_delete_course($course, $showfeedback);
5015 if ($showfeedback) {
5016 echo $OUTPUT->notification($strdeleted.get_string('questions', 'question'), 'notifysuccess');
5019 // Make sure there are no subcontexts left - all valid blocks and modules should be already gone.
5020 $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
5021 foreach ($childcontexts as $childcontext) {
5022 $childcontext->delete();
5024 unset($childcontexts);
5026 // Remove all roles and enrolments by default.
5027 if (empty($options['keep_roles_and_enrolments'])) {
5028 // This hack is used in restore when deleting contents of existing course.
5029 role_unassign_all(array('contextid' => $coursecontext->id, 'component' => ''), true);
5030 enrol_course_delete($course);
5031 if ($showfeedback) {
5032 echo $OUTPUT->notification($strdeleted.get_string('type_enrol_plural', 'plugin'), 'notifysuccess');
5036 // Delete any groups, removing members and grouping/course links first.
5037 if (empty($options['keep_groups_and_groupings'])) {
5038 groups_delete_groupings($course->id, $showfeedback);
5039 groups_delete_groups($course->id, $showfeedback);
5042 // Filters be gone!
5043 filter_delete_all_for_context($coursecontext->id);
5045 // Die comments!
5046 comment::delete_comments($coursecontext->id);
5048 // Ratings are history too.
5049 $delopt = new stdclass();
5050 $delopt->contextid = $coursecontext->id;
5051 $rm = new rating_manager();
5052 $rm->delete_ratings($delopt);
5054 // Delete course tags.
5055 coursetag_delete_course_tags($course->id, $showfeedback);
5057 // Delete calendar events.
5058 $DB->delete_records('event', array('courseid' => $course->id));
5059 $fs->delete_area_files($coursecontext->id, 'calendar');
5061 // Delete all related records in other core tables that may have a courseid
5062 // This array stores the tables that need to be cleared, as
5063 // table_name => column_name that contains the course id.
5064 $tablestoclear = array(
5065 'log' => 'course', // Course logs (NOTE: this might be changed in the future).
5066 'backup_courses' => 'courseid', // Scheduled backup stuff.
5067 'user_lastaccess' => 'courseid', // User access info.
5069 foreach ($tablestoclear as $table => $col) {
5070 $DB->delete_records($table, array($col => $course->id));
5073 // Delete all course backup files.
5074 $fs->delete_area_files($coursecontext->id, 'backup');
5076 // Cleanup course record - remove links to deleted stuff.
5077 $oldcourse = new stdClass();
5078 $oldcourse->id = $course->id;
5079 $oldcourse->summary = '';
5080 $oldcourse->cacherev = 0;
5081 $oldcourse->legacyfiles = 0;
5082 $oldcourse->enablecompletion = 0;
5083 if (!empty($options['keep_groups_and_groupings'])) {
5084 $oldcourse->defaultgroupingid = 0;
5086 $DB->update_record('course', $oldcourse);
5088 // Delete course sections and availability options.
5089 $DB->delete_records_select('course_sections_availability',
5090 'coursesectionid IN (SELECT id from {course_sections} WHERE course=?)',
5091 array($course->id));
5092 $DB->delete_records_select('course_sections_avail_fields',
5093 'coursesectionid IN (SELECT id from {course_sections} WHERE course=?)',
5094 array($course->id));
5095 $DB->delete_records('course_sections', array('course' => $course->id));
5097 // Delete legacy, section and any other course files.
5098 $fs->delete_area_files($coursecontext->id, 'course'); // Files from summary and section.
5100 // Delete all remaining stuff linked to context such as files, comments, ratings, etc.
5101 if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) {
5102 // Easy, do not delete the context itself...
5103 $coursecontext->delete_content();
5104 } else {
5105 // Hack alert!!!!
5106 // We can not drop all context stuff because it would bork enrolments and roles,
5107 // there might be also files used by enrol plugins...
5110 // Delete legacy files - just in case some files are still left there after conversion to new file api,
5111 // also some non-standard unsupported plugins may try to store something there.
5112 fulldelete($CFG->dataroot.'/'.$course->id);
5114 // Delete from cache to reduce the cache size especially makes sense in case of bulk course deletion.
5115 $cachemodinfo = cache::make('core', 'coursemodinfo');
5116 $cachemodinfo->delete($courseid);
5118 // Trigger a course content deleted event.
5119 $event = \core\event\course_content_deleted::create(array(
5120 'objectid' => $course->id,
5121 'context' => $coursecontext,
5122 'other' => array('shortname' => $course->shortname,
5123 'fullname' => $course->fullname,
5124 'options' => $options) // Passing this for legacy reasons.
5126 $event->add_record_snapshot('course', $course);
5127 $event->trigger();
5129 return true;
5133 * Change dates in module - used from course reset.
5135 * @param string $modname forum, assignment, etc
5136 * @param array $fields array of date fields from mod table
5137 * @param int $timeshift time difference
5138 * @param int $courseid
5139 * @return bool success
5141 function shift_course_mod_dates($modname, $fields, $timeshift, $courseid) {
5142 global $CFG, $DB;
5143 include_once($CFG->dirroot.'/mod/'.$modname.'/lib.php');
5145 $return = true;
5146 foreach ($fields as $field) {
5147 $updatesql = "UPDATE {".$modname."}
5148 SET $field = $field + ?
5149 WHERE course=? AND $field<>0";
5150 $return = $DB->execute($updatesql, array($timeshift, $courseid)) && $return;
5153 $refreshfunction = $modname.'_refresh_events';
5154 if (function_exists($refreshfunction)) {
5155 $refreshfunction($courseid);
5158 return $return;
5162 * This function will empty a course of user data.
5163 * It will retain the activities and the structure of the course.
5165 * @param object $data an object containing all the settings including courseid (without magic quotes)
5166 * @return array status array of array component, item, error
5168 function reset_course_userdata($data) {
5169 global $CFG, $DB;
5170 require_once($CFG->libdir.'/gradelib.php');
5171 require_once($CFG->libdir.'/completionlib.php');
5172 require_once($CFG->dirroot.'/group/lib.php');
5174 $data->courseid = $data->id;
5175 $context = context_course::instance($data->courseid);
5177 $eventparams = array(
5178 'context' => $context,
5179 'courseid' => $data->id,
5180 'other' => array(
5181 'reset_options' => (array) $data
5184 $event = \core\event\course_reset_started::create($eventparams);
5185 $event->trigger();
5187 // Calculate the time shift of dates.
5188 if (!empty($data->reset_start_date)) {
5189 // Time part of course startdate should be zero.
5190 $data->timeshift = $data->reset_start_date - usergetmidnight($data->reset_start_date_old);
5191 } else {
5192 $data->timeshift = 0;
5195 // Result array: component, item, error.
5196 $status = array();
5198 // Start the resetting.
5199 $componentstr = get_string('general');
5201 // Move the course start time.
5202 if (!empty($data->reset_start_date) and $data->timeshift) {
5203 // Change course start data.
5204 $DB->set_field('course', 'startdate', $data->reset_start_date, array('id' => $data->courseid));
5205 // Update all course and group events - do not move activity events.
5206 $updatesql = "UPDATE {event}
5207 SET timestart = timestart + ?
5208 WHERE courseid=? AND instance=0";
5209 $DB->execute($updatesql, array($data->timeshift, $data->courseid));
5211 $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false);
5214 if (!empty($data->reset_logs)) {
5215 $DB->delete_records('log', array('course' => $data->courseid));
5216 $status[] = array('component' => $componentstr, 'item' => get_string('deletelogs'), 'error' => false);
5219 if (!empty($data->reset_events)) {
5220 $DB->delete_records('event', array('courseid' => $data->courseid));
5221 $status[] = array('component' => $componentstr, 'item' => get_string('deleteevents', 'calendar'), 'error' => false);
5224 if (!empty($data->reset_notes)) {
5225 require_once($CFG->dirroot.'/notes/lib.php');
5226 note_delete_all($data->courseid);
5227 $status[] = array('component' => $componentstr, 'item' => get_string('deletenotes', 'notes'), 'error' => false);
5230 if (!empty($data->delete_blog_associations)) {
5231 require_once($CFG->dirroot.'/blog/lib.php');
5232 blog_remove_associations_for_course($data->courseid);
5233 $status[] = array('component' => $componentstr, 'item' => get_string('deleteblogassociations', 'blog'), 'error' => false);
5236 if (!empty($data->reset_completion)) {
5237 // Delete course and activity completion information.
5238 $course = $DB->get_record('course', array('id' => $data->courseid));
5239 $cc = new completion_info($course);
5240 $cc->delete_all_completion_data();
5241 $status[] = array('component' => $componentstr,
5242 'item' => get_string('deletecompletiondata', 'completion'), 'error' => false);
5245 $componentstr = get_string('roles');
5247 if (!empty($data->reset_roles_overrides)) {
5248 $children = $context->get_child_contexts();
5249 foreach ($children as $child) {
5250 $DB->delete_records('role_capabilities', array('contextid' => $child->id));
5252 $DB->delete_records('role_capabilities', array('contextid' => $context->id));
5253 // Force refresh for logged in users.
5254 $context->mark_dirty();
5255 $status[] = array('component' => $componentstr, 'item' => get_string('deletecourseoverrides', 'role'), 'error' => false);
5258 if (!empty($data->reset_roles_local)) {
5259 $children = $context->get_child_contexts();
5260 foreach ($children as $child) {
5261 role_unassign_all(array('contextid' => $child->id));
5263 // Force refresh for logged in users.
5264 $context->mark_dirty();
5265 $status[] = array('component' => $componentstr, 'item' => get_string('deletelocalroles', 'role'), 'error' => false);
5268 // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc.
5269 $data->unenrolled = array();
5270 if (!empty($data->unenrol_users)) {
5271 $plugins = enrol_get_plugins(true);
5272 $instances = enrol_get_instances($data->courseid, true);
5273 foreach ($instances as $key => $instance) {
5274 if (!isset($plugins[$instance->enrol])) {
5275 unset($instances[$key]);
5276 continue;
5280 foreach ($data->unenrol_users as $withroleid) {
5281 if ($withroleid) {
5282 $sql = "SELECT ue.*
5283 FROM {user_enrolments} ue
5284 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5285 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5286 JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)";
5287 $params = array('courseid' => $data->courseid, 'roleid' => $withroleid, 'courselevel' => CONTEXT_COURSE);
5289 } else {
5290 // Without any role assigned at course context.
5291 $sql = "SELECT ue.*
5292 FROM {user_enrolments} ue
5293 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5294 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5295 LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid)
5296 WHERE ra.id IS null";
5297 $params = array('courseid' => $data->courseid, 'courselevel' => CONTEXT_COURSE);
5300 $rs = $DB->get_recordset_sql($sql, $params);
5301 foreach ($rs as $ue) {
5302 if (!isset($instances[$ue->enrolid])) {
5303 continue;
5305 $instance = $instances[$ue->enrolid];
5306 $plugin = $plugins[$instance->enrol];
5307 if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) {
5308 continue;
5311 $plugin->unenrol_user($instance, $ue->userid);
5312 $data->unenrolled[$ue->userid] = $ue->userid;
5314 $rs->close();
5317 if (!empty($data->unenrolled)) {
5318 $status[] = array(
5319 'component' => $componentstr,
5320 'item' => get_string('unenrol', 'enrol').' ('.count($data->unenrolled).')',
5321 'error' => false
5325 $componentstr = get_string('groups');
5327 // Remove all group members.
5328 if (!empty($data->reset_groups_members)) {
5329 groups_delete_group_members($data->courseid);
5330 $status[] = array('component' => $componentstr, 'item' => get_string('removegroupsmembers', 'group'), 'error' => false);
5333 // Remove all groups.
5334 if (!empty($data->reset_groups_remove)) {
5335 groups_delete_groups($data->courseid, false);
5336 $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroups', 'group'), 'error' => false);
5339 // Remove all grouping members.
5340 if (!empty($data->reset_groupings_members)) {
5341 groups_delete_groupings_groups($data->courseid, false);
5342 $status[] = array('component' => $componentstr, 'item' => get_string('removegroupingsmembers', 'group'), 'error' => false);
5345 // Remove all groupings.
5346 if (!empty($data->reset_groupings_remove)) {
5347 groups_delete_groupings($data->courseid, false);
5348 $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroupings', 'group'), 'error' => false);
5351 // Look in every instance of every module for data to delete.
5352 $unsupportedmods = array();
5353 if ($allmods = $DB->get_records('modules') ) {
5354 foreach ($allmods as $mod) {
5355 $modname = $mod->name;
5356 $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php';
5357 $moddeleteuserdata = $modname.'_reset_userdata'; // Function to delete user data.
5358 if (file_exists($modfile)) {
5359 if (!$DB->count_records($modname, array('course' => $data->courseid))) {
5360 continue; // Skip mods with no instances.
5362 include_once($modfile);
5363 if (function_exists($moddeleteuserdata)) {
5364 $modstatus = $moddeleteuserdata($data);
5365 if (is_array($modstatus)) {
5366 $status = array_merge($status, $modstatus);
5367 } else {
5368 debugging('Module '.$modname.' returned incorrect staus - must be an array!');
5370 } else {
5371 $unsupportedmods[] = $mod;
5373 } else {
5374 debugging('Missing lib.php in '.$modname.' module!');
5379 // Mention unsupported mods.
5380 if (!empty($unsupportedmods)) {
5381 foreach ($unsupportedmods as $mod) {
5382 $status[] = array(
5383 'component' => get_string('modulenameplural', $mod->name),
5384 'item' => '',
5385 'error' => get_string('resetnotimplemented')
5390 $componentstr = get_string('gradebook', 'grades');
5391 // Reset gradebook,.
5392 if (!empty($data->reset_gradebook_items)) {
5393 remove_course_grades($data->courseid, false);
5394 grade_grab_course_grades($data->courseid);
5395 grade_regrade_final_grades($data->courseid);
5396 $status[] = array('component' => $componentstr, 'item' => get_string('removeallcourseitems', 'grades'), 'error' => false);
5398 } else if (!empty($data->reset_gradebook_grades)) {
5399 grade_course_reset($data->courseid);
5400 $status[] = array('component' => $componentstr, 'item' => get_string('removeallcoursegrades', 'grades'), 'error' => false);
5402 // Reset comments.
5403 if (!empty($data->reset_comments)) {
5404 require_once($CFG->dirroot.'/comment/lib.php');
5405 comment::reset_course_page_comments($context);
5408 $event = \core\event\course_reset_ended::create($eventparams);
5409 $event->trigger();
5411 return $status;
5415 * Generate an email processing address.
5417 * @param int $modid
5418 * @param string $modargs
5419 * @return string Returns email processing address
5421 function generate_email_processing_address($modid, $modargs) {
5422 global $CFG;
5424 $header = $CFG->mailprefix . substr(base64_encode(pack('C', $modid)), 0, 2).$modargs;
5425 return $header . substr(md5($header.get_site_identifier()), 0, 16).'@'.$CFG->maildomain;
5431 * @todo Finish documenting this function
5433 * @param string $modargs
5434 * @param string $body Currently unused
5436 function moodle_process_email($modargs, $body) {
5437 global $DB;
5439 // The first char should be an unencoded letter. We'll take this as an action.
5440 switch ($modargs{0}) {
5441 case 'B': { // Bounce.
5442 list(, $userid) = unpack('V', base64_decode(substr($modargs, 1, 8)));
5443 if ($user = $DB->get_record("user", array('id' => $userid), "id,email")) {
5444 // Check the half md5 of their email.
5445 $md5check = substr(md5($user->email), 0, 16);
5446 if ($md5check == substr($modargs, -16)) {
5447 set_bounce_count($user);
5449 // Else maybe they've already changed it?
5452 break;
5453 // Maybe more later?
5457 // CORRESPONDENCE.
5460 * Get mailer instance, enable buffering, flush buffer or disable buffering.
5462 * @param string $action 'get', 'buffer', 'close' or 'flush'
5463 * @return moodle_phpmailer|null mailer instance if 'get' used or nothing
5465 function get_mailer($action='get') {
5466 global $CFG;
5468 /** @var moodle_phpmailer $mailer */
5469 static $mailer = null;
5470 static $counter = 0;
5472 if (!isset($CFG->smtpmaxbulk)) {
5473 $CFG->smtpmaxbulk = 1;
5476 if ($action == 'get') {
5477 $prevkeepalive = false;
5479 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5480 if ($counter < $CFG->smtpmaxbulk and !$mailer->isError()) {
5481 $counter++;
5482 // Reset the mailer.
5483 $mailer->Priority = 3;
5484 $mailer->CharSet = 'UTF-8'; // Our default.
5485 $mailer->ContentType = "text/plain";
5486 $mailer->Encoding = "8bit";
5487 $mailer->From = "root@localhost";
5488 $mailer->FromName = "Root User";
5489 $mailer->Sender = "";
5490 $mailer->Subject = "";
5491 $mailer->Body = "";
5492 $mailer->AltBody = "";
5493 $mailer->ConfirmReadingTo = "";
5495 $mailer->clearAllRecipients();
5496 $mailer->clearReplyTos();
5497 $mailer->clearAttachments();
5498 $mailer->clearCustomHeaders();
5499 return $mailer;
5502 $prevkeepalive = $mailer->SMTPKeepAlive;
5503 get_mailer('flush');
5506 require_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php');
5507 $mailer = new moodle_phpmailer();
5509 $counter = 1;
5511 if ($CFG->smtphosts == 'qmail') {
5512 // Use Qmail system.
5513 $mailer->isQmail();
5515 } else if (empty($CFG->smtphosts)) {
5516 // Use PHP mail() = sendmail.
5517 $mailer->isMail();
5519 } else {
5520 // Use SMTP directly.
5521 $mailer->isSMTP();
5522 if (!empty($CFG->debugsmtp)) {
5523 $mailer->SMTPDebug = true;
5525 // Specify main and backup servers.
5526 $mailer->Host = $CFG->smtphosts;
5527 // Specify secure connection protocol.
5528 $mailer->SMTPSecure = $CFG->smtpsecure;
5529 // Use previous keepalive.
5530 $mailer->SMTPKeepAlive = $prevkeepalive;
5532 if ($CFG->smtpuser) {
5533 // Use SMTP authentication.
5534 $mailer->SMTPAuth = true;
5535 $mailer->Username = $CFG->smtpuser;
5536 $mailer->Password = $CFG->smtppass;
5540 return $mailer;
5543 $nothing = null;
5545 // Keep smtp session open after sending.
5546 if ($action == 'buffer') {
5547 if (!empty($CFG->smtpmaxbulk)) {
5548 get_mailer('flush');
5549 $m = get_mailer();
5550 if ($m->Mailer == 'smtp') {
5551 $m->SMTPKeepAlive = true;
5554 return $nothing;
5557 // Close smtp session, but continue buffering.
5558 if ($action == 'flush') {
5559 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5560 if (!empty($mailer->SMTPDebug)) {
5561 echo '<pre>'."\n";
5563 $mailer->SmtpClose();
5564 if (!empty($mailer->SMTPDebug)) {
5565 echo '</pre>';
5568 return $nothing;
5571 // Close smtp session, do not buffer anymore.
5572 if ($action == 'close') {
5573 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5574 get_mailer('flush');
5575 $mailer->SMTPKeepAlive = false;
5577 $mailer = null; // Better force new instance.
5578 return $nothing;
5583 * Send an email to a specified user
5585 * @param stdClass $user A {@link $USER} object
5586 * @param stdClass $from A {@link $USER} object
5587 * @param string $subject plain text subject line of the email
5588 * @param string $messagetext plain text version of the message
5589 * @param string $messagehtml complete html version of the message (optional)
5590 * @param string $attachment a file on the filesystem, relative to $CFG->dataroot
5591 * @param string $attachname the name of the file (extension indicates MIME)
5592 * @param bool $usetrueaddress determines whether $from email address should
5593 * be sent out. Will be overruled by user profile setting for maildisplay
5594 * @param string $replyto Email address to reply to
5595 * @param string $replytoname Name of reply to recipient
5596 * @param int $wordwrapwidth custom word wrap width, default 79
5597 * @return bool Returns true if mail was sent OK and false if there was an error.
5599 function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '', $attachment = '', $attachname = '',
5600 $usetrueaddress = true, $replyto = '', $replytoname = '', $wordwrapwidth = 79) {
5602 global $CFG;
5604 if (empty($user) || empty($user->email)) {
5605 $nulluser = 'User is null or has no email';
5606 error_log($nulluser);
5607 if (CLI_SCRIPT) {
5608 mtrace('Error: lib/moodlelib.php email_to_user(): '.$nulluser);
5610 return false;
5613 if (!empty($user->deleted)) {
5614 // Do not mail deleted users.
5615 $userdeleted = 'User is deleted';
5616 error_log($userdeleted);
5617 if (CLI_SCRIPT) {
5618 mtrace('Error: lib/moodlelib.php email_to_user(): '.$userdeleted);
5620 return false;
5623 if (!empty($CFG->noemailever)) {
5624 // Hidden setting for development sites, set in config.php if needed.
5625 $noemail = 'Not sending email due to noemailever config setting';
5626 error_log($noemail);
5627 if (CLI_SCRIPT) {
5628 mtrace('Error: lib/moodlelib.php email_to_user(): '.$noemail);
5630 return true;
5633 if (!empty($CFG->divertallemailsto)) {
5634 $subject = "[DIVERTED {$user->email}] $subject";
5635 $user = clone($user);
5636 $user->email = $CFG->divertallemailsto;
5639 // Skip mail to suspended users.
5640 if ((isset($user->auth) && $user->auth=='nologin') or (isset($user->suspended) && $user->suspended)) {
5641 return true;
5644 if (!validate_email($user->email)) {
5645 // We can not send emails to invalid addresses - it might create security issue or confuse the mailer.
5646 $invalidemail = "User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending.";
5647 error_log($invalidemail);
5648 if (CLI_SCRIPT) {
5649 mtrace('Error: lib/moodlelib.php email_to_user(): '.$invalidemail);
5651 return false;
5654 if (over_bounce_threshold($user)) {
5655 $bouncemsg = "User $user->id (".fullname($user).") is over bounce threshold! Not sending.";
5656 error_log($bouncemsg);
5657 if (CLI_SCRIPT) {
5658 mtrace('Error: lib/moodlelib.php email_to_user(): '.$bouncemsg);
5660 return false;
5663 // If the user is a remote mnet user, parse the email text for URL to the
5664 // wwwroot and modify the url to direct the user's browser to login at their
5665 // home site (identity provider - idp) before hitting the link itself.
5666 if (is_mnet_remote_user($user)) {
5667 require_once($CFG->dirroot.'/mnet/lib.php');
5669 $jumpurl = mnet_get_idp_jump_url($user);
5670 $callback = partial('mnet_sso_apply_indirection', $jumpurl);
5672 $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%",
5673 $callback,
5674 $messagetext);
5675 $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%",
5676 $callback,
5677 $messagehtml);
5679 $mail = get_mailer();
5681 if (!empty($mail->SMTPDebug)) {
5682 echo '<pre>' . "\n";
5685 $temprecipients = array();
5686 $tempreplyto = array();
5688 $supportuser = core_user::get_support_user();
5690 // Make up an email address for handling bounces.
5691 if (!empty($CFG->handlebounces)) {
5692 $modargs = 'B'.base64_encode(pack('V', $user->id)).substr(md5($user->email), 0, 16);
5693 $mail->Sender = generate_email_processing_address(0, $modargs);
5694 } else {
5695 $mail->Sender = $supportuser->email;
5698 if (is_string($from)) { // So we can pass whatever we want if there is need.
5699 $mail->From = $CFG->noreplyaddress;
5700 $mail->FromName = $from;
5701 } else if ($usetrueaddress and $from->maildisplay) {
5702 $mail->From = $from->email;
5703 $mail->FromName = fullname($from);
5704 } else {
5705 $mail->From = $CFG->noreplyaddress;
5706 $mail->FromName = fullname($from);
5707 if (empty($replyto)) {
5708 $tempreplyto[] = array($CFG->noreplyaddress, get_string('noreplyname'));
5712 if (!empty($replyto)) {
5713 $tempreplyto[] = array($replyto, $replytoname);
5716 $mail->Subject = substr($subject, 0, 900);
5718 $temprecipients[] = array($user->email, fullname($user));
5720 // Set word wrap.
5721 $mail->WordWrap = $wordwrapwidth;
5723 if (!empty($from->customheaders)) {
5724 // Add custom headers.
5725 if (is_array($from->customheaders)) {
5726 foreach ($from->customheaders as $customheader) {
5727 $mail->addCustomHeader($customheader);
5729 } else {
5730 $mail->addCustomHeader($from->customheaders);
5734 if (!empty($from->priority)) {
5735 $mail->Priority = $from->priority;
5738 if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) {
5739 // Don't ever send HTML to users who don't want it.
5740 $mail->isHTML(true);
5741 $mail->Encoding = 'quoted-printable';
5742 $mail->Body = $messagehtml;
5743 $mail->AltBody = "\n$messagetext\n";
5744 } else {
5745 $mail->IsHTML(false);
5746 $mail->Body = "\n$messagetext\n";
5749 if ($attachment && $attachname) {
5750 if (preg_match( "~\\.\\.~" , $attachment )) {
5751 // Security check for ".." in dir path.
5752 $temprecipients[] = array($supportuser->email, fullname($supportuser, true));
5753 $mail->addStringAttachment('Error in attachment. User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain');
5754 } else {
5755 require_once($CFG->libdir.'/filelib.php');
5756 $mimetype = mimeinfo('type', $attachname);
5757 $mail->addAttachment($CFG->dataroot .'/'. $attachment, $attachname, 'base64', $mimetype);
5761 // Check if the email should be sent in an other charset then the default UTF-8.
5762 if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) {
5764 // Use the defined site mail charset or eventually the one preferred by the recipient.
5765 $charset = $CFG->sitemailcharset;
5766 if (!empty($CFG->allowusermailcharset)) {
5767 if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) {
5768 $charset = $useremailcharset;
5772 // Convert all the necessary strings if the charset is supported.
5773 $charsets = get_list_of_charsets();
5774 unset($charsets['UTF-8']);
5775 if (in_array($charset, $charsets)) {
5776 $mail->CharSet = $charset;
5777 $mail->FromName = core_text::convert($mail->FromName, 'utf-8', strtolower($charset));
5778 $mail->Subject = core_text::convert($mail->Subject, 'utf-8', strtolower($charset));
5779 $mail->Body = core_text::convert($mail->Body, 'utf-8', strtolower($charset));
5780 $mail->AltBody = core_text::convert($mail->AltBody, 'utf-8', strtolower($charset));
5782 foreach ($temprecipients as $key => $values) {
5783 $temprecipients[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
5785 foreach ($tempreplyto as $key => $values) {
5786 $tempreplyto[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
5791 foreach ($temprecipients as $values) {
5792 $mail->addAddress($values[0], $values[1]);
5794 foreach ($tempreplyto as $values) {
5795 $mail->addReplyTo($values[0], $values[1]);
5798 if ($mail->send()) {
5799 set_send_count($user);
5800 if (!empty($mail->SMTPDebug)) {
5801 echo '</pre>';
5803 return true;
5804 } else {
5805 add_to_log(SITEID, 'library', 'mailer', qualified_me(), 'ERROR: '. $mail->ErrorInfo);
5806 if (CLI_SCRIPT) {
5807 mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo);
5809 if (!empty($mail->SMTPDebug)) {
5810 echo '</pre>';
5812 return false;
5817 * Generate a signoff for emails based on support settings
5819 * @return string
5821 function generate_email_signoff() {
5822 global $CFG;
5824 $signoff = "\n";
5825 if (!empty($CFG->supportname)) {
5826 $signoff .= $CFG->supportname."\n";
5828 if (!empty($CFG->supportemail)) {
5829 $signoff .= $CFG->supportemail."\n";
5831 if (!empty($CFG->supportpage)) {
5832 $signoff .= $CFG->supportpage."\n";
5834 return $signoff;
5838 * Sets specified user's password and send the new password to the user via email.
5840 * @param stdClass $user A {@link $USER} object
5841 * @param bool $fasthash If true, use a low cost factor when generating the hash for speed.
5842 * @return bool|string Returns "true" if mail was sent OK and "false" if there was an error
5844 function setnew_password_and_mail($user, $fasthash = false) {
5845 global $CFG, $DB;
5847 // We try to send the mail in language the user understands,
5848 // unfortunately the filter_string() does not support alternative langs yet
5849 // so multilang will not work properly for site->fullname.
5850 $lang = empty($user->lang) ? $CFG->lang : $user->lang;
5852 $site = get_site();
5854 $supportuser = core_user::get_support_user();
5856 $newpassword = generate_password();
5858 $hashedpassword = hash_internal_user_password($newpassword, $fasthash);
5859 $DB->set_field('user', 'password', $hashedpassword, array('id' => $user->id));
5861 $a = new stdClass();
5862 $a->firstname = fullname($user, true);
5863 $a->sitename = format_string($site->fullname);
5864 $a->username = $user->username;
5865 $a->newpassword = $newpassword;
5866 $a->link = $CFG->wwwroot .'/login/';
5867 $a->signoff = generate_email_signoff();
5869 $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang);
5871 $subject = format_string($site->fullname) .': '. (string)new lang_string('newusernewpasswordsubj', '', $a, $lang);
5873 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
5874 return email_to_user($user, $supportuser, $subject, $message);
5879 * Resets specified user's password and send the new password to the user via email.
5881 * @param stdClass $user A {@link $USER} object
5882 * @return bool Returns true if mail was sent OK and false if there was an error.
5884 function reset_password_and_mail($user) {
5885 global $CFG;
5887 $site = get_site();
5888 $supportuser = core_user::get_support_user();
5890 $userauth = get_auth_plugin($user->auth);
5891 if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) {
5892 trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth.");
5893 return false;
5896 $newpassword = generate_password();
5898 if (!$userauth->user_update_password($user, $newpassword)) {
5899 print_error("cannotsetpassword");
5902 $a = new stdClass();
5903 $a->firstname = $user->firstname;
5904 $a->lastname = $user->lastname;
5905 $a->sitename = format_string($site->fullname);
5906 $a->username = $user->username;
5907 $a->newpassword = $newpassword;
5908 $a->link = $CFG->httpswwwroot .'/login/change_password.php';
5909 $a->signoff = generate_email_signoff();
5911 $message = get_string('newpasswordtext', '', $a);
5913 $subject = format_string($site->fullname) .': '. get_string('changedpassword');
5915 unset_user_preference('create_password', $user); // Prevent cron from generating the password.
5917 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
5918 return email_to_user($user, $supportuser, $subject, $message);
5922 * Send email to specified user with confirmation text and activation link.
5924 * @param stdClass $user A {@link $USER} object
5925 * @return bool Returns true if mail was sent OK and false if there was an error.
5927 function send_confirmation_email($user) {
5928 global $CFG;
5930 $site = get_site();
5931 $supportuser = core_user::get_support_user();
5933 $data = new stdClass();
5934 $data->firstname = fullname($user);
5935 $data->sitename = format_string($site->fullname);
5936 $data->admin = generate_email_signoff();
5938 $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname));
5940 $username = urlencode($user->username);
5941 $username = str_replace('.', '%2E', $username); // Prevent problems with trailing dots.
5942 $data->link = $CFG->wwwroot .'/login/confirm.php?data='. $user->secret .'/'. $username;
5943 $message = get_string('emailconfirmation', '', $data);
5944 $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true);
5946 $user->mailformat = 1; // Always send HTML version as well.
5948 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
5949 return email_to_user($user, $supportuser, $subject, $message, $messagehtml);
5953 * Sends a password change confirmation email.
5955 * @param stdClass $user A {@link $USER} object
5956 * @return bool Returns true if mail was sent OK and false if there was an error.
5958 function send_password_change_confirmation_email($user) {
5959 global $CFG;
5961 $site = get_site();
5962 $supportuser = core_user::get_support_user();
5964 $data = new stdClass();
5965 $data->firstname = $user->firstname;
5966 $data->lastname = $user->lastname;
5967 $data->sitename = format_string($site->fullname);
5968 $data->link = $CFG->httpswwwroot .'/login/forgot_password.php?p='. $user->secret .'&s='. urlencode($user->username);
5969 $data->admin = generate_email_signoff();
5971 $message = get_string('emailpasswordconfirmation', '', $data);
5972 $subject = get_string('emailpasswordconfirmationsubject', '', format_string($site->fullname));
5974 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
5975 return email_to_user($user, $supportuser, $subject, $message);
5980 * Sends an email containinginformation on how to change your password.
5982 * @param stdClass $user A {@link $USER} object
5983 * @return bool Returns true if mail was sent OK and false if there was an error.
5985 function send_password_change_info($user) {
5986 global $CFG;
5988 $site = get_site();
5989 $supportuser = core_user::get_support_user();
5990 $systemcontext = context_system::instance();
5992 $data = new stdClass();
5993 $data->firstname = $user->firstname;
5994 $data->lastname = $user->lastname;
5995 $data->sitename = format_string($site->fullname);
5996 $data->admin = generate_email_signoff();
5998 $userauth = get_auth_plugin($user->auth);
6000 if (!is_enabled_auth($user->auth) or $user->auth == 'nologin') {
6001 $message = get_string('emailpasswordchangeinfodisabled', '', $data);
6002 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
6003 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6004 return email_to_user($user, $supportuser, $subject, $message);
6007 if ($userauth->can_change_password() and $userauth->change_password_url()) {
6008 // We have some external url for password changing.
6009 $data->link .= $userauth->change_password_url();
6011 } else {
6012 // No way to change password, sorry.
6013 $data->link = '';
6016 if (!empty($data->link) and has_capability('moodle/user:changeownpassword', $systemcontext, $user->id)) {
6017 $message = get_string('emailpasswordchangeinfo', '', $data);
6018 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
6019 } else {
6020 $message = get_string('emailpasswordchangeinfofail', '', $data);
6021 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
6024 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6025 return email_to_user($user, $supportuser, $subject, $message);
6030 * Check that an email is allowed. It returns an error message if there was a problem.
6032 * @param string $email Content of email
6033 * @return string|false
6035 function email_is_not_allowed($email) {
6036 global $CFG;
6038 if (!empty($CFG->allowemailaddresses)) {
6039 $allowed = explode(' ', $CFG->allowemailaddresses);
6040 foreach ($allowed as $allowedpattern) {
6041 $allowedpattern = trim($allowedpattern);
6042 if (!$allowedpattern) {
6043 continue;
6045 if (strpos($allowedpattern, '.') === 0) {
6046 if (strpos(strrev($email), strrev($allowedpattern)) === 0) {
6047 // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6048 return false;
6051 } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) {
6052 return false;
6055 return get_string('emailonlyallowed', '', $CFG->allowemailaddresses);
6057 } else if (!empty($CFG->denyemailaddresses)) {
6058 $denied = explode(' ', $CFG->denyemailaddresses);
6059 foreach ($denied as $deniedpattern) {
6060 $deniedpattern = trim($deniedpattern);
6061 if (!$deniedpattern) {
6062 continue;
6064 if (strpos($deniedpattern, '.') === 0) {
6065 if (strpos(strrev($email), strrev($deniedpattern)) === 0) {
6066 // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6067 return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6070 } else if (strpos(strrev($email), strrev('@'.$deniedpattern)) === 0) {
6071 return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6076 return false;
6079 // FILE HANDLING.
6082 * Returns local file storage instance
6084 * @return file_storage
6086 function get_file_storage() {
6087 global $CFG;
6089 static $fs = null;
6091 if ($fs) {
6092 return $fs;
6095 require_once("$CFG->libdir/filelib.php");
6097 if (isset($CFG->filedir)) {
6098 $filedir = $CFG->filedir;
6099 } else {
6100 $filedir = $CFG->dataroot.'/filedir';
6103 if (isset($CFG->trashdir)) {
6104 $trashdirdir = $CFG->trashdir;
6105 } else {
6106 $trashdirdir = $CFG->dataroot.'/trashdir';
6109 $fs = new file_storage($filedir, $trashdirdir, "$CFG->tempdir/filestorage", $CFG->directorypermissions, $CFG->filepermissions);
6111 return $fs;
6115 * Returns local file storage instance
6117 * @return file_browser
6119 function get_file_browser() {
6120 global $CFG;
6122 static $fb = null;
6124 if ($fb) {
6125 return $fb;
6128 require_once("$CFG->libdir/filelib.php");
6130 $fb = new file_browser();
6132 return $fb;
6136 * Returns file packer
6138 * @param string $mimetype default application/zip
6139 * @return file_packer
6141 function get_file_packer($mimetype='application/zip') {
6142 global $CFG;
6144 static $fp = array();
6146 if (isset($fp[$mimetype])) {
6147 return $fp[$mimetype];
6150 switch ($mimetype) {
6151 case 'application/zip':
6152 case 'application/vnd.moodle.backup':
6153 case 'application/vnd.moodle.profiling':
6154 $classname = 'zip_packer';
6155 break;
6156 case 'application/x-tar':
6157 // One day we hope to support tar - for the time being it is a pipe dream.
6158 default:
6159 return false;
6162 require_once("$CFG->libdir/filestorage/$classname.php");
6163 $fp[$mimetype] = new $classname();
6165 return $fp[$mimetype];
6169 * Returns current name of file on disk if it exists.
6171 * @param string $newfile File to be verified
6172 * @return string Current name of file on disk if true
6174 function valid_uploaded_file($newfile) {
6175 if (empty($newfile)) {
6176 return '';
6178 if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) {
6179 return $newfile['tmp_name'];
6180 } else {
6181 return '';
6186 * Returns the maximum size for uploading files.
6188 * There are seven possible upload limits:
6189 * 1. in Apache using LimitRequestBody (no way of checking or changing this)
6190 * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
6191 * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
6192 * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
6193 * 5. by the Moodle admin in $CFG->maxbytes
6194 * 6. by the teacher in the current course $course->maxbytes
6195 * 7. by the teacher for the current module, eg $assignment->maxbytes
6197 * These last two are passed to this function as arguments (in bytes).
6198 * Anything defined as 0 is ignored.
6199 * The smallest of all the non-zero numbers is returned.
6201 * @todo Finish documenting this function
6203 * @param int $sitebytes Set maximum size
6204 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6205 * @param int $modulebytes Current module ->maxbytes (in bytes)
6206 * @return int The maximum size for uploading files.
6208 function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0) {
6210 if (! $filesize = ini_get('upload_max_filesize')) {
6211 $filesize = '5M';
6213 $minimumsize = get_real_size($filesize);
6215 if ($postsize = ini_get('post_max_size')) {
6216 $postsize = get_real_size($postsize);
6217 if ($postsize < $minimumsize) {
6218 $minimumsize = $postsize;
6222 if (($sitebytes > 0) and ($sitebytes < $minimumsize)) {
6223 $minimumsize = $sitebytes;
6226 if (($coursebytes > 0) and ($coursebytes < $minimumsize)) {
6227 $minimumsize = $coursebytes;
6230 if (($modulebytes > 0) and ($modulebytes < $minimumsize)) {
6231 $minimumsize = $modulebytes;
6234 return $minimumsize;
6238 * Returns the maximum size for uploading files for the current user
6240 * This function takes in account {@link get_max_upload_file_size()} the user's capabilities
6242 * @param context $context The context in which to check user capabilities
6243 * @param int $sitebytes Set maximum size
6244 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6245 * @param int $modulebytes Current module ->maxbytes (in bytes)
6246 * @param stdClass $user The user
6247 * @return int The maximum size for uploading files.
6249 function get_user_max_upload_file_size($context, $sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $user = null) {
6250 global $USER;
6252 if (empty($user)) {
6253 $user = $USER;
6256 if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) {
6257 return USER_CAN_IGNORE_FILE_SIZE_LIMITS;
6260 return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes);
6264 * Returns an array of possible sizes in local language
6266 * Related to {@link get_max_upload_file_size()} - this function returns an
6267 * array of possible sizes in an array, translated to the
6268 * local language.
6270 * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes.
6272 * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)"
6273 * with the value set to 0. This option will be the first in the list.
6275 * @uses SORT_NUMERIC
6276 * @param int $sitebytes Set maximum size
6277 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6278 * @param int $modulebytes Current module ->maxbytes (in bytes)
6279 * @param int|array $custombytes custom upload size/s which will be added to list,
6280 * Only value/s smaller then maxsize will be added to list.
6281 * @return array
6283 function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null) {
6284 global $CFG;
6286 if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) {
6287 return array();
6290 if ($sitebytes == 0) {
6291 // Will get the minimum of upload_max_filesize or post_max_size.
6292 $sitebytes = get_max_upload_file_size();
6295 $filesize = array();
6296 $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152,
6297 5242880, 10485760, 20971520, 52428800, 104857600);
6299 // If custombytes is given and is valid then add it to the list.
6300 if (is_number($custombytes) and $custombytes > 0) {
6301 $custombytes = (int)$custombytes;
6302 if (!in_array($custombytes, $sizelist)) {
6303 $sizelist[] = $custombytes;
6305 } else if (is_array($custombytes)) {
6306 $sizelist = array_unique(array_merge($sizelist, $custombytes));
6309 // Allow maxbytes to be selected if it falls outside the above boundaries.
6310 if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) {
6311 // Note: get_real_size() is used in order to prevent problems with invalid values.
6312 $sizelist[] = get_real_size($CFG->maxbytes);
6315 foreach ($sizelist as $sizebytes) {
6316 if ($sizebytes < $maxsize && $sizebytes > 0) {
6317 $filesize[(string)intval($sizebytes)] = display_size($sizebytes);
6321 $limitlevel = '';
6322 $displaysize = '';
6323 if ($modulebytes &&
6324 (($modulebytes < $coursebytes || $coursebytes == 0) &&
6325 ($modulebytes < $sitebytes || $sitebytes == 0))) {
6326 $limitlevel = get_string('activity', 'core');
6327 $displaysize = display_size($modulebytes);
6328 $filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list.
6330 } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) {
6331 $limitlevel = get_string('course', 'core');
6332 $displaysize = display_size($coursebytes);
6333 $filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list.
6335 } else if ($sitebytes) {
6336 $limitlevel = get_string('site', 'core');
6337 $displaysize = display_size($sitebytes);
6338 $filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list.
6341 krsort($filesize, SORT_NUMERIC);
6342 if ($limitlevel) {
6343 $params = (object) array('contextname' => $limitlevel, 'displaysize' => $displaysize);
6344 $filesize = array('0' => get_string('uploadlimitwithsize', 'core', $params)) + $filesize;
6347 return $filesize;
6351 * Returns an array with all the filenames in all subdirectories, relative to the given rootdir.
6353 * If excludefiles is defined, then that file/directory is ignored
6354 * If getdirs is true, then (sub)directories are included in the output
6355 * If getfiles is true, then files are included in the output
6356 * (at least one of these must be true!)
6358 * @todo Finish documenting this function. Add examples of $excludefile usage.
6360 * @param string $rootdir A given root directory to start from
6361 * @param string|array $excludefiles If defined then the specified file/directory is ignored
6362 * @param bool $descend If true then subdirectories are recursed as well
6363 * @param bool $getdirs If true then (sub)directories are included in the output
6364 * @param bool $getfiles If true then files are included in the output
6365 * @return array An array with all the filenames in all subdirectories, relative to the given rootdir
6367 function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) {
6369 $dirs = array();
6371 if (!$getdirs and !$getfiles) { // Nothing to show.
6372 return $dirs;
6375 if (!is_dir($rootdir)) { // Must be a directory.
6376 return $dirs;
6379 if (!$dir = opendir($rootdir)) { // Can't open it for some reason.
6380 return $dirs;
6383 if (!is_array($excludefiles)) {
6384 $excludefiles = array($excludefiles);
6387 while (false !== ($file = readdir($dir))) {
6388 $firstchar = substr($file, 0, 1);
6389 if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) {
6390 continue;
6392 $fullfile = $rootdir .'/'. $file;
6393 if (filetype($fullfile) == 'dir') {
6394 if ($getdirs) {
6395 $dirs[] = $file;
6397 if ($descend) {
6398 $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles);
6399 foreach ($subdirs as $subdir) {
6400 $dirs[] = $file .'/'. $subdir;
6403 } else if ($getfiles) {
6404 $dirs[] = $file;
6407 closedir($dir);
6409 asort($dirs);
6411 return $dirs;
6416 * Adds up all the files in a directory and works out the size.
6418 * @param string $rootdir The directory to start from
6419 * @param string $excludefile A file to exclude when summing directory size
6420 * @return int The summed size of all files and subfiles within the root directory
6422 function get_directory_size($rootdir, $excludefile='') {
6423 global $CFG;
6425 // Do it this way if we can, it's much faster.
6426 if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) {
6427 $command = trim($CFG->pathtodu).' -sk '.escapeshellarg($rootdir);
6428 $output = null;
6429 $return = null;
6430 exec($command, $output, $return);
6431 if (is_array($output)) {
6432 // We told it to return k.
6433 return get_real_size(intval($output[0]).'k');
6437 if (!is_dir($rootdir)) {
6438 // Must be a directory.
6439 return 0;
6442 if (!$dir = @opendir($rootdir)) {
6443 // Can't open it for some reason.
6444 return 0;
6447 $size = 0;
6449 while (false !== ($file = readdir($dir))) {
6450 $firstchar = substr($file, 0, 1);
6451 if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) {
6452 continue;
6454 $fullfile = $rootdir .'/'. $file;
6455 if (filetype($fullfile) == 'dir') {
6456 $size += get_directory_size($fullfile, $excludefile);
6457 } else {
6458 $size += filesize($fullfile);
6461 closedir($dir);
6463 return $size;
6467 * Converts bytes into display form
6469 * @static string $gb Localized string for size in gigabytes
6470 * @static string $mb Localized string for size in megabytes
6471 * @static string $kb Localized string for size in kilobytes
6472 * @static string $b Localized string for size in bytes
6473 * @param int $size The size to convert to human readable form
6474 * @return string
6476 function display_size($size) {
6478 static $gb, $mb, $kb, $b;
6480 if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) {
6481 return get_string('unlimited');
6484 if (empty($gb)) {
6485 $gb = get_string('sizegb');
6486 $mb = get_string('sizemb');
6487 $kb = get_string('sizekb');
6488 $b = get_string('sizeb');
6491 if ($size >= 1073741824) {
6492 $size = round($size / 1073741824 * 10) / 10 . $gb;
6493 } else if ($size >= 1048576) {
6494 $size = round($size / 1048576 * 10) / 10 . $mb;
6495 } else if ($size >= 1024) {
6496 $size = round($size / 1024 * 10) / 10 . $kb;
6497 } else {
6498 $size = intval($size) .' '. $b; // File sizes over 2GB can not work in 32bit PHP anyway.
6500 return $size;
6504 * Cleans a given filename by removing suspicious or troublesome characters
6506 * @see clean_param()
6507 * @param string $string file name
6508 * @return string cleaned file name
6510 function clean_filename($string) {
6511 return clean_param($string, PARAM_FILE);
6515 // STRING TRANSLATION.
6518 * Returns the code for the current language
6520 * @category string
6521 * @return string
6523 function current_language() {
6524 global $CFG, $USER, $SESSION, $COURSE;
6526 if (!empty($COURSE->id) and $COURSE->id != SITEID and !empty($COURSE->lang)) {
6527 // Course language can override all other settings for this page.
6528 $return = $COURSE->lang;
6530 } else if (!empty($SESSION->lang)) {
6531 // Session language can override other settings.
6532 $return = $SESSION->lang;
6534 } else if (!empty($USER->lang)) {
6535 $return = $USER->lang;
6537 } else if (isset($CFG->lang)) {
6538 $return = $CFG->lang;
6540 } else {
6541 $return = 'en';
6544 // Just in case this slipped in from somewhere by accident.
6545 $return = str_replace('_utf8', '', $return);
6547 return $return;
6551 * Returns parent language of current active language if defined
6553 * @category string
6554 * @param string $lang null means current language
6555 * @return string
6557 function get_parent_language($lang=null) {
6558 global $COURSE, $SESSION;
6560 // Let's hack around the current language.
6561 if (!empty($lang)) {
6562 $oldcourselang = empty($COURSE->lang) ? '' : $COURSE->lang;
6563 $oldsessionlang = empty($SESSION->lang) ? '' : $SESSION->lang;
6564 $COURSE->lang = '';
6565 $SESSION->lang = $lang;
6568 $parentlang = get_string('parentlanguage', 'langconfig');
6569 if ($parentlang === 'en') {
6570 $parentlang = '';
6573 // Let's hack around the current language.
6574 if (!empty($lang)) {
6575 $COURSE->lang = $oldcourselang;
6576 $SESSION->lang = $oldsessionlang;
6579 return $parentlang;
6583 * Returns current string_manager instance.
6585 * The param $forcereload is needed for CLI installer only where the string_manager instance
6586 * must be replaced during the install.php script life time.
6588 * @category string
6589 * @param bool $forcereload shall the singleton be released and new instance created instead?
6590 * @return core_string_manager
6592 function get_string_manager($forcereload=false) {
6593 global $CFG;
6595 static $singleton = null;
6597 if ($forcereload) {
6598 $singleton = null;
6600 if ($singleton === null) {
6601 if (empty($CFG->early_install_lang)) {
6603 if (empty($CFG->langlist)) {
6604 $translist = array();
6605 } else {
6606 $translist = explode(',', $CFG->langlist);
6609 $singleton = new core_string_manager_standard($CFG->langotherroot, $CFG->langlocalroot, $translist);
6611 } else {
6612 $singleton = new core_string_manager_install();
6616 return $singleton;
6620 * Returns a localized string.
6622 * Returns the translated string specified by $identifier as
6623 * for $module. Uses the same format files as STphp.
6624 * $a is an object, string or number that can be used
6625 * within translation strings
6627 * eg 'hello {$a->firstname} {$a->lastname}'
6628 * or 'hello {$a}'
6630 * If you would like to directly echo the localized string use
6631 * the function {@link print_string()}
6633 * Example usage of this function involves finding the string you would
6634 * like a local equivalent of and using its identifier and module information
6635 * to retrieve it.<br/>
6636 * If you open moodle/lang/en/moodle.php and look near line 278
6637 * you will find a string to prompt a user for their word for 'course'
6638 * <code>
6639 * $string['course'] = 'Course';
6640 * </code>
6641 * So if you want to display the string 'Course'
6642 * in any language that supports it on your site
6643 * you just need to use the identifier 'course'
6644 * <code>
6645 * $mystring = '<strong>'. get_string('course') .'</strong>';
6646 * or
6647 * </code>
6648 * If the string you want is in another file you'd take a slightly
6649 * different approach. Looking in moodle/lang/en/calendar.php you find
6650 * around line 75:
6651 * <code>
6652 * $string['typecourse'] = 'Course event';
6653 * </code>
6654 * If you want to display the string "Course event" in any language
6655 * supported you would use the identifier 'typecourse' and the module 'calendar'
6656 * (because it is in the file calendar.php):
6657 * <code>
6658 * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>';
6659 * </code>
6661 * As a last resort, should the identifier fail to map to a string
6662 * the returned string will be [[ $identifier ]]
6664 * In Moodle 2.3 there is a new argument to this function $lazyload.
6665 * Setting $lazyload to true causes get_string to return a lang_string object
6666 * rather than the string itself. The fetching of the string is then put off until
6667 * the string object is first used. The object can be used by calling it's out
6668 * method or by casting the object to a string, either directly e.g.
6669 * (string)$stringobject
6670 * or indirectly by using the string within another string or echoing it out e.g.
6671 * echo $stringobject
6672 * return "<p>{$stringobject}</p>";
6673 * It is worth noting that using $lazyload and attempting to use the string as an
6674 * array key will cause a fatal error as objects cannot be used as array keys.
6675 * But you should never do that anyway!
6676 * For more information {@link lang_string}
6678 * @category string
6679 * @param string $identifier The key identifier for the localized string
6680 * @param string $component The module where the key identifier is stored,
6681 * usually expressed as the filename in the language pack without the
6682 * .php on the end but can also be written as mod/forum or grade/export/xls.
6683 * If none is specified then moodle.php is used.
6684 * @param string|object|array $a An object, string or number that can be used
6685 * within translation strings
6686 * @param bool $lazyload If set to true a string object is returned instead of
6687 * the string itself. The string then isn't calculated until it is first used.
6688 * @return string The localized string.
6689 * @throws coding_exception
6691 function get_string($identifier, $component = '', $a = null, $lazyload = false) {
6692 global $CFG;
6694 // If the lazy load argument has been supplied return a lang_string object
6695 // instead.
6696 // We need to make sure it is true (and a bool) as you will see below there
6697 // used to be a forth argument at one point.
6698 if ($lazyload === true) {
6699 return new lang_string($identifier, $component, $a);
6702 if ($CFG->debugdeveloper && clean_param($identifier, PARAM_STRINGID) === '') {
6703 throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.', DEBUG_DEVELOPER);
6706 // There is now a forth argument again, this time it is a boolean however so
6707 // we can still check for the old extralocations parameter.
6708 if (!is_bool($lazyload) && !empty($lazyload)) {
6709 debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.');
6712 if (strpos($component, '/') !== false) {
6713 debugging('The module name you passed to get_string is the deprecated format ' .
6714 'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER);
6715 $componentpath = explode('/', $component);
6717 switch ($componentpath[0]) {
6718 case 'mod':
6719 $component = $componentpath[1];
6720 break;
6721 case 'blocks':
6722 case 'block':
6723 $component = 'block_'.$componentpath[1];
6724 break;
6725 case 'enrol':
6726 $component = 'enrol_'.$componentpath[1];
6727 break;
6728 case 'format':
6729 $component = 'format_'.$componentpath[1];
6730 break;
6731 case 'grade':
6732 $component = 'grade'.$componentpath[1].'_'.$componentpath[2];
6733 break;
6737 $result = get_string_manager()->get_string($identifier, $component, $a);
6739 // Debugging feature lets you display string identifier and component.
6740 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
6741 $result .= ' {' . $identifier . '/' . $component . '}';
6743 return $result;
6747 * Converts an array of strings to their localized value.
6749 * @param array $array An array of strings
6750 * @param string $component The language module that these strings can be found in.
6751 * @return stdClass translated strings.
6753 function get_strings($array, $component = '') {
6754 $string = new stdClass;
6755 foreach ($array as $item) {
6756 $string->$item = get_string($item, $component);
6758 return $string;
6762 * Prints out a translated string.
6764 * Prints out a translated string using the return value from the {@link get_string()} function.
6766 * Example usage of this function when the string is in the moodle.php file:<br/>
6767 * <code>
6768 * echo '<strong>';
6769 * print_string('course');
6770 * echo '</strong>';
6771 * </code>
6773 * Example usage of this function when the string is not in the moodle.php file:<br/>
6774 * <code>
6775 * echo '<h1>';
6776 * print_string('typecourse', 'calendar');
6777 * echo '</h1>';
6778 * </code>
6780 * @category string
6781 * @param string $identifier The key identifier for the localized string
6782 * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used.
6783 * @param string|object|array $a An object, string or number that can be used within translation strings
6785 function print_string($identifier, $component = '', $a = null) {
6786 echo get_string($identifier, $component, $a);
6790 * Returns a list of charset codes
6792 * Returns a list of charset codes. It's hardcoded, so they should be added manually
6793 * (checking that such charset is supported by the texlib library!)
6795 * @return array And associative array with contents in the form of charset => charset
6797 function get_list_of_charsets() {
6799 $charsets = array(
6800 'EUC-JP' => 'EUC-JP',
6801 'ISO-2022-JP'=> 'ISO-2022-JP',
6802 'ISO-8859-1' => 'ISO-8859-1',
6803 'SHIFT-JIS' => 'SHIFT-JIS',
6804 'GB2312' => 'GB2312',
6805 'GB18030' => 'GB18030', // GB18030 not supported by typo and mbstring.
6806 'UTF-8' => 'UTF-8');
6808 asort($charsets);
6810 return $charsets;
6814 * Returns a list of valid and compatible themes
6816 * @return array
6818 function get_list_of_themes() {
6819 global $CFG;
6821 $themes = array();
6823 if (!empty($CFG->themelist)) { // Use admin's list of themes.
6824 $themelist = explode(',', $CFG->themelist);
6825 } else {
6826 $themelist = array_keys(core_component::get_plugin_list("theme"));
6829 foreach ($themelist as $key => $themename) {
6830 $theme = theme_config::load($themename);
6831 $themes[$themename] = $theme;
6834 core_collator::asort_objects_by_method($themes, 'get_theme_name');
6836 return $themes;
6840 * Returns a list of timezones in the current language
6842 * @return array
6844 function get_list_of_timezones() {
6845 global $DB;
6847 static $timezones;
6849 if (!empty($timezones)) { // This function has been called recently.
6850 return $timezones;
6853 $timezones = array();
6855 if ($rawtimezones = $DB->get_records_sql("SELECT MAX(id), name FROM {timezone} GROUP BY name")) {
6856 foreach ($rawtimezones as $timezone) {
6857 if (!empty($timezone->name)) {
6858 if (get_string_manager()->string_exists(strtolower($timezone->name), 'timezones')) {
6859 $timezones[$timezone->name] = get_string(strtolower($timezone->name), 'timezones');
6860 } else {
6861 $timezones[$timezone->name] = $timezone->name;
6863 if (substr($timezones[$timezone->name], 0, 1) == '[') { // No translation found.
6864 $timezones[$timezone->name] = $timezone->name;
6870 asort($timezones);
6872 for ($i = -13; $i <= 13; $i += .5) {
6873 $tzstring = 'UTC';
6874 if ($i < 0) {
6875 $timezones[sprintf("%.1f", $i)] = $tzstring . $i;
6876 } else if ($i > 0) {
6877 $timezones[sprintf("%.1f", $i)] = $tzstring . '+' . $i;
6878 } else {
6879 $timezones[sprintf("%.1f", $i)] = $tzstring;
6883 return $timezones;
6887 * Factory function for emoticon_manager
6889 * @return emoticon_manager singleton
6891 function get_emoticon_manager() {
6892 static $singleton = null;
6894 if (is_null($singleton)) {
6895 $singleton = new emoticon_manager();
6898 return $singleton;
6902 * Provides core support for plugins that have to deal with emoticons (like HTML editor or emoticon filter).
6904 * Whenever this manager mentiones 'emoticon object', the following data
6905 * structure is expected: stdClass with properties text, imagename, imagecomponent,
6906 * altidentifier and altcomponent
6908 * @see admin_setting_emoticons
6910 * @copyright 2010 David Mudrak
6911 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6913 class emoticon_manager {
6916 * Returns the currently enabled emoticons
6918 * @return array of emoticon objects
6920 public function get_emoticons() {
6921 global $CFG;
6923 if (empty($CFG->emoticons)) {
6924 return array();
6927 $emoticons = $this->decode_stored_config($CFG->emoticons);
6929 if (!is_array($emoticons)) {
6930 // Something is wrong with the format of stored setting.
6931 debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL);
6932 return array();
6935 return $emoticons;
6939 * Converts emoticon object into renderable pix_emoticon object
6941 * @param stdClass $emoticon emoticon object
6942 * @param array $attributes explicit HTML attributes to set
6943 * @return pix_emoticon
6945 public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array()) {
6946 $stringmanager = get_string_manager();
6947 if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) {
6948 $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent);
6949 } else {
6950 $alt = s($emoticon->text);
6952 return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes);
6956 * Encodes the array of emoticon objects into a string storable in config table
6958 * @see self::decode_stored_config()
6959 * @param array $emoticons array of emtocion objects
6960 * @return string
6962 public function encode_stored_config(array $emoticons) {
6963 return json_encode($emoticons);
6967 * Decodes the string into an array of emoticon objects
6969 * @see self::encode_stored_config()
6970 * @param string $encoded
6971 * @return string|null
6973 public function decode_stored_config($encoded) {
6974 $decoded = json_decode($encoded);
6975 if (!is_array($decoded)) {
6976 return null;
6978 return $decoded;
6982 * Returns default set of emoticons supported by Moodle
6984 * @return array of sdtClasses
6986 public function default_emoticons() {
6987 return array(
6988 $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'),
6989 $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'),
6990 $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'),
6991 $this->prepare_emoticon_object(";-)", 's/wink', 'wink'),
6992 $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'),
6993 $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'),
6994 $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'),
6995 $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'),
6996 $this->prepare_emoticon_object("B-)", 's/cool', 'cool'),
6997 $this->prepare_emoticon_object("^-)", 's/approve', 'approve'),
6998 $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'),
6999 $this->prepare_emoticon_object(":o)", 's/clown', 'clown'),
7000 $this->prepare_emoticon_object(":-(", 's/sad', 'sad'),
7001 $this->prepare_emoticon_object(":(", 's/sad', 'sad'),
7002 $this->prepare_emoticon_object("8-.", 's/shy', 'shy'),
7003 $this->prepare_emoticon_object(":-I", 's/blush', 'blush'),
7004 $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'),
7005 $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'),
7006 $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'),
7007 $this->prepare_emoticon_object("8-[", 's/angry', 'angry'),
7008 $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'),
7009 $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'),
7010 $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'),
7011 $this->prepare_emoticon_object("}-]", 's/evil', 'evil'),
7012 $this->prepare_emoticon_object("(h)", 's/heart', 'heart'),
7013 $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'),
7014 $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'),
7015 $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'),
7016 $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'),
7017 $this->prepare_emoticon_object("( )", 's/egg', 'egg'),
7022 * Helper method preparing the stdClass with the emoticon properties
7024 * @param string|array $text or array of strings
7025 * @param string $imagename to be used by {@link pix_emoticon}
7026 * @param string $altidentifier alternative string identifier, null for no alt
7027 * @param string $altcomponent where the alternative string is defined
7028 * @param string $imagecomponent to be used by {@link pix_emoticon}
7029 * @return stdClass
7031 protected function prepare_emoticon_object($text, $imagename, $altidentifier = null,
7032 $altcomponent = 'core_pix', $imagecomponent = 'core') {
7033 return (object)array(
7034 'text' => $text,
7035 'imagename' => $imagename,
7036 'imagecomponent' => $imagecomponent,
7037 'altidentifier' => $altidentifier,
7038 'altcomponent' => $altcomponent,
7043 // ENCRYPTION.
7046 * rc4encrypt
7048 * @param string $data Data to encrypt.
7049 * @return string The now encrypted data.
7051 function rc4encrypt($data) {
7052 return endecrypt(get_site_identifier(), $data, '');
7056 * rc4decrypt
7058 * @param string $data Data to decrypt.
7059 * @return string The now decrypted data.
7061 function rc4decrypt($data) {
7062 return endecrypt(get_site_identifier(), $data, 'de');
7066 * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com]
7068 * @todo Finish documenting this function
7070 * @param string $pwd The password to use when encrypting or decrypting
7071 * @param string $data The data to be decrypted/encrypted
7072 * @param string $case Either 'de' for decrypt or '' for encrypt
7073 * @return string
7075 function endecrypt ($pwd, $data, $case) {
7077 if ($case == 'de') {
7078 $data = urldecode($data);
7081 $key[] = '';
7082 $box[] = '';
7083 $pwdlength = strlen($pwd);
7085 for ($i = 0; $i <= 255; $i++) {
7086 $key[$i] = ord(substr($pwd, ($i % $pwdlength), 1));
7087 $box[$i] = $i;
7090 $x = 0;
7092 for ($i = 0; $i <= 255; $i++) {
7093 $x = ($x + $box[$i] + $key[$i]) % 256;
7094 $tempswap = $box[$i];
7095 $box[$i] = $box[$x];
7096 $box[$x] = $tempswap;
7099 $cipher = '';
7101 $a = 0;
7102 $j = 0;
7104 for ($i = 0; $i < strlen($data); $i++) {
7105 $a = ($a + 1) % 256;
7106 $j = ($j + $box[$a]) % 256;
7107 $temp = $box[$a];
7108 $box[$a] = $box[$j];
7109 $box[$j] = $temp;
7110 $k = $box[(($box[$a] + $box[$j]) % 256)];
7111 $cipherby = ord(substr($data, $i, 1)) ^ $k;
7112 $cipher .= chr($cipherby);
7115 if ($case == 'de') {
7116 $cipher = urldecode(urlencode($cipher));
7117 } else {
7118 $cipher = urlencode($cipher);
7121 return $cipher;
7124 // ENVIRONMENT CHECKING.
7127 * This method validates a plug name. It is much faster than calling clean_param.
7129 * @param string $name a string that might be a plugin name.
7130 * @return bool if this string is a valid plugin name.
7132 function is_valid_plugin_name($name) {
7133 // This does not work for 'mod', bad luck, use any other type.
7134 return core_component::is_valid_plugin_name('tool', $name);
7138 * Get a list of all the plugins of a given type that define a certain API function
7139 * in a certain file. The plugin component names and function names are returned.
7141 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
7142 * @param string $function the part of the name of the function after the
7143 * frankenstyle prefix. e.g 'hook' if you are looking for functions with
7144 * names like report_courselist_hook.
7145 * @param string $file the name of file within the plugin that defines the
7146 * function. Defaults to lib.php.
7147 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
7148 * and the function names as values (e.g. 'report_courselist_hook', 'forum_hook').
7150 function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php') {
7151 $pluginfunctions = array();
7152 $pluginswithfile = core_component::get_plugin_list_with_file($plugintype, $file, true);
7153 foreach ($pluginswithfile as $plugin => $notused) {
7154 $fullfunction = $plugintype . '_' . $plugin . '_' . $function;
7156 if (function_exists($fullfunction)) {
7157 // Function exists with standard name. Store, indexed by frankenstyle name of plugin.
7158 $pluginfunctions[$plugintype . '_' . $plugin] = $fullfunction;
7160 } else if ($plugintype === 'mod') {
7161 // For modules, we also allow plugin without full frankenstyle but just starting with the module name.
7162 $shortfunction = $plugin . '_' . $function;
7163 if (function_exists($shortfunction)) {
7164 $pluginfunctions[$plugintype . '_' . $plugin] = $shortfunction;
7168 return $pluginfunctions;
7172 * Lists plugin-like directories within specified directory
7174 * This function was originally used for standard Moodle plugins, please use
7175 * new core_component::get_plugin_list() now.
7177 * This function is used for general directory listing and backwards compatility.
7179 * @param string $directory relative directory from root
7180 * @param string $exclude dir name to exclude from the list (defaults to none)
7181 * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot)
7182 * @return array Sorted array of directory names found under the requested parameters
7184 function get_list_of_plugins($directory='mod', $exclude='', $basedir='') {
7185 global $CFG;
7187 $plugins = array();
7189 if (empty($basedir)) {
7190 $basedir = $CFG->dirroot .'/'. $directory;
7192 } else {
7193 $basedir = $basedir .'/'. $directory;
7196 if ($CFG->debugdeveloper and empty($exclude)) {
7197 // Make sure devs do not use this to list normal plugins,
7198 // this is intended for general directories that are not plugins!
7200 $subtypes = core_component::get_plugin_types();
7201 if (in_array($basedir, $subtypes)) {
7202 debugging('get_list_of_plugins() should not be used to list real plugins, use core_component::get_plugin_list() instead!', DEBUG_DEVELOPER);
7204 unset($subtypes);
7207 if (file_exists($basedir) && filetype($basedir) == 'dir') {
7208 if (!$dirhandle = opendir($basedir)) {
7209 debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER);
7210 return array();
7212 while (false !== ($dir = readdir($dirhandle))) {
7213 // Func: strpos is marginally but reliably faster than substr($dir, 0, 1).
7214 if (strpos($dir, '.') === 0 or $dir === 'CVS' or $dir === '_vti_cnf' or $dir === 'simpletest' or $dir === 'yui' or
7215 $dir === 'tests' or $dir === 'classes' or $dir === $exclude) {
7216 continue;
7218 if (filetype($basedir .'/'. $dir) != 'dir') {
7219 continue;
7221 $plugins[] = $dir;
7223 closedir($dirhandle);
7225 if ($plugins) {
7226 asort($plugins);
7228 return $plugins;
7232 * Invoke plugin's callback functions
7234 * @param string $type plugin type e.g. 'mod'
7235 * @param string $name plugin name
7236 * @param string $feature feature name
7237 * @param string $action feature's action
7238 * @param array $params parameters of callback function, should be an array
7239 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
7240 * @return mixed
7242 * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743
7244 function plugin_callback($type, $name, $feature, $action, $params = null, $default = null) {
7245 return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default);
7249 * Invoke component's callback functions
7251 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
7252 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
7253 * @param array $params parameters of callback function
7254 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
7255 * @return mixed
7257 function component_callback($component, $function, array $params = array(), $default = null) {
7259 $functionname = component_callback_exists($component, $function);
7261 if ($functionname) {
7262 // Function exists, so just return function result.
7263 $ret = call_user_func_array($functionname, $params);
7264 if (is_null($ret)) {
7265 return $default;
7266 } else {
7267 return $ret;
7270 return $default;
7274 * Determine if a component callback exists and return the function name to call. Note that this
7275 * function will include the required library files so that the functioname returned can be
7276 * called directly.
7278 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
7279 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
7280 * @return mixed Complete function name to call if the callback exists or false if it doesn't.
7281 * @throws coding_exception if invalid component specfied
7283 function component_callback_exists($component, $function) {
7284 global $CFG; // This is needed for the inclusions.
7286 $cleancomponent = clean_param($component, PARAM_COMPONENT);
7287 if (empty($cleancomponent)) {
7288 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
7290 $component = $cleancomponent;
7292 list($type, $name) = core_component::normalize_component($component);
7293 $component = $type . '_' . $name;
7295 $oldfunction = $name.'_'.$function;
7296 $function = $component.'_'.$function;
7298 $dir = core_component::get_component_directory($component);
7299 if (empty($dir)) {
7300 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
7303 // Load library and look for function.
7304 if (file_exists($dir.'/lib.php')) {
7305 require_once($dir.'/lib.php');
7308 if (!function_exists($function) and function_exists($oldfunction)) {
7309 if ($type !== 'mod' and $type !== 'core') {
7310 debugging("Please use new function name $function instead of legacy $oldfunction", DEBUG_DEVELOPER);
7312 $function = $oldfunction;
7315 if (function_exists($function)) {
7316 return $function;
7318 return false;
7322 * Checks whether a plugin supports a specified feature.
7324 * @param string $type Plugin type e.g. 'mod'
7325 * @param string $name Plugin name e.g. 'forum'
7326 * @param string $feature Feature code (FEATURE_xx constant)
7327 * @param mixed $default default value if feature support unknown
7328 * @return mixed Feature result (false if not supported, null if feature is unknown,
7329 * otherwise usually true but may have other feature-specific value such as array)
7330 * @throws coding_exception
7332 function plugin_supports($type, $name, $feature, $default = null) {
7333 global $CFG;
7335 if ($type === 'mod' and $name === 'NEWMODULE') {
7336 // Somebody forgot to rename the module template.
7337 return false;
7340 $component = clean_param($type . '_' . $name, PARAM_COMPONENT);
7341 if (empty($component)) {
7342 throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name);
7345 $function = null;
7347 if ($type === 'mod') {
7348 // We need this special case because we support subplugins in modules,
7349 // otherwise it would end up in infinite loop.
7350 if (file_exists("$CFG->dirroot/mod/$name/lib.php")) {
7351 include_once("$CFG->dirroot/mod/$name/lib.php");
7352 $function = $component.'_supports';
7353 if (!function_exists($function)) {
7354 // Legacy non-frankenstyle function name.
7355 $function = $name.'_supports';
7359 } else {
7360 if (!$path = core_component::get_plugin_directory($type, $name)) {
7361 // Non existent plugin type.
7362 return false;
7364 if (file_exists("$path/lib.php")) {
7365 include_once("$path/lib.php");
7366 $function = $component.'_supports';
7370 if ($function and function_exists($function)) {
7371 $supports = $function($feature);
7372 if (is_null($supports)) {
7373 // Plugin does not know - use default.
7374 return $default;
7375 } else {
7376 return $supports;
7380 // Plugin does not care, so use default.
7381 return $default;
7385 * Returns true if the current version of PHP is greater that the specified one.
7387 * @todo Check PHP version being required here is it too low?
7389 * @param string $version The version of php being tested.
7390 * @return bool
7392 function check_php_version($version='5.2.4') {
7393 return (version_compare(phpversion(), $version) >= 0);
7397 * Determine if moodle installation requires update.
7399 * Checks version numbers of main code and all plugins to see
7400 * if there are any mismatches.
7402 * @return bool
7404 function moodle_needs_upgrading() {
7405 global $CFG;
7407 if (empty($CFG->version)) {
7408 return true;
7411 // There is no need to purge plugininfo caches here because
7412 // these caches are not used during upgrade and they are purged after
7413 // every upgrade.
7415 if (empty($CFG->allversionshash)) {
7416 return true;
7419 $hash = core_component::get_all_versions_hash();
7421 return ($hash !== $CFG->allversionshash);
7425 * Returns the major version of this site
7427 * Moodle version numbers consist of three numbers separated by a dot, for
7428 * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so
7429 * called major version. This function extracts the major version from either
7430 * $CFG->release (default) or eventually from the $release variable defined in
7431 * the main version.php.
7433 * @param bool $fromdisk should the version if source code files be used
7434 * @return string|false the major version like '2.3', false if could not be determined
7436 function moodle_major_version($fromdisk = false) {
7437 global $CFG;
7439 if ($fromdisk) {
7440 $release = null;
7441 require($CFG->dirroot.'/version.php');
7442 if (empty($release)) {
7443 return false;
7446 } else {
7447 if (empty($CFG->release)) {
7448 return false;
7450 $release = $CFG->release;
7453 if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) {
7454 return $matches[0];
7455 } else {
7456 return false;
7460 // MISCELLANEOUS.
7463 * Sets the system locale
7465 * @category string
7466 * @param string $locale Can be used to force a locale
7468 function moodle_setlocale($locale='') {
7469 global $CFG;
7471 static $currentlocale = ''; // Last locale caching.
7473 $oldlocale = $currentlocale;
7475 // Fetch the correct locale based on ostype.
7476 if ($CFG->ostype == 'WINDOWS') {
7477 $stringtofetch = 'localewin';
7478 } else {
7479 $stringtofetch = 'locale';
7482 // The priority is the same as in get_string() - parameter, config, course, session, user, global language.
7483 if (!empty($locale)) {
7484 $currentlocale = $locale;
7485 } else if (!empty($CFG->locale)) { // Override locale for all language packs.
7486 $currentlocale = $CFG->locale;
7487 } else {
7488 $currentlocale = get_string($stringtofetch, 'langconfig');
7491 // Do nothing if locale already set up.
7492 if ($oldlocale == $currentlocale) {
7493 return;
7496 // Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values,
7497 // set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7
7498 // Some day, numeric, monetary and other categories should be set too, I think. :-/.
7500 // Get current values.
7501 $monetary= setlocale (LC_MONETARY, 0);
7502 $numeric = setlocale (LC_NUMERIC, 0);
7503 $ctype = setlocale (LC_CTYPE, 0);
7504 if ($CFG->ostype != 'WINDOWS') {
7505 $messages= setlocale (LC_MESSAGES, 0);
7507 // Set locale to all.
7508 setlocale (LC_ALL, $currentlocale);
7509 // Set old values.
7510 setlocale (LC_MONETARY, $monetary);
7511 setlocale (LC_NUMERIC, $numeric);
7512 if ($CFG->ostype != 'WINDOWS') {
7513 setlocale (LC_MESSAGES, $messages);
7515 if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') {
7516 // To workaround a well-known PHP problem with Turkish letter Ii.
7517 setlocale (LC_CTYPE, $ctype);
7522 * Count words in a string.
7524 * Words are defined as things between whitespace.
7526 * @category string
7527 * @param string $string The text to be searched for words.
7528 * @return int The count of words in the specified string
7530 function count_words($string) {
7531 $string = strip_tags($string);
7532 return count(preg_split("/\w\b/", $string)) - 1;
7536 * Count letters in a string.
7538 * Letters are defined as chars not in tags and different from whitespace.
7540 * @category string
7541 * @param string $string The text to be searched for letters.
7542 * @return int The count of letters in the specified text.
7544 function count_letters($string) {
7545 $string = strip_tags($string); // Tags are out now.
7546 $string = preg_replace('/[[:space:]]*/', '', $string); // Whitespace are out now.
7548 return core_text::strlen($string);
7552 * Generate and return a random string of the specified length.
7554 * @param int $length The length of the string to be created.
7555 * @return string
7557 function random_string ($length=15) {
7558 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
7559 $pool .= 'abcdefghijklmnopqrstuvwxyz';
7560 $pool .= '0123456789';
7561 $poollen = strlen($pool);
7562 mt_srand ((double) microtime() * 1000000);
7563 $string = '';
7564 for ($i = 0; $i < $length; $i++) {
7565 $string .= substr($pool, (mt_rand()%($poollen)), 1);
7567 return $string;
7571 * Generate a complex random string (useful for md5 salts)
7573 * This function is based on the above {@link random_string()} however it uses a
7574 * larger pool of characters and generates a string between 24 and 32 characters
7576 * @param int $length Optional if set generates a string to exactly this length
7577 * @return string
7579 function complex_random_string($length=null) {
7580 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
7581 $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
7582 $poollen = strlen($pool);
7583 mt_srand ((double) microtime() * 1000000);
7584 if ($length===null) {
7585 $length = floor(rand(24, 32));
7587 $string = '';
7588 for ($i = 0; $i < $length; $i++) {
7589 $string .= $pool[(mt_rand()%$poollen)];
7591 return $string;
7595 * Given some text (which may contain HTML) and an ideal length,
7596 * this function truncates the text neatly on a word boundary if possible
7598 * @category string
7599 * @param string $text text to be shortened
7600 * @param int $ideal ideal string length
7601 * @param boolean $exact if false, $text will not be cut mid-word
7602 * @param string $ending The string to append if the passed string is truncated
7603 * @return string $truncate shortened string
7605 function shorten_text($text, $ideal=30, $exact = false, $ending='...') {
7606 // If the plain text is shorter than the maximum length, return the whole text.
7607 if (core_text::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) {
7608 return $text;
7611 // Splits on HTML tags. Each open/close/empty tag will be the first thing
7612 // and only tag in its 'line'.
7613 preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
7615 $totallength = core_text::strlen($ending);
7616 $truncate = '';
7618 // This array stores information about open and close tags and their position
7619 // in the truncated string. Each item in the array is an object with fields
7620 // ->open (true if open), ->tag (tag name in lower case), and ->pos
7621 // (byte position in truncated text).
7622 $tagdetails = array();
7624 foreach ($lines as $linematchings) {
7625 // If there is any html-tag in this line, handle it and add it (uncounted) to the output.
7626 if (!empty($linematchings[1])) {
7627 // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>).
7628 if (!preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $linematchings[1])) {
7629 if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $linematchings[1], $tagmatchings)) {
7630 // Record closing tag.
7631 $tagdetails[] = (object) array(
7632 'open' => false,
7633 'tag' => core_text::strtolower($tagmatchings[1]),
7634 'pos' => core_text::strlen($truncate),
7637 } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $linematchings[1], $tagmatchings)) {
7638 // Record opening tag.
7639 $tagdetails[] = (object) array(
7640 'open' => true,
7641 'tag' => core_text::strtolower($tagmatchings[1]),
7642 'pos' => core_text::strlen($truncate),
7646 // Add html-tag to $truncate'd text.
7647 $truncate .= $linematchings[1];
7650 // Calculate the length of the plain text part of the line; handle entities as one character.
7651 $contentlength = core_text::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $linematchings[2]));
7652 if ($totallength + $contentlength > $ideal) {
7653 // The number of characters which are left.
7654 $left = $ideal - $totallength;
7655 $entitieslength = 0;
7656 // Search for html entities.
7657 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)) {
7658 // Calculate the real length of all entities in the legal range.
7659 foreach ($entities[0] as $entity) {
7660 if ($entity[1]+1-$entitieslength <= $left) {
7661 $left--;
7662 $entitieslength += core_text::strlen($entity[0]);
7663 } else {
7664 // No more characters left.
7665 break;
7669 $breakpos = $left + $entitieslength;
7671 // If the words shouldn't be cut in the middle...
7672 if (!$exact) {
7673 // Search the last occurence of a space.
7674 for (; $breakpos > 0; $breakpos--) {
7675 if ($char = core_text::substr($linematchings[2], $breakpos, 1)) {
7676 if ($char === '.' or $char === ' ') {
7677 $breakpos += 1;
7678 break;
7679 } else if (strlen($char) > 2) {
7680 // Chinese/Japanese/Korean text can be truncated at any UTF-8 character boundary.
7681 $breakpos += 1;
7682 break;
7687 if ($breakpos == 0) {
7688 // This deals with the test_shorten_text_no_spaces case.
7689 $breakpos = $left + $entitieslength;
7690 } else if ($breakpos > $left + $entitieslength) {
7691 // This deals with the previous for loop breaking on the first char.
7692 $breakpos = $left + $entitieslength;
7695 $truncate .= core_text::substr($linematchings[2], 0, $breakpos);
7696 // Maximum length is reached, so get off the loop.
7697 break;
7698 } else {
7699 $truncate .= $linematchings[2];
7700 $totallength += $contentlength;
7703 // If the maximum length is reached, get off the loop.
7704 if ($totallength >= $ideal) {
7705 break;
7709 // Add the defined ending to the text.
7710 $truncate .= $ending;
7712 // Now calculate the list of open html tags based on the truncate position.
7713 $opentags = array();
7714 foreach ($tagdetails as $taginfo) {
7715 if ($taginfo->open) {
7716 // Add tag to the beginning of $opentags list.
7717 array_unshift($opentags, $taginfo->tag);
7718 } else {
7719 // Can have multiple exact same open tags, close the last one.
7720 $pos = array_search($taginfo->tag, array_reverse($opentags, true));
7721 if ($pos !== false) {
7722 unset($opentags[$pos]);
7727 // Close all unclosed html-tags.
7728 foreach ($opentags as $tag) {
7729 $truncate .= '</' . $tag . '>';
7732 return $truncate;
7737 * Given dates in seconds, how many weeks is the date from startdate
7738 * The first week is 1, the second 2 etc ...
7740 * @param int $startdate Timestamp for the start date
7741 * @param int $thedate Timestamp for the end date
7742 * @return string
7744 function getweek ($startdate, $thedate) {
7745 if ($thedate < $startdate) {
7746 return 0;
7749 return floor(($thedate - $startdate) / WEEKSECS) + 1;
7753 * Returns a randomly generated password of length $maxlen. inspired by
7755 * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and
7756 * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254}
7758 * @param int $maxlen The maximum size of the password being generated.
7759 * @return string
7761 function generate_password($maxlen=10) {
7762 global $CFG;
7764 if (empty($CFG->passwordpolicy)) {
7765 $fillers = PASSWORD_DIGITS;
7766 $wordlist = file($CFG->wordlist);
7767 $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]);
7768 $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]);
7769 $filler1 = $fillers[rand(0, strlen($fillers) - 1)];
7770 $password = $word1 . $filler1 . $word2;
7771 } else {
7772 $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0;
7773 $digits = $CFG->minpassworddigits;
7774 $lower = $CFG->minpasswordlower;
7775 $upper = $CFG->minpasswordupper;
7776 $nonalphanum = $CFG->minpasswordnonalphanum;
7777 $total = $lower + $upper + $digits + $nonalphanum;
7778 // Var minlength should be the greater one of the two ( $minlen and $total ).
7779 $minlen = $minlen < $total ? $total : $minlen;
7780 // Var maxlen can never be smaller than minlen.
7781 $maxlen = $minlen > $maxlen ? $minlen : $maxlen;
7782 $additional = $maxlen - $total;
7784 // Make sure we have enough characters to fulfill
7785 // complexity requirements.
7786 $passworddigits = PASSWORD_DIGITS;
7787 while ($digits > strlen($passworddigits)) {
7788 $passworddigits .= PASSWORD_DIGITS;
7790 $passwordlower = PASSWORD_LOWER;
7791 while ($lower > strlen($passwordlower)) {
7792 $passwordlower .= PASSWORD_LOWER;
7794 $passwordupper = PASSWORD_UPPER;
7795 while ($upper > strlen($passwordupper)) {
7796 $passwordupper .= PASSWORD_UPPER;
7798 $passwordnonalphanum = PASSWORD_NONALPHANUM;
7799 while ($nonalphanum > strlen($passwordnonalphanum)) {
7800 $passwordnonalphanum .= PASSWORD_NONALPHANUM;
7803 // Now mix and shuffle it all.
7804 $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) .
7805 substr(str_shuffle ($passwordupper), 0, $upper) .
7806 substr(str_shuffle ($passworddigits), 0, $digits) .
7807 substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) .
7808 substr(str_shuffle ($passwordlower .
7809 $passwordupper .
7810 $passworddigits .
7811 $passwordnonalphanum), 0 , $additional));
7814 return substr ($password, 0, $maxlen);
7818 * Given a float, prints it nicely.
7819 * Localized floats must not be used in calculations!
7821 * The stripzeros feature is intended for making numbers look nicer in small
7822 * areas where it is not necessary to indicate the degree of accuracy by showing
7823 * ending zeros. If you turn it on with $decimalpoints set to 3, for example,
7824 * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'.
7826 * @param float $float The float to print
7827 * @param int $decimalpoints The number of decimal places to print.
7828 * @param bool $localized use localized decimal separator
7829 * @param bool $stripzeros If true, removes final zeros after decimal point
7830 * @return string locale float
7832 function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=false) {
7833 if (is_null($float)) {
7834 return '';
7836 if ($localized) {
7837 $separator = get_string('decsep', 'langconfig');
7838 } else {
7839 $separator = '.';
7841 $result = number_format($float, $decimalpoints, $separator, '');
7842 if ($stripzeros) {
7843 // Remove zeros and final dot if not needed.
7844 $result = preg_replace('~(' . preg_quote($separator) . ')?0+$~', '', $result);
7846 return $result;
7850 * Converts locale specific floating point/comma number back to standard PHP float value
7851 * Do NOT try to do any math operations before this conversion on any user submitted floats!
7853 * @param string $localefloat locale aware float representation
7854 * @param bool $strict If true, then check the input and return false if it is not a valid number.
7855 * @return mixed float|bool - false or the parsed float.
7857 function unformat_float($localefloat, $strict = false) {
7858 $localefloat = trim($localefloat);
7860 if ($localefloat == '') {
7861 return null;
7864 $localefloat = str_replace(' ', '', $localefloat); // No spaces - those might be used as thousand separators.
7865 $localefloat = str_replace(get_string('decsep', 'langconfig'), '.', $localefloat);
7867 if ($strict && !is_numeric($localefloat)) {
7868 return false;
7871 return (float)$localefloat;
7875 * Given a simple array, this shuffles it up just like shuffle()
7876 * Unlike PHP's shuffle() this function works on any machine.
7878 * @param array $array The array to be rearranged
7879 * @return array
7881 function swapshuffle($array) {
7883 srand ((double) microtime() * 10000000);
7884 $last = count($array) - 1;
7885 for ($i = 0; $i <= $last; $i++) {
7886 $from = rand(0, $last);
7887 $curr = $array[$i];
7888 $array[$i] = $array[$from];
7889 $array[$from] = $curr;
7891 return $array;
7895 * Like {@link swapshuffle()}, but works on associative arrays
7897 * @param array $array The associative array to be rearranged
7898 * @return array
7900 function swapshuffle_assoc($array) {
7902 $newarray = array();
7903 $newkeys = swapshuffle(array_keys($array));
7905 foreach ($newkeys as $newkey) {
7906 $newarray[$newkey] = $array[$newkey];
7908 return $newarray;
7912 * Given an arbitrary array, and a number of draws,
7913 * this function returns an array with that amount
7914 * of items. The indexes are retained.
7916 * @todo Finish documenting this function
7918 * @param array $array
7919 * @param int $draws
7920 * @return array
7922 function draw_rand_array($array, $draws) {
7923 srand ((double) microtime() * 10000000);
7925 $return = array();
7927 $last = count($array);
7929 if ($draws > $last) {
7930 $draws = $last;
7933 while ($draws > 0) {
7934 $last--;
7936 $keys = array_keys($array);
7937 $rand = rand(0, $last);
7939 $return[$keys[$rand]] = $array[$keys[$rand]];
7940 unset($array[$keys[$rand]]);
7942 $draws--;
7945 return $return;
7949 * Calculate the difference between two microtimes
7951 * @param string $a The first Microtime
7952 * @param string $b The second Microtime
7953 * @return string
7955 function microtime_diff($a, $b) {
7956 list($adec, $asec) = explode(' ', $a);
7957 list($bdec, $bsec) = explode(' ', $b);
7958 return $bsec - $asec + $bdec - $adec;
7962 * Given a list (eg a,b,c,d,e) this function returns
7963 * an array of 1->a, 2->b, 3->c etc
7965 * @param string $list The string to explode into array bits
7966 * @param string $separator The separator used within the list string
7967 * @return array The now assembled array
7969 function make_menu_from_list($list, $separator=',') {
7971 $array = array_reverse(explode($separator, $list), true);
7972 foreach ($array as $key => $item) {
7973 $outarray[$key+1] = trim($item);
7975 return $outarray;
7979 * Creates an array that represents all the current grades that
7980 * can be chosen using the given grading type.
7982 * Negative numbers
7983 * are scales, zero is no grade, and positive numbers are maximum
7984 * grades.
7986 * @todo Finish documenting this function or better deprecated this completely!
7988 * @param int $gradingtype
7989 * @return array
7991 function make_grades_menu($gradingtype) {
7992 global $DB;
7994 $grades = array();
7995 if ($gradingtype < 0) {
7996 if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) {
7997 return make_menu_from_list($scale->scale);
7999 } else if ($gradingtype > 0) {
8000 for ($i=$gradingtype; $i>=0; $i--) {
8001 $grades[$i] = $i .' / '. $gradingtype;
8003 return $grades;
8005 return $grades;
8009 * This function returns the number of activities using the given scale in the given course.
8011 * @param int $courseid The course ID to check.
8012 * @param int $scaleid The scale ID to check
8013 * @return int
8015 function course_scale_used($courseid, $scaleid) {
8016 global $CFG, $DB;
8018 $return = 0;
8020 if (!empty($scaleid)) {
8021 if ($cms = get_course_mods($courseid)) {
8022 foreach ($cms as $cm) {
8023 // Check cm->name/lib.php exists.
8024 if (file_exists($CFG->dirroot.'/mod/'.$cm->modname.'/lib.php')) {
8025 include_once($CFG->dirroot.'/mod/'.$cm->modname.'/lib.php');
8026 $functionname = $cm->modname.'_scale_used';
8027 if (function_exists($functionname)) {
8028 if ($functionname($cm->instance, $scaleid)) {
8029 $return++;
8036 // Check if any course grade item makes use of the scale.
8037 $return += $DB->count_records('grade_items', array('courseid' => $courseid, 'scaleid' => $scaleid));
8039 // Check if any outcome in the course makes use of the scale.
8040 $return += $DB->count_records_sql("SELECT COUNT('x')
8041 FROM {grade_outcomes_courses} goc,
8042 {grade_outcomes} go
8043 WHERE go.id = goc.outcomeid
8044 AND go.scaleid = ? AND goc.courseid = ?",
8045 array($scaleid, $courseid));
8047 return $return;
8051 * This function returns the number of activities using scaleid in the entire site
8053 * @param int $scaleid
8054 * @param array $courses
8055 * @return int
8057 function site_scale_used($scaleid, &$courses) {
8058 $return = 0;
8060 if (!is_array($courses) || count($courses) == 0) {
8061 $courses = get_courses("all", false, "c.id, c.shortname");
8064 if (!empty($scaleid)) {
8065 if (is_array($courses) && count($courses) > 0) {
8066 foreach ($courses as $course) {
8067 $return += course_scale_used($course->id, $scaleid);
8071 return $return;
8075 * make_unique_id_code
8077 * @todo Finish documenting this function
8079 * @uses $_SERVER
8080 * @param string $extra Extra string to append to the end of the code
8081 * @return string
8083 function make_unique_id_code($extra = '') {
8085 $hostname = 'unknownhost';
8086 if (!empty($_SERVER['HTTP_HOST'])) {
8087 $hostname = $_SERVER['HTTP_HOST'];
8088 } else if (!empty($_ENV['HTTP_HOST'])) {
8089 $hostname = $_ENV['HTTP_HOST'];
8090 } else if (!empty($_SERVER['SERVER_NAME'])) {
8091 $hostname = $_SERVER['SERVER_NAME'];
8092 } else if (!empty($_ENV['SERVER_NAME'])) {
8093 $hostname = $_ENV['SERVER_NAME'];
8096 $date = gmdate("ymdHis");
8098 $random = random_string(6);
8100 if ($extra) {
8101 return $hostname .'+'. $date .'+'. $random .'+'. $extra;
8102 } else {
8103 return $hostname .'+'. $date .'+'. $random;
8109 * Function to check the passed address is within the passed subnet
8111 * The parameter is a comma separated string of subnet definitions.
8112 * Subnet strings can be in one of three formats:
8113 * 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn (number of bits in net mask)
8114 * 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)
8115 * 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. (incomplete address, a bit non-technical ;-)
8116 * Code for type 1 modified from user posted comments by mediator at
8117 * {@link http://au.php.net/manual/en/function.ip2long.php}
8119 * @param string $addr The address you are checking
8120 * @param string $subnetstr The string of subnet addresses
8121 * @return bool
8123 function address_in_subnet($addr, $subnetstr) {
8125 if ($addr == '0.0.0.0') {
8126 return false;
8128 $subnets = explode(',', $subnetstr);
8129 $found = false;
8130 $addr = trim($addr);
8131 $addr = cleanremoteaddr($addr, false); // Normalise.
8132 if ($addr === null) {
8133 return false;
8135 $addrparts = explode(':', $addr);
8137 $ipv6 = strpos($addr, ':');
8139 foreach ($subnets as $subnet) {
8140 $subnet = trim($subnet);
8141 if ($subnet === '') {
8142 continue;
8145 if (strpos($subnet, '/') !== false) {
8146 // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn.
8147 list($ip, $mask) = explode('/', $subnet);
8148 $mask = trim($mask);
8149 if (!is_number($mask)) {
8150 continue; // Incorect mask number, eh?
8152 $ip = cleanremoteaddr($ip, false); // Normalise.
8153 if ($ip === null) {
8154 continue;
8156 if (strpos($ip, ':') !== false) {
8157 // IPv6.
8158 if (!$ipv6) {
8159 continue;
8161 if ($mask > 128 or $mask < 0) {
8162 continue; // Nonsense.
8164 if ($mask == 0) {
8165 return true; // Any address.
8167 if ($mask == 128) {
8168 if ($ip === $addr) {
8169 return true;
8171 continue;
8173 $ipparts = explode(':', $ip);
8174 $modulo = $mask % 16;
8175 $ipnet = array_slice($ipparts, 0, ($mask-$modulo)/16);
8176 $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16);
8177 if (implode(':', $ipnet) === implode(':', $addrnet)) {
8178 if ($modulo == 0) {
8179 return true;
8181 $pos = ($mask-$modulo)/16;
8182 $ipnet = hexdec($ipparts[$pos]);
8183 $addrnet = hexdec($addrparts[$pos]);
8184 $mask = 0xffff << (16 - $modulo);
8185 if (($addrnet & $mask) == ($ipnet & $mask)) {
8186 return true;
8190 } else {
8191 // IPv4.
8192 if ($ipv6) {
8193 continue;
8195 if ($mask > 32 or $mask < 0) {
8196 continue; // Nonsense.
8198 if ($mask == 0) {
8199 return true;
8201 if ($mask == 32) {
8202 if ($ip === $addr) {
8203 return true;
8205 continue;
8207 $mask = 0xffffffff << (32 - $mask);
8208 if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
8209 return true;
8213 } else if (strpos($subnet, '-') !== false) {
8214 // 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.
8215 $parts = explode('-', $subnet);
8216 if (count($parts) != 2) {
8217 continue;
8220 if (strpos($subnet, ':') !== false) {
8221 // IPv6.
8222 if (!$ipv6) {
8223 continue;
8225 $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
8226 if ($ipstart === null) {
8227 continue;
8229 $ipparts = explode(':', $ipstart);
8230 $start = hexdec(array_pop($ipparts));
8231 $ipparts[] = trim($parts[1]);
8232 $ipend = cleanremoteaddr(implode(':', $ipparts), false); // Normalise.
8233 if ($ipend === null) {
8234 continue;
8236 $ipparts[7] = '';
8237 $ipnet = implode(':', $ipparts);
8238 if (strpos($addr, $ipnet) !== 0) {
8239 continue;
8241 $ipparts = explode(':', $ipend);
8242 $end = hexdec($ipparts[7]);
8244 $addrend = hexdec($addrparts[7]);
8246 if (($addrend >= $start) and ($addrend <= $end)) {
8247 return true;
8250 } else {
8251 // IPv4.
8252 if ($ipv6) {
8253 continue;
8255 $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
8256 if ($ipstart === null) {
8257 continue;
8259 $ipparts = explode('.', $ipstart);
8260 $ipparts[3] = trim($parts[1]);
8261 $ipend = cleanremoteaddr(implode('.', $ipparts), false); // Normalise.
8262 if ($ipend === null) {
8263 continue;
8266 if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
8267 return true;
8271 } else {
8272 // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
8273 if (strpos($subnet, ':') !== false) {
8274 // IPv6.
8275 if (!$ipv6) {
8276 continue;
8278 $parts = explode(':', $subnet);
8279 $count = count($parts);
8280 if ($parts[$count-1] === '') {
8281 unset($parts[$count-1]); // Trim trailing :'s.
8282 $count--;
8283 $subnet = implode('.', $parts);
8285 $isip = cleanremoteaddr($subnet, false); // Normalise.
8286 if ($isip !== null) {
8287 if ($isip === $addr) {
8288 return true;
8290 continue;
8291 } else if ($count > 8) {
8292 continue;
8294 $zeros = array_fill(0, 8-$count, '0');
8295 $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16);
8296 if (address_in_subnet($addr, $subnet)) {
8297 return true;
8300 } else {
8301 // IPv4.
8302 if ($ipv6) {
8303 continue;
8305 $parts = explode('.', $subnet);
8306 $count = count($parts);
8307 if ($parts[$count-1] === '') {
8308 unset($parts[$count-1]); // Trim trailing .
8309 $count--;
8310 $subnet = implode('.', $parts);
8312 if ($count == 4) {
8313 $subnet = cleanremoteaddr($subnet, false); // Normalise.
8314 if ($subnet === $addr) {
8315 return true;
8317 continue;
8318 } else if ($count > 4) {
8319 continue;
8321 $zeros = array_fill(0, 4-$count, '0');
8322 $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8);
8323 if (address_in_subnet($addr, $subnet)) {
8324 return true;
8330 return false;
8334 * For outputting debugging info
8336 * @param string $string The string to write
8337 * @param string $eol The end of line char(s) to use
8338 * @param string $sleep Period to make the application sleep
8339 * This ensures any messages have time to display before redirect
8341 function mtrace($string, $eol="\n", $sleep=0) {
8343 if (defined('STDOUT') and !PHPUNIT_TEST) {
8344 fwrite(STDOUT, $string.$eol);
8345 } else {
8346 echo $string . $eol;
8349 flush();
8351 // Delay to keep message on user's screen in case of subsequent redirect.
8352 if ($sleep) {
8353 sleep($sleep);
8358 * Replace 1 or more slashes or backslashes to 1 slash
8360 * @param string $path The path to strip
8361 * @return string the path with double slashes removed
8363 function cleardoubleslashes ($path) {
8364 return preg_replace('/(\/|\\\){1,}/', '/', $path);
8368 * Is current ip in give list?
8370 * @param string $list
8371 * @return bool
8373 function remoteip_in_list($list) {
8374 $inlist = false;
8375 $clientip = getremoteaddr(null);
8377 if (!$clientip) {
8378 // Ensure access on cli.
8379 return true;
8382 $list = explode("\n", $list);
8383 foreach ($list as $subnet) {
8384 $subnet = trim($subnet);
8385 if (address_in_subnet($clientip, $subnet)) {
8386 $inlist = true;
8387 break;
8390 return $inlist;
8394 * Returns most reliable client address
8396 * @param string $default If an address can't be determined, then return this
8397 * @return string The remote IP address
8399 function getremoteaddr($default='0.0.0.0') {
8400 global $CFG;
8402 if (empty($CFG->getremoteaddrconf)) {
8403 // This will happen, for example, before just after the upgrade, as the
8404 // user is redirected to the admin screen.
8405 $variablestoskip = 0;
8406 } else {
8407 $variablestoskip = $CFG->getremoteaddrconf;
8409 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) {
8410 if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
8411 $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']);
8412 return $address ? $address : $default;
8415 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
8416 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
8417 $address = cleanremoteaddr($_SERVER['HTTP_X_FORWARDED_FOR']);
8418 return $address ? $address : $default;
8421 if (!empty($_SERVER['REMOTE_ADDR'])) {
8422 $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']);
8423 return $address ? $address : $default;
8424 } else {
8425 return $default;
8430 * Cleans an ip address. Internal addresses are now allowed.
8431 * (Originally local addresses were not allowed.)
8433 * @param string $addr IPv4 or IPv6 address
8434 * @param bool $compress use IPv6 address compression
8435 * @return string normalised ip address string, null if error
8437 function cleanremoteaddr($addr, $compress=false) {
8438 $addr = trim($addr);
8440 // TODO: maybe add a separate function is_addr_public() or something like this.
8442 if (strpos($addr, ':') !== false) {
8443 // Can be only IPv6.
8444 $parts = explode(':', $addr);
8445 $count = count($parts);
8447 if (strpos($parts[$count-1], '.') !== false) {
8448 // Legacy ipv4 notation.
8449 $last = array_pop($parts);
8450 $ipv4 = cleanremoteaddr($last, true);
8451 if ($ipv4 === null) {
8452 return null;
8454 $bits = explode('.', $ipv4);
8455 $parts[] = dechex($bits[0]).dechex($bits[1]);
8456 $parts[] = dechex($bits[2]).dechex($bits[3]);
8457 $count = count($parts);
8458 $addr = implode(':', $parts);
8461 if ($count < 3 or $count > 8) {
8462 return null; // Severly malformed.
8465 if ($count != 8) {
8466 if (strpos($addr, '::') === false) {
8467 return null; // Malformed.
8469 // Uncompress.
8470 $insertat = array_search('', $parts, true);
8471 $missing = array_fill(0, 1 + 8 - $count, '0');
8472 array_splice($parts, $insertat, 1, $missing);
8473 foreach ($parts as $key => $part) {
8474 if ($part === '') {
8475 $parts[$key] = '0';
8480 $adr = implode(':', $parts);
8481 if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) {
8482 return null; // Incorrect format - sorry.
8485 // Normalise 0s and case.
8486 $parts = array_map('hexdec', $parts);
8487 $parts = array_map('dechex', $parts);
8489 $result = implode(':', $parts);
8491 if (!$compress) {
8492 return $result;
8495 if ($result === '0:0:0:0:0:0:0:0') {
8496 return '::'; // All addresses.
8499 $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1);
8500 if ($compressed !== $result) {
8501 return $compressed;
8504 $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
8505 if ($compressed !== $result) {
8506 return $compressed;
8509 $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
8510 if ($compressed !== $result) {
8511 return $compressed;
8514 return $result;
8517 // First get all things that look like IPv4 addresses.
8518 $parts = array();
8519 if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
8520 return null;
8522 unset($parts[0]);
8524 foreach ($parts as $key => $match) {
8525 if ($match > 255) {
8526 return null;
8528 $parts[$key] = (int)$match; // Normalise 0s.
8531 return implode('.', $parts);
8535 * This function will make a complete copy of anything it's given,
8536 * regardless of whether it's an object or not.
8538 * @param mixed $thing Something you want cloned
8539 * @return mixed What ever it is you passed it
8541 function fullclone($thing) {
8542 return unserialize(serialize($thing));
8546 * If new messages are waiting for the current user, then insert
8547 * JavaScript to pop up the messaging window into the page
8549 * @return void
8551 function message_popup_window() {
8552 global $USER, $DB, $PAGE, $CFG;
8554 if (!$PAGE->get_popup_notification_allowed() || empty($CFG->messaging)) {
8555 return;
8558 if (!isloggedin() || isguestuser()) {
8559 return;
8562 if (!isset($USER->message_lastpopup)) {
8563 $USER->message_lastpopup = 0;
8564 } else if ($USER->message_lastpopup > (time()-120)) {
8565 // Don't run the query to check whether to display a popup if its been run in the last 2 minutes.
8566 return;
8569 // A quick query to check whether the user has new messages.
8570 $messagecount = $DB->count_records('message', array('useridto' => $USER->id));
8571 if ($messagecount<1) {
8572 return;
8575 // Got unread messages so now do another query that joins with the user table.
8576 $messagesql = "SELECT m.id, m.smallmessage, m.fullmessageformat, m.notification, u.firstname, u.lastname
8577 FROM {message} m
8578 JOIN {message_working} mw ON m.id=mw.unreadmessageid
8579 JOIN {message_processors} p ON mw.processorid=p.id
8580 JOIN {user} u ON m.useridfrom=u.id
8581 WHERE m.useridto = :userid
8582 AND p.name='popup'";
8584 // If the user was last notified over an hour ago we can re-notify them of old messages
8585 // so don't worry about when the new message was sent.
8586 $lastnotifiedlongago = $USER->message_lastpopup < (time()-3600);
8587 if (!$lastnotifiedlongago) {
8588 $messagesql .= 'AND m.timecreated > :lastpopuptime';
8591 $messageusers = $DB->get_records_sql($messagesql, array('userid' => $USER->id, 'lastpopuptime' => $USER->message_lastpopup));
8593 // If we have new messages to notify the user about.
8594 if (!empty($messageusers)) {
8596 $strmessages = '';
8597 if (count($messageusers)>1) {
8598 $strmessages = get_string('unreadnewmessages', 'message', count($messageusers));
8599 } else {
8600 $messageusers = reset($messageusers);
8602 // Show who the message is from if its not a notification.
8603 if (!$messageusers->notification) {
8604 $strmessages = get_string('unreadnewmessage', 'message', fullname($messageusers) );
8607 // Try to display the small version of the message.
8608 $smallmessage = null;
8609 if (!empty($messageusers->smallmessage)) {
8610 // Display the first 200 chars of the message in the popup.
8611 $smallmessage = null;
8612 if (core_text::strlen($messageusers->smallmessage) > 200) {
8613 $smallmessage = core_text::substr($messageusers->smallmessage, 0, 200).'...';
8614 } else {
8615 $smallmessage = $messageusers->smallmessage;
8618 // Prevent html symbols being displayed.
8619 if ($messageusers->fullmessageformat == FORMAT_HTML) {
8620 $smallmessage = html_to_text($smallmessage);
8621 } else {
8622 $smallmessage = s($smallmessage);
8624 } else if ($messageusers->notification) {
8625 // Its a notification with no smallmessage so just say they have a notification.
8626 $smallmessage = get_string('unreadnewnotification', 'message');
8628 if (!empty($smallmessage)) {
8629 $strmessages .= '<div id="usermessage">'.s($smallmessage).'</div>';
8633 $strgomessage = get_string('gotomessages', 'message');
8634 $strstaymessage = get_string('ignore', 'admin');
8636 $notificationsound = null;
8637 $beep = get_user_preferences('message_beepnewmessage', '');
8638 if (!empty($beep)) {
8639 // Browsers will work down this list until they find something they support.
8640 $sourcetags = html_writer::empty_tag('source', array('src' => $CFG->wwwroot.'/message/bell.wav', 'type' => 'audio/wav'));
8641 $sourcetags .= html_writer::empty_tag('source', array('src' => $CFG->wwwroot.'/message/bell.ogg', 'type' => 'audio/ogg'));
8642 $sourcetags .= html_writer::empty_tag('source', array('src' => $CFG->wwwroot.'/message/bell.mp3', 'type' => 'audio/mpeg'));
8643 $sourcetags .= html_writer::empty_tag('embed', array('src' => $CFG->wwwroot.'/message/bell.wav', 'autostart' => 'true', 'hidden' => 'true'));
8645 $notificationsound = html_writer::tag('audio', $sourcetags, array('preload' => 'auto', 'autoplay' => 'autoplay'));
8648 $url = $CFG->wwwroot.'/message/index.php';
8649 $content = html_writer::start_tag('div', array('id' => 'newmessageoverlay', 'class' => 'mdl-align')).
8650 html_writer::start_tag('div', array('id' => 'newmessagetext')).
8651 $strmessages.
8652 html_writer::end_tag('div').
8654 $notificationsound.
8655 html_writer::start_tag('div', array('id' => 'newmessagelinks')).
8656 html_writer::link($url, $strgomessage, array('id' => 'notificationyes')).'&nbsp;&nbsp;&nbsp;'.
8657 html_writer::link('', $strstaymessage, array('id' => 'notificationno')).
8658 html_writer::end_tag('div');
8659 html_writer::end_tag('div');
8661 $PAGE->requires->js_init_call('M.core_message.init_notification', array('', $content, $url));
8663 $USER->message_lastpopup = time();
8668 * Used to make sure that $min <= $value <= $max
8670 * Make sure that value is between min, and max
8672 * @param int $min The minimum value
8673 * @param int $value The value to check
8674 * @param int $max The maximum value
8675 * @return int
8677 function bounded_number($min, $value, $max) {
8678 if ($value < $min) {
8679 return $min;
8681 if ($value > $max) {
8682 return $max;
8684 return $value;
8688 * Check if there is a nested array within the passed array
8690 * @param array $array
8691 * @return bool true if there is a nested array false otherwise
8693 function array_is_nested($array) {
8694 foreach ($array as $value) {
8695 if (is_array($value)) {
8696 return true;
8699 return false;
8703 * get_performance_info() pairs up with init_performance_info()
8704 * loaded in setup.php. Returns an array with 'html' and 'txt'
8705 * values ready for use, and each of the individual stats provided
8706 * separately as well.
8708 * @return array
8710 function get_performance_info() {
8711 global $CFG, $PERF, $DB, $PAGE;
8713 $info = array();
8714 $info['html'] = ''; // Holds userfriendly HTML representation.
8715 $info['txt'] = me() . ' '; // Holds log-friendly representation.
8717 $info['realtime'] = microtime_diff($PERF->starttime, microtime());
8719 $info['html'] .= '<span class="timeused">'.$info['realtime'].' secs</span> ';
8720 $info['txt'] .= 'time: '.$info['realtime'].'s ';
8722 if (function_exists('memory_get_usage')) {
8723 $info['memory_total'] = memory_get_usage();
8724 $info['memory_growth'] = memory_get_usage() - $PERF->startmemory;
8725 $info['html'] .= '<span class="memoryused">RAM: '.display_size($info['memory_total']).'</span> ';
8726 $info['txt'] .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '.
8727 $info['memory_growth'].'B ('.display_size($info['memory_growth']).') ';
8730 if (function_exists('memory_get_peak_usage')) {
8731 $info['memory_peak'] = memory_get_peak_usage();
8732 $info['html'] .= '<span class="memoryused">RAM peak: '.display_size($info['memory_peak']).'</span> ';
8733 $info['txt'] .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') ';
8736 $inc = get_included_files();
8737 $info['includecount'] = count($inc);
8738 $info['html'] .= '<span class="included">Included '.$info['includecount'].' files</span> ';
8739 $info['txt'] .= 'includecount: '.$info['includecount'].' ';
8741 if (!empty($CFG->early_install_lang) or empty($PAGE)) {
8742 // We can not track more performance before installation or before PAGE init, sorry.
8743 return $info;
8746 $filtermanager = filter_manager::instance();
8747 if (method_exists($filtermanager, 'get_performance_summary')) {
8748 list($filterinfo, $nicenames) = $filtermanager->get_performance_summary();
8749 $info = array_merge($filterinfo, $info);
8750 foreach ($filterinfo as $key => $value) {
8751 $info['html'] .= "<span class='$key'>$nicenames[$key]: $value </span> ";
8752 $info['txt'] .= "$key: $value ";
8756 $stringmanager = get_string_manager();
8757 if (method_exists($stringmanager, 'get_performance_summary')) {
8758 list($filterinfo, $nicenames) = $stringmanager->get_performance_summary();
8759 $info = array_merge($filterinfo, $info);
8760 foreach ($filterinfo as $key => $value) {
8761 $info['html'] .= "<span class='$key'>$nicenames[$key]: $value </span> ";
8762 $info['txt'] .= "$key: $value ";
8766 $jsmodules = $PAGE->requires->get_loaded_modules();
8767 if ($jsmodules) {
8768 $yuicount = 0;
8769 $othercount = 0;
8770 $details = '';
8771 foreach ($jsmodules as $module => $backtraces) {
8772 if (strpos($module, 'yui') === 0) {
8773 $yuicount += 1;
8774 } else {
8775 $othercount += 1;
8777 if (!empty($CFG->yuimoduledebug)) {
8778 // Hidden feature for developers working on YUI module infrastructure.
8779 $details .= "<div class='yui-module'><p>$module</p>";
8780 foreach ($backtraces as $backtrace) {
8781 $details .= "<div class='backtrace'>$backtrace</div>";
8783 $details .= '</div>';
8786 $info['html'] .= "<span class='includedyuimodules'>Included YUI modules: $yuicount</span> ";
8787 $info['txt'] .= "includedyuimodules: $yuicount ";
8788 $info['html'] .= "<span class='includedjsmodules'>Other JavaScript modules: $othercount</span> ";
8789 $info['txt'] .= "includedjsmodules: $othercount ";
8790 if ($details) {
8791 $info['html'] .= '<div id="yui-module-debug" class="notifytiny">'.$details.'</div>';
8795 if (!empty($PERF->logwrites)) {
8796 $info['logwrites'] = $PERF->logwrites;
8797 $info['html'] .= '<span class="logwrites">Log DB writes '.$info['logwrites'].'</span> ';
8798 $info['txt'] .= 'logwrites: '.$info['logwrites'].' ';
8801 $info['dbqueries'] = $DB->perf_get_reads().'/'.($DB->perf_get_writes() - $PERF->logwrites);
8802 $info['html'] .= '<span class="dbqueries">DB reads/writes: '.$info['dbqueries'].'</span> ';
8803 $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' ';
8805 if (function_exists('posix_times')) {
8806 $ptimes = posix_times();
8807 if (is_array($ptimes)) {
8808 foreach ($ptimes as $key => $val) {
8809 $info[$key] = $ptimes[$key] - $PERF->startposixtimes[$key];
8811 $info['html'] .= "<span class=\"posixtimes\">ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</span> ";
8812 $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] ";
8816 // Grab the load average for the last minute.
8817 // /proc will only work under some linux configurations
8818 // while uptime is there under MacOSX/Darwin and other unices.
8819 if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) {
8820 list($serverload) = explode(' ', $loadavg[0]);
8821 unset($loadavg);
8822 } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) {
8823 if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) {
8824 $serverload = $matches[1];
8825 } else {
8826 trigger_error('Could not parse uptime output!');
8829 if (!empty($serverload)) {
8830 $info['serverload'] = $serverload;
8831 $info['html'] .= '<span class="serverload">Load average: '.$info['serverload'].'</span> ';
8832 $info['txt'] .= "serverload: {$info['serverload']} ";
8835 // Display size of session if session started.
8836 if ($si = \core\session\manager::get_performance_info()) {
8837 $info['sessionsize'] = $si['size'];
8838 $info['html'] .= $si['html'];
8839 $info['txt'] .= $si['txt'];
8842 if ($stats = cache_helper::get_stats()) {
8843 $html = '<span class="cachesused">';
8844 $html .= '<span class="cache-stats-heading">Caches used (hits/misses/sets)</span>';
8845 $text = 'Caches used (hits/misses/sets): ';
8846 $hits = 0;
8847 $misses = 0;
8848 $sets = 0;
8849 foreach ($stats as $definition => $stores) {
8850 $html .= '<span class="cache-definition-stats">';
8851 $html .= '<span class="cache-definition-stats-heading">'.$definition.'</span>';
8852 $text .= "$definition {";
8853 foreach ($stores as $store => $data) {
8854 $hits += $data['hits'];
8855 $misses += $data['misses'];
8856 $sets += $data['sets'];
8857 if ($data['hits'] == 0 and $data['misses'] > 0) {
8858 $cachestoreclass = 'nohits';
8859 } else if ($data['hits'] < $data['misses']) {
8860 $cachestoreclass = 'lowhits';
8861 } else {
8862 $cachestoreclass = 'hihits';
8864 $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
8865 $html .= "<span class=\"cache-store-stats $cachestoreclass\">$store: $data[hits] / $data[misses] / $data[sets]</span>";
8867 $html .= '</span>';
8868 $text .= '} ';
8870 $html .= "<span class='cache-total-stats'>Total: $hits / $misses / $sets</span>";
8871 $html .= '</span> ';
8872 $info['cachesused'] = "$hits / $misses / $sets";
8873 $info['html'] .= $html;
8874 $info['txt'] .= $text.'. ';
8875 } else {
8876 $info['cachesused'] = '0 / 0 / 0';
8877 $info['html'] .= '<span class="cachesused">Caches used (hits/misses/sets): 0/0/0</span>';
8878 $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 ';
8881 $info['html'] = '<div class="performanceinfo siteinfo">'.$info['html'].'</div>';
8882 return $info;
8886 * Legacy function.
8888 * @todo Document this function linux people
8890 function apd_get_profiling() {
8891 return shell_exec('pprofp -u ' . ini_get('apd.dumpdir') . '/pprof.' . getmypid() . '.*');
8895 * Delete directory or only its content
8897 * @param string $dir directory path
8898 * @param bool $contentonly
8899 * @return bool success, true also if dir does not exist
8901 function remove_dir($dir, $contentonly=false) {
8902 if (!file_exists($dir)) {
8903 // Nothing to do.
8904 return true;
8906 if (!$handle = opendir($dir)) {
8907 return false;
8909 $result = true;
8910 while (false!==($item = readdir($handle))) {
8911 if ($item != '.' && $item != '..') {
8912 if (is_dir($dir.'/'.$item)) {
8913 $result = remove_dir($dir.'/'.$item) && $result;
8914 } else {
8915 $result = unlink($dir.'/'.$item) && $result;
8919 closedir($handle);
8920 if ($contentonly) {
8921 clearstatcache(); // Make sure file stat cache is properly invalidated.
8922 return $result;
8924 $result = rmdir($dir); // If anything left the result will be false, no need for && $result.
8925 clearstatcache(); // Make sure file stat cache is properly invalidated.
8926 return $result;
8930 * Detect if an object or a class contains a given property
8931 * will take an actual object or the name of a class
8933 * @param mix $obj Name of class or real object to test
8934 * @param string $property name of property to find
8935 * @return bool true if property exists
8937 function object_property_exists( $obj, $property ) {
8938 if (is_string( $obj )) {
8939 $properties = get_class_vars( $obj );
8940 } else {
8941 $properties = get_object_vars( $obj );
8943 return array_key_exists( $property, $properties );
8947 * Converts an object into an associative array
8949 * This function converts an object into an associative array by iterating
8950 * over its public properties. Because this function uses the foreach
8951 * construct, Iterators are respected. It works recursively on arrays of objects.
8952 * Arrays and simple values are returned as is.
8954 * If class has magic properties, it can implement IteratorAggregate
8955 * and return all available properties in getIterator()
8957 * @param mixed $var
8958 * @return array
8960 function convert_to_array($var) {
8961 $result = array();
8963 // Loop over elements/properties.
8964 foreach ($var as $key => $value) {
8965 // Recursively convert objects.
8966 if (is_object($value) || is_array($value)) {
8967 $result[$key] = convert_to_array($value);
8968 } else {
8969 // Simple values are untouched.
8970 $result[$key] = $value;
8973 return $result;
8977 * Detect a custom script replacement in the data directory that will
8978 * replace an existing moodle script
8980 * @return string|bool full path name if a custom script exists, false if no custom script exists
8982 function custom_script_path() {
8983 global $CFG, $SCRIPT;
8985 if ($SCRIPT === null) {
8986 // Probably some weird external script.
8987 return false;
8990 $scriptpath = $CFG->customscripts . $SCRIPT;
8992 // Check the custom script exists.
8993 if (file_exists($scriptpath) and is_file($scriptpath)) {
8994 return $scriptpath;
8995 } else {
8996 return false;
9001 * Returns whether or not the user object is a remote MNET user. This function
9002 * is in moodlelib because it does not rely on loading any of the MNET code.
9004 * @param object $user A valid user object
9005 * @return bool True if the user is from a remote Moodle.
9007 function is_mnet_remote_user($user) {
9008 global $CFG;
9010 if (!isset($CFG->mnet_localhost_id)) {
9011 include_once($CFG->dirroot . '/mnet/lib.php');
9012 $env = new mnet_environment();
9013 $env->init();
9014 unset($env);
9017 return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id);
9021 * This function will search for browser prefereed languages, setting Moodle
9022 * to use the best one available if $SESSION->lang is undefined
9024 function setup_lang_from_browser() {
9025 global $CFG, $SESSION, $USER;
9027 if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) {
9028 // Lang is defined in session or user profile, nothing to do.
9029 return;
9032 if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do.
9033 return;
9036 // Extract and clean langs from headers.
9037 $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
9038 $rawlangs = str_replace('-', '_', $rawlangs); // We are using underscores.
9039 $rawlangs = explode(',', $rawlangs); // Convert to array.
9040 $langs = array();
9042 $order = 1.0;
9043 foreach ($rawlangs as $lang) {
9044 if (strpos($lang, ';') === false) {
9045 $langs[(string)$order] = $lang;
9046 $order = $order-0.01;
9047 } else {
9048 $parts = explode(';', $lang);
9049 $pos = strpos($parts[1], '=');
9050 $langs[substr($parts[1], $pos+1)] = $parts[0];
9053 krsort($langs, SORT_NUMERIC);
9055 // Look for such langs under standard locations.
9056 foreach ($langs as $lang) {
9057 // Clean it properly for include.
9058 $lang = strtolower(clean_param($lang, PARAM_SAFEDIR));
9059 if (get_string_manager()->translation_exists($lang, false)) {
9060 // Lang exists, set it in session.
9061 $SESSION->lang = $lang;
9062 // We have finished. Go out.
9063 break;
9066 return;
9070 * Check if $url matches anything in proxybypass list
9072 * Any errors just result in the proxy being used (least bad)
9074 * @param string $url url to check
9075 * @return boolean true if we should bypass the proxy
9077 function is_proxybypass( $url ) {
9078 global $CFG;
9080 // Sanity check.
9081 if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) {
9082 return false;
9085 // Get the host part out of the url.
9086 if (!$host = parse_url( $url, PHP_URL_HOST )) {
9087 return false;
9090 // Get the possible bypass hosts into an array.
9091 $matches = explode( ',', $CFG->proxybypass );
9093 // Check for a match.
9094 // (IPs need to match the left hand side and hosts the right of the url,
9095 // but we can recklessly check both as there can't be a false +ve).
9096 foreach ($matches as $match) {
9097 $match = trim($match);
9099 // Try for IP match (Left side).
9100 $lhs = substr($host, 0, strlen($match));
9101 if (strcasecmp($match, $lhs)==0) {
9102 return true;
9105 // Try for host match (Right side).
9106 $rhs = substr($host, -strlen($match));
9107 if (strcasecmp($match, $rhs)==0) {
9108 return true;
9112 // Nothing matched.
9113 return false;
9117 * Check if the passed navigation is of the new style
9119 * @param mixed $navigation
9120 * @return bool true for yes false for no
9122 function is_newnav($navigation) {
9123 if (is_array($navigation) && !empty($navigation['newnav'])) {
9124 return true;
9125 } else {
9126 return false;
9131 * Checks whether the given variable name is defined as a variable within the given object.
9133 * This will NOT work with stdClass objects, which have no class variables.
9135 * @param string $var The variable name
9136 * @param object $object The object to check
9137 * @return boolean
9139 function in_object_vars($var, $object) {
9140 $classvars = get_class_vars(get_class($object));
9141 $classvars = array_keys($classvars);
9142 return in_array($var, $classvars);
9146 * Returns an array without repeated objects.
9147 * This function is similar to array_unique, but for arrays that have objects as values
9149 * @param array $array
9150 * @param bool $keepkeyassoc
9151 * @return array
9153 function object_array_unique($array, $keepkeyassoc = true) {
9154 $duplicatekeys = array();
9155 $tmp = array();
9157 foreach ($array as $key => $val) {
9158 // Convert objects to arrays, in_array() does not support objects.
9159 if (is_object($val)) {
9160 $val = (array)$val;
9163 if (!in_array($val, $tmp)) {
9164 $tmp[] = $val;
9165 } else {
9166 $duplicatekeys[] = $key;
9170 foreach ($duplicatekeys as $key) {
9171 unset($array[$key]);
9174 return $keepkeyassoc ? $array : array_values($array);
9178 * Is a userid the primary administrator?
9180 * @param int $userid int id of user to check
9181 * @return boolean
9183 function is_primary_admin($userid) {
9184 $primaryadmin = get_admin();
9186 if ($userid == $primaryadmin->id) {
9187 return true;
9188 } else {
9189 return false;
9194 * Returns the site identifier
9196 * @return string $CFG->siteidentifier, first making sure it is properly initialised.
9198 function get_site_identifier() {
9199 global $CFG;
9200 // Check to see if it is missing. If so, initialise it.
9201 if (empty($CFG->siteidentifier)) {
9202 set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']);
9204 // Return it.
9205 return $CFG->siteidentifier;
9209 * Check whether the given password has no more than the specified
9210 * number of consecutive identical characters.
9212 * @param string $password password to be checked against the password policy
9213 * @param integer $maxchars maximum number of consecutive identical characters
9214 * @return bool
9216 function check_consecutive_identical_characters($password, $maxchars) {
9218 if ($maxchars < 1) {
9219 return true; // Zero 0 is to disable this check.
9221 if (strlen($password) <= $maxchars) {
9222 return true; // Too short to fail this test.
9225 $previouschar = '';
9226 $consecutivecount = 1;
9227 foreach (str_split($password) as $char) {
9228 if ($char != $previouschar) {
9229 $consecutivecount = 1;
9230 } else {
9231 $consecutivecount++;
9232 if ($consecutivecount > $maxchars) {
9233 return false; // Check failed already.
9237 $previouschar = $char;
9240 return true;
9244 * Helper function to do partial function binding.
9245 * so we can use it for preg_replace_callback, for example
9246 * this works with php functions, user functions, static methods and class methods
9247 * it returns you a callback that you can pass on like so:
9249 * $callback = partial('somefunction', $arg1, $arg2);
9250 * or
9251 * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
9252 * or even
9253 * $obj = new someclass();
9254 * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2);
9256 * and then the arguments that are passed through at calltime are appended to the argument list.
9258 * @param mixed $function a php callback
9259 * @param mixed $arg1,... $argv arguments to partially bind with
9260 * @return array Array callback
9262 function partial() {
9263 if (!class_exists('partial')) {
9265 * Used to manage function binding.
9266 * @copyright 2009 Penny Leach
9267 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
9269 class partial{
9270 /** @var array */
9271 public $values = array();
9272 /** @var string The function to call as a callback. */
9273 public $func;
9275 * Constructor
9276 * @param string $func
9277 * @param array $args
9279 public function __construct($func, $args) {
9280 $this->values = $args;
9281 $this->func = $func;
9284 * Calls the callback function.
9285 * @return mixed
9287 public function method() {
9288 $args = func_get_args();
9289 return call_user_func_array($this->func, array_merge($this->values, $args));
9293 $args = func_get_args();
9294 $func = array_shift($args);
9295 $p = new partial($func, $args);
9296 return array($p, 'method');
9300 * helper function to load up and initialise the mnet environment
9301 * this must be called before you use mnet functions.
9303 * @return mnet_environment the equivalent of old $MNET global
9305 function get_mnet_environment() {
9306 global $CFG;
9307 require_once($CFG->dirroot . '/mnet/lib.php');
9308 static $instance = null;
9309 if (empty($instance)) {
9310 $instance = new mnet_environment();
9311 $instance->init();
9313 return $instance;
9317 * during xmlrpc server code execution, any code wishing to access
9318 * information about the remote peer must use this to get it.
9320 * @return mnet_remote_client the equivalent of old $MNETREMOTE_CLIENT global
9322 function get_mnet_remote_client() {
9323 if (!defined('MNET_SERVER')) {
9324 debugging(get_string('notinxmlrpcserver', 'mnet'));
9325 return false;
9327 global $MNET_REMOTE_CLIENT;
9328 if (isset($MNET_REMOTE_CLIENT)) {
9329 return $MNET_REMOTE_CLIENT;
9331 return false;
9335 * during the xmlrpc server code execution, this will be called
9336 * to setup the object returned by {@link get_mnet_remote_client}
9338 * @param mnet_remote_client $client the client to set up
9339 * @throws moodle_exception
9341 function set_mnet_remote_client($client) {
9342 if (!defined('MNET_SERVER')) {
9343 throw new moodle_exception('notinxmlrpcserver', 'mnet');
9345 global $MNET_REMOTE_CLIENT;
9346 $MNET_REMOTE_CLIENT = $client;
9350 * return the jump url for a given remote user
9351 * this is used for rewriting forum post links in emails, etc
9353 * @param stdclass $user the user to get the idp url for
9355 function mnet_get_idp_jump_url($user) {
9356 global $CFG;
9358 static $mnetjumps = array();
9359 if (!array_key_exists($user->mnethostid, $mnetjumps)) {
9360 $idp = mnet_get_peer_host($user->mnethostid);
9361 $idpjumppath = mnet_get_app_jumppath($idp->applicationid);
9362 $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl=';
9364 return $mnetjumps[$user->mnethostid];
9368 * Gets the homepage to use for the current user
9370 * @return int One of HOMEPAGE_*
9372 function get_home_page() {
9373 global $CFG;
9375 if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) {
9376 if ($CFG->defaulthomepage == HOMEPAGE_MY) {
9377 return HOMEPAGE_MY;
9378 } else {
9379 return (int)get_user_preferences('user_home_page_preference', HOMEPAGE_MY);
9382 return HOMEPAGE_SITE;
9386 * Gets the name of a course to be displayed when showing a list of courses.
9387 * By default this is just $course->fullname but user can configure it. The
9388 * result of this function should be passed through print_string.
9389 * @param stdClass|course_in_list $course Moodle course object
9390 * @return string Display name of course (either fullname or short + fullname)
9392 function get_course_display_name_for_list($course) {
9393 global $CFG;
9394 if (!empty($CFG->courselistshortnames)) {
9395 if (!($course instanceof stdClass)) {
9396 $course = (object)convert_to_array($course);
9398 return get_string('courseextendednamedisplay', '', $course);
9399 } else {
9400 return $course->fullname;
9405 * The lang_string class
9407 * This special class is used to create an object representation of a string request.
9408 * It is special because processing doesn't occur until the object is first used.
9409 * The class was created especially to aid performance in areas where strings were
9410 * required to be generated but were not necessarily used.
9411 * As an example the admin tree when generated uses over 1500 strings, of which
9412 * normally only 1/3 are ever actually printed at any time.
9413 * The performance advantage is achieved by not actually processing strings that
9414 * arn't being used, as such reducing the processing required for the page.
9416 * How to use the lang_string class?
9417 * There are two methods of using the lang_string class, first through the
9418 * forth argument of the get_string function, and secondly directly.
9419 * The following are examples of both.
9420 * 1. Through get_string calls e.g.
9421 * $string = get_string($identifier, $component, $a, true);
9422 * $string = get_string('yes', 'moodle', null, true);
9423 * 2. Direct instantiation
9424 * $string = new lang_string($identifier, $component, $a, $lang);
9425 * $string = new lang_string('yes');
9427 * How do I use a lang_string object?
9428 * The lang_string object makes use of a magic __toString method so that you
9429 * are able to use the object exactly as you would use a string in most cases.
9430 * This means you are able to collect it into a variable and then directly
9431 * echo it, or concatenate it into another string, or similar.
9432 * The other thing you can do is manually get the string by calling the
9433 * lang_strings out method e.g.
9434 * $string = new lang_string('yes');
9435 * $string->out();
9436 * Also worth noting is that the out method can take one argument, $lang which
9437 * allows the developer to change the language on the fly.
9439 * When should I use a lang_string object?
9440 * The lang_string object is designed to be used in any situation where a
9441 * string may not be needed, but needs to be generated.
9442 * The admin tree is a good example of where lang_string objects should be
9443 * used.
9444 * A more practical example would be any class that requries strings that may
9445 * not be printed (after all classes get renderer by renderers and who knows
9446 * what they will do ;))
9448 * When should I not use a lang_string object?
9449 * Don't use lang_strings when you are going to use a string immediately.
9450 * There is no need as it will be processed immediately and there will be no
9451 * advantage, and in fact perhaps a negative hit as a class has to be
9452 * instantiated for a lang_string object, however get_string won't require
9453 * that.
9455 * Limitations:
9456 * 1. You cannot use a lang_string object as an array offset. Doing so will
9457 * result in PHP throwing an error. (You can use it as an object property!)
9459 * @package core
9460 * @category string
9461 * @copyright 2011 Sam Hemelryk
9462 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
9464 class lang_string {
9466 /** @var string The strings identifier */
9467 protected $identifier;
9468 /** @var string The strings component. Default '' */
9469 protected $component = '';
9470 /** @var array|stdClass Any arguments required for the string. Default null */
9471 protected $a = null;
9472 /** @var string The language to use when processing the string. Default null */
9473 protected $lang = null;
9475 /** @var string The processed string (once processed) */
9476 protected $string = null;
9479 * A special boolean. If set to true then the object has been woken up and
9480 * cannot be regenerated. If this is set then $this->string MUST be used.
9481 * @var bool
9483 protected $forcedstring = false;
9486 * Constructs a lang_string object
9488 * This function should do as little processing as possible to ensure the best
9489 * performance for strings that won't be used.
9491 * @param string $identifier The strings identifier
9492 * @param string $component The strings component
9493 * @param stdClass|array $a Any arguments the string requires
9494 * @param string $lang The language to use when processing the string.
9495 * @throws coding_exception
9497 public function __construct($identifier, $component = '', $a = null, $lang = null) {
9498 if (empty($component)) {
9499 $component = 'moodle';
9502 $this->identifier = $identifier;
9503 $this->component = $component;
9504 $this->lang = $lang;
9506 // We MUST duplicate $a to ensure that it if it changes by reference those
9507 // changes are not carried across.
9508 // To do this we always ensure $a or its properties/values are strings
9509 // and that any properties/values that arn't convertable are forgotten.
9510 if (!empty($a)) {
9511 if (is_scalar($a)) {
9512 $this->a = $a;
9513 } else if ($a instanceof lang_string) {
9514 $this->a = $a->out();
9515 } else if (is_object($a) or is_array($a)) {
9516 $a = (array)$a;
9517 $this->a = array();
9518 foreach ($a as $key => $value) {
9519 // Make sure conversion errors don't get displayed (results in '').
9520 if (is_array($value)) {
9521 $this->a[$key] = '';
9522 } else if (is_object($value)) {
9523 if (method_exists($value, '__toString')) {
9524 $this->a[$key] = $value->__toString();
9525 } else {
9526 $this->a[$key] = '';
9528 } else {
9529 $this->a[$key] = (string)$value;
9535 if (debugging(false, DEBUG_DEVELOPER)) {
9536 if (clean_param($this->identifier, PARAM_STRINGID) == '') {
9537 throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition');
9539 if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') {
9540 throw new coding_exception('Invalid string compontent. Please check your string definition');
9542 if (!get_string_manager()->string_exists($this->identifier, $this->component)) {
9543 debugging('String does not exist. Please check your string definition for '.$this->identifier.'/'.$this->component, DEBUG_DEVELOPER);
9549 * Processes the string.
9551 * This function actually processes the string, stores it in the string property
9552 * and then returns it.
9553 * You will notice that this function is VERY similar to the get_string method.
9554 * That is because it is pretty much doing the same thing.
9555 * However as this function is an upgrade it isn't as tolerant to backwards
9556 * compatibility.
9558 * @return string
9559 * @throws coding_exception
9561 protected function get_string() {
9562 global $CFG;
9564 // Check if we need to process the string.
9565 if ($this->string === null) {
9566 // Check the quality of the identifier.
9567 if ($CFG->debugdeveloper && clean_param($this->identifier, PARAM_STRINGID) === '') {
9568 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);
9571 // Process the string.
9572 $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang);
9573 // Debugging feature lets you display string identifier and component.
9574 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
9575 $this->string .= ' {' . $this->identifier . '/' . $this->component . '}';
9578 // Return the string.
9579 return $this->string;
9583 * Returns the string
9585 * @param string $lang The langauge to use when processing the string
9586 * @return string
9588 public function out($lang = null) {
9589 if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) {
9590 if ($this->forcedstring) {
9591 debugging('lang_string objects that have been used cannot be printed in another language. ('.$this->lang.' used)', DEBUG_DEVELOPER);
9592 return $this->get_string();
9594 $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang);
9595 return $translatedstring->out();
9597 return $this->get_string();
9601 * Magic __toString method for printing a string
9603 * @return string
9605 public function __toString() {
9606 return $this->get_string();
9610 * Magic __set_state method used for var_export
9612 * @return string
9614 public function __set_state() {
9615 return $this->get_string();
9619 * Prepares the lang_string for sleep and stores only the forcedstring and
9620 * string properties... the string cannot be regenerated so we need to ensure
9621 * it is generated for this.
9623 * @return string
9625 public function __sleep() {
9626 $this->get_string();
9627 $this->forcedstring = true;
9628 return array('forcedstring', 'string', 'lang');