MDL-41395 Improved phpdocs for coursecatlib.php
[moodle.git] / lib / moodlelib.php
blobad04ace16d11f9f8471d0432a00af09b8fcaaef7
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 /** Unspecified module archetype */
437 define('MOD_ARCHETYPE_OTHER', 0);
438 /** Resource-like type module */
439 define('MOD_ARCHETYPE_RESOURCE', 1);
440 /** Assignment module archetype */
441 define('MOD_ARCHETYPE_ASSIGNMENT', 2);
442 /** System (not user-addable) module archetype */
443 define('MOD_ARCHETYPE_SYSTEM', 3);
446 * Security token used for allowing access
447 * from external application such as web services.
448 * Scripts do not use any session, performance is relatively
449 * low because we need to load access info in each request.
450 * Scripts are executed in parallel.
452 define('EXTERNAL_TOKEN_PERMANENT', 0);
455 * Security token used for allowing access
456 * of embedded applications, the code is executed in the
457 * active user session. Token is invalidated after user logs out.
458 * Scripts are executed serially - normal session locking is used.
460 define('EXTERNAL_TOKEN_EMBEDDED', 1);
463 * The home page should be the site home
465 define('HOMEPAGE_SITE', 0);
467 * The home page should be the users my page
469 define('HOMEPAGE_MY', 1);
471 * The home page can be chosen by the user
473 define('HOMEPAGE_USER', 2);
476 * Hub directory url (should be moodle.org)
478 define('HUB_HUBDIRECTORYURL', "http://hubdirectory.moodle.org");
482 * Moodle.org url (should be moodle.org)
484 define('HUB_MOODLEORGHUBURL', "http://hub.moodle.org");
487 * Moodle mobile app service name
489 define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app');
492 * Indicates the user has the capabilities required to ignore activity and course file size restrictions
494 define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1);
497 * Course display settings: display all sections on one page.
499 define('COURSE_DISPLAY_SINGLEPAGE', 0);
501 * Course display settings: split pages into a page per section.
503 define('COURSE_DISPLAY_MULTIPAGE', 1);
506 * Authentication constant: String used in password field when password is not stored.
508 define('AUTH_PASSWORD_NOT_CACHED', 'not cached');
510 // PARAMETER HANDLING.
513 * Returns a particular value for the named variable, taken from
514 * POST or GET. If the parameter doesn't exist then an error is
515 * thrown because we require this variable.
517 * This function should be used to initialise all required values
518 * in a script that are based on parameters. Usually it will be
519 * used like this:
520 * $id = required_param('id', PARAM_INT);
522 * Please note the $type parameter is now required and the value can not be array.
524 * @param string $parname the name of the page parameter we want
525 * @param string $type expected type of parameter
526 * @return mixed
527 * @throws coding_exception
529 function required_param($parname, $type) {
530 if (func_num_args() != 2 or empty($parname) or empty($type)) {
531 throw new coding_exception('required_param() requires $parname and $type to be specified (parameter: '.$parname.')');
533 // POST has precedence.
534 if (isset($_POST[$parname])) {
535 $param = $_POST[$parname];
536 } else if (isset($_GET[$parname])) {
537 $param = $_GET[$parname];
538 } else {
539 print_error('missingparam', '', '', $parname);
542 if (is_array($param)) {
543 debugging('Invalid array parameter detected in required_param(): '.$parname);
544 // TODO: switch to fatal error in Moodle 2.3.
545 return required_param_array($parname, $type);
548 return clean_param($param, $type);
552 * Returns a particular array value for the named variable, taken from
553 * POST or GET. If the parameter doesn't exist then an error is
554 * thrown because we require this variable.
556 * This function should be used to initialise all required values
557 * in a script that are based on parameters. Usually it will be
558 * used like this:
559 * $ids = required_param_array('ids', PARAM_INT);
561 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
563 * @param string $parname the name of the page parameter we want
564 * @param string $type expected type of parameter
565 * @return array
566 * @throws coding_exception
568 function required_param_array($parname, $type) {
569 if (func_num_args() != 2 or empty($parname) or empty($type)) {
570 throw new coding_exception('required_param_array() requires $parname and $type to be specified (parameter: '.$parname.')');
572 // POST has precedence.
573 if (isset($_POST[$parname])) {
574 $param = $_POST[$parname];
575 } else if (isset($_GET[$parname])) {
576 $param = $_GET[$parname];
577 } else {
578 print_error('missingparam', '', '', $parname);
580 if (!is_array($param)) {
581 print_error('missingparam', '', '', $parname);
584 $result = array();
585 foreach ($param as $key => $value) {
586 if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
587 debugging('Invalid key name in required_param_array() detected: '.$key.', parameter: '.$parname);
588 continue;
590 $result[$key] = clean_param($value, $type);
593 return $result;
597 * Returns a particular value for the named variable, taken from
598 * POST or GET, otherwise returning a given default.
600 * This function should be used to initialise all optional values
601 * in a script that are based on parameters. Usually it will be
602 * used like this:
603 * $name = optional_param('name', 'Fred', PARAM_TEXT);
605 * Please note the $type parameter is now required and the value can not be array.
607 * @param string $parname the name of the page parameter we want
608 * @param mixed $default the default value to return if nothing is found
609 * @param string $type expected type of parameter
610 * @return mixed
611 * @throws coding_exception
613 function optional_param($parname, $default, $type) {
614 if (func_num_args() != 3 or empty($parname) or empty($type)) {
615 throw new coding_exception('optional_param requires $parname, $default + $type to be specified (parameter: '.$parname.')');
617 if (!isset($default)) {
618 $default = null;
621 // POST has precedence.
622 if (isset($_POST[$parname])) {
623 $param = $_POST[$parname];
624 } else if (isset($_GET[$parname])) {
625 $param = $_GET[$parname];
626 } else {
627 return $default;
630 if (is_array($param)) {
631 debugging('Invalid array parameter detected in required_param(): '.$parname);
632 // TODO: switch to $default in Moodle 2.3.
633 return optional_param_array($parname, $default, $type);
636 return clean_param($param, $type);
640 * Returns a particular array value for the named variable, taken from
641 * POST or GET, otherwise returning a given default.
643 * This function should be used to initialise all optional values
644 * in a script that are based on parameters. Usually it will be
645 * used like this:
646 * $ids = optional_param('id', array(), PARAM_INT);
648 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
650 * @param string $parname the name of the page parameter we want
651 * @param mixed $default the default value to return if nothing is found
652 * @param string $type expected type of parameter
653 * @return array
654 * @throws coding_exception
656 function optional_param_array($parname, $default, $type) {
657 if (func_num_args() != 3 or empty($parname) or empty($type)) {
658 throw new coding_exception('optional_param_array requires $parname, $default + $type to be specified (parameter: '.$parname.')');
661 // POST has precedence.
662 if (isset($_POST[$parname])) {
663 $param = $_POST[$parname];
664 } else if (isset($_GET[$parname])) {
665 $param = $_GET[$parname];
666 } else {
667 return $default;
669 if (!is_array($param)) {
670 debugging('optional_param_array() expects array parameters only: '.$parname);
671 return $default;
674 $result = array();
675 foreach ($param as $key => $value) {
676 if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
677 debugging('Invalid key name in optional_param_array() detected: '.$key.', parameter: '.$parname);
678 continue;
680 $result[$key] = clean_param($value, $type);
683 return $result;
687 * Strict validation of parameter values, the values are only converted
688 * to requested PHP type. Internally it is using clean_param, the values
689 * before and after cleaning must be equal - otherwise
690 * an invalid_parameter_exception is thrown.
691 * Objects and classes are not accepted.
693 * @param mixed $param
694 * @param string $type PARAM_ constant
695 * @param bool $allownull are nulls valid value?
696 * @param string $debuginfo optional debug information
697 * @return mixed the $param value converted to PHP type
698 * @throws invalid_parameter_exception if $param is not of given type
700 function validate_param($param, $type, $allownull=NULL_NOT_ALLOWED, $debuginfo='') {
701 if (is_null($param)) {
702 if ($allownull == NULL_ALLOWED) {
703 return null;
704 } else {
705 throw new invalid_parameter_exception($debuginfo);
708 if (is_array($param) or is_object($param)) {
709 throw new invalid_parameter_exception($debuginfo);
712 $cleaned = clean_param($param, $type);
714 if ($type == PARAM_FLOAT) {
715 // Do not detect precision loss here.
716 if (is_float($param) or is_int($param)) {
717 // These always fit.
718 } else if (!is_numeric($param) or !preg_match('/^[\+-]?[0-9]*\.?[0-9]*(e[-+]?[0-9]+)?$/i', (string)$param)) {
719 throw new invalid_parameter_exception($debuginfo);
721 } else if ((string)$param !== (string)$cleaned) {
722 // Conversion to string is usually lossless.
723 throw new invalid_parameter_exception($debuginfo);
726 return $cleaned;
730 * Makes sure array contains only the allowed types, this function does not validate array key names!
732 * <code>
733 * $options = clean_param($options, PARAM_INT);
734 * </code>
736 * @param array $param the variable array we are cleaning
737 * @param string $type expected format of param after cleaning.
738 * @param bool $recursive clean recursive arrays
739 * @return array
740 * @throws coding_exception
742 function clean_param_array(array $param = null, $type, $recursive = false) {
743 // Convert null to empty array.
744 $param = (array)$param;
745 foreach ($param as $key => $value) {
746 if (is_array($value)) {
747 if ($recursive) {
748 $param[$key] = clean_param_array($value, $type, true);
749 } else {
750 throw new coding_exception('clean_param_array can not process multidimensional arrays when $recursive is false.');
752 } else {
753 $param[$key] = clean_param($value, $type);
756 return $param;
760 * Used by {@link optional_param()} and {@link required_param()} to
761 * clean the variables and/or cast to specific types, based on
762 * an options field.
763 * <code>
764 * $course->format = clean_param($course->format, PARAM_ALPHA);
765 * $selectedgradeitem = clean_param($selectedgradeitem, PARAM_INT);
766 * </code>
768 * @param mixed $param the variable we are cleaning
769 * @param string $type expected format of param after cleaning.
770 * @return mixed
771 * @throws coding_exception
773 function clean_param($param, $type) {
774 global $CFG;
776 if (is_array($param)) {
777 throw new coding_exception('clean_param() can not process arrays, please use clean_param_array() instead.');
778 } else if (is_object($param)) {
779 if (method_exists($param, '__toString')) {
780 $param = $param->__toString();
781 } else {
782 throw new coding_exception('clean_param() can not process objects, please use clean_param_array() instead.');
786 switch ($type) {
787 case PARAM_RAW:
788 // No cleaning at all.
789 $param = fix_utf8($param);
790 return $param;
792 case PARAM_RAW_TRIMMED:
793 // No cleaning, but strip leading and trailing whitespace.
794 $param = fix_utf8($param);
795 return trim($param);
797 case PARAM_CLEAN:
798 // General HTML cleaning, try to use more specific type if possible this is deprecated!
799 // Please use more specific type instead.
800 if (is_numeric($param)) {
801 return $param;
803 $param = fix_utf8($param);
804 // Sweep for scripts, etc.
805 return clean_text($param);
807 case PARAM_CLEANHTML:
808 // Clean html fragment.
809 $param = fix_utf8($param);
810 // Sweep for scripts, etc.
811 $param = clean_text($param, FORMAT_HTML);
812 return trim($param);
814 case PARAM_INT:
815 // Convert to integer.
816 return (int)$param;
818 case PARAM_FLOAT:
819 // Convert to float.
820 return (float)$param;
822 case PARAM_ALPHA:
823 // Remove everything not `a-z`.
824 return preg_replace('/[^a-zA-Z]/i', '', $param);
826 case PARAM_ALPHAEXT:
827 // Remove everything not `a-zA-Z_-` (originally allowed "/" too).
828 return preg_replace('/[^a-zA-Z_-]/i', '', $param);
830 case PARAM_ALPHANUM:
831 // Remove everything not `a-zA-Z0-9`.
832 return preg_replace('/[^A-Za-z0-9]/i', '', $param);
834 case PARAM_ALPHANUMEXT:
835 // Remove everything not `a-zA-Z0-9_-`.
836 return preg_replace('/[^A-Za-z0-9_-]/i', '', $param);
838 case PARAM_SEQUENCE:
839 // Remove everything not `0-9,`.
840 return preg_replace('/[^0-9,]/i', '', $param);
842 case PARAM_BOOL:
843 // Convert to 1 or 0.
844 $tempstr = strtolower($param);
845 if ($tempstr === 'on' or $tempstr === 'yes' or $tempstr === 'true') {
846 $param = 1;
847 } else if ($tempstr === 'off' or $tempstr === 'no' or $tempstr === 'false') {
848 $param = 0;
849 } else {
850 $param = empty($param) ? 0 : 1;
852 return $param;
854 case PARAM_NOTAGS:
855 // Strip all tags.
856 $param = fix_utf8($param);
857 return strip_tags($param);
859 case PARAM_TEXT:
860 // Leave only tags needed for multilang.
861 $param = fix_utf8($param);
862 // If the multilang syntax is not correct we strip all tags because it would break xhtml strict which is required
863 // for accessibility standards please note this cleaning does not strip unbalanced '>' for BC compatibility reasons.
864 do {
865 if (strpos($param, '</lang>') !== false) {
866 // Old and future mutilang syntax.
867 $param = strip_tags($param, '<lang>');
868 if (!preg_match_all('/<.*>/suU', $param, $matches)) {
869 break;
871 $open = false;
872 foreach ($matches[0] as $match) {
873 if ($match === '</lang>') {
874 if ($open) {
875 $open = false;
876 continue;
877 } else {
878 break 2;
881 if (!preg_match('/^<lang lang="[a-zA-Z0-9_-]+"\s*>$/u', $match)) {
882 break 2;
883 } else {
884 $open = true;
887 if ($open) {
888 break;
890 return $param;
892 } else if (strpos($param, '</span>') !== false) {
893 // Current problematic multilang syntax.
894 $param = strip_tags($param, '<span>');
895 if (!preg_match_all('/<.*>/suU', $param, $matches)) {
896 break;
898 $open = false;
899 foreach ($matches[0] as $match) {
900 if ($match === '</span>') {
901 if ($open) {
902 $open = false;
903 continue;
904 } else {
905 break 2;
908 if (!preg_match('/^<span(\s+lang="[a-zA-Z0-9_-]+"|\s+class="multilang"){2}\s*>$/u', $match)) {
909 break 2;
910 } else {
911 $open = true;
914 if ($open) {
915 break;
917 return $param;
919 } while (false);
920 // Easy, just strip all tags, if we ever want to fix orphaned '&' we have to do that in format_string().
921 return strip_tags($param);
923 case PARAM_COMPONENT:
924 // We do not want any guessing here, either the name is correct or not
925 // please note only normalised component names are accepted.
926 if (!preg_match('/^[a-z]+(_[a-z][a-z0-9_]*)?[a-z0-9]$/', $param)) {
927 return '';
929 if (strpos($param, '__') !== false) {
930 return '';
932 if (strpos($param, 'mod_') === 0) {
933 // Module names must not contain underscores because we need to differentiate them from invalid plugin types.
934 if (substr_count($param, '_') != 1) {
935 return '';
938 return $param;
940 case PARAM_PLUGIN:
941 case PARAM_AREA:
942 // We do not want any guessing here, either the name is correct or not.
943 if (!is_valid_plugin_name($param)) {
944 return '';
946 return $param;
948 case PARAM_SAFEDIR:
949 // Remove everything not a-zA-Z0-9_- .
950 return preg_replace('/[^a-zA-Z0-9_-]/i', '', $param);
952 case PARAM_SAFEPATH:
953 // Remove everything not a-zA-Z0-9/_- .
954 return preg_replace('/[^a-zA-Z0-9\/_-]/i', '', $param);
956 case PARAM_FILE:
957 // Strip all suspicious characters from filename.
958 $param = fix_utf8($param);
959 $param = preg_replace('~[[:cntrl:]]|[&<>"`\|\':\\\\/]~u', '', $param);
960 if ($param === '.' || $param === '..') {
961 $param = '';
963 return $param;
965 case PARAM_PATH:
966 // Strip all suspicious characters from file path.
967 $param = fix_utf8($param);
968 $param = str_replace('\\', '/', $param);
970 // Explode the path and clean each element using the PARAM_FILE rules.
971 $breadcrumb = explode('/', $param);
972 foreach ($breadcrumb as $key => $crumb) {
973 if ($crumb === '.' && $key === 0) {
974 // Special condition to allow for relative current path such as ./currentdirfile.txt.
975 } else {
976 $crumb = clean_param($crumb, PARAM_FILE);
978 $breadcrumb[$key] = $crumb;
980 $param = implode('/', $breadcrumb);
982 // Remove multiple current path (./././) and multiple slashes (///).
983 $param = preg_replace('~//+~', '/', $param);
984 $param = preg_replace('~/(\./)+~', '/', $param);
985 return $param;
987 case PARAM_HOST:
988 // Allow FQDN or IPv4 dotted quad.
989 $param = preg_replace('/[^\.\d\w-]/', '', $param );
990 // Match ipv4 dotted quad.
991 if (preg_match('/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/', $param, $match)) {
992 // Confirm values are ok.
993 if ( $match[0] > 255
994 || $match[1] > 255
995 || $match[3] > 255
996 || $match[4] > 255 ) {
997 // Hmmm, what kind of dotted quad is this?
998 $param = '';
1000 } else if ( preg_match('/^[\w\d\.-]+$/', $param) // Dots, hyphens, numbers.
1001 && !preg_match('/^[\.-]/', $param) // No leading dots/hyphens.
1002 && !preg_match('/[\.-]$/', $param) // No trailing dots/hyphens.
1004 // All is ok - $param is respected.
1005 } else {
1006 // All is not ok...
1007 $param='';
1009 return $param;
1011 case PARAM_URL: // Allow safe ftp, http, mailto urls.
1012 $param = fix_utf8($param);
1013 include_once($CFG->dirroot . '/lib/validateurlsyntax.php');
1014 if (!empty($param) && validateUrlSyntax($param, 's?H?S?F?E?u-P-a?I?p?f?q?r?')) {
1015 // All is ok, param is respected.
1016 } else {
1017 // Not really ok.
1018 $param ='';
1020 return $param;
1022 case PARAM_LOCALURL:
1023 // Allow http absolute, root relative and relative URLs within wwwroot.
1024 $param = clean_param($param, PARAM_URL);
1025 if (!empty($param)) {
1026 if (preg_match(':^/:', $param)) {
1027 // Root-relative, ok!
1028 } else if (preg_match('/^'.preg_quote($CFG->wwwroot, '/').'/i', $param)) {
1029 // Absolute, and matches our wwwroot.
1030 } else {
1031 // Relative - let's make sure there are no tricks.
1032 if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?')) {
1033 // Looks ok.
1034 } else {
1035 $param = '';
1039 return $param;
1041 case PARAM_PEM:
1042 $param = trim($param);
1043 // PEM formatted strings may contain letters/numbers and the symbols:
1044 // forward slash: /
1045 // plus sign: +
1046 // equal sign: =
1047 // , surrounded by BEGIN and END CERTIFICATE prefix and suffixes.
1048 if (preg_match('/^-----BEGIN CERTIFICATE-----([\s\w\/\+=]+)-----END CERTIFICATE-----$/', trim($param), $matches)) {
1049 list($wholething, $body) = $matches;
1050 unset($wholething, $matches);
1051 $b64 = clean_param($body, PARAM_BASE64);
1052 if (!empty($b64)) {
1053 return "-----BEGIN CERTIFICATE-----\n$b64\n-----END CERTIFICATE-----\n";
1054 } else {
1055 return '';
1058 return '';
1060 case PARAM_BASE64:
1061 if (!empty($param)) {
1062 // PEM formatted strings may contain letters/numbers and the symbols
1063 // forward slash: /
1064 // plus sign: +
1065 // equal sign: =.
1066 if (0 >= preg_match('/^([\s\w\/\+=]+)$/', trim($param))) {
1067 return '';
1069 $lines = preg_split('/[\s]+/', $param, -1, PREG_SPLIT_NO_EMPTY);
1070 // Each line of base64 encoded data must be 64 characters in length, except for the last line which may be less
1071 // than (or equal to) 64 characters long.
1072 for ($i=0, $j=count($lines); $i < $j; $i++) {
1073 if ($i + 1 == $j) {
1074 if (64 < strlen($lines[$i])) {
1075 return '';
1077 continue;
1080 if (64 != strlen($lines[$i])) {
1081 return '';
1084 return implode("\n", $lines);
1085 } else {
1086 return '';
1089 case PARAM_TAG:
1090 $param = fix_utf8($param);
1091 // Please note it is not safe to use the tag name directly anywhere,
1092 // it must be processed with s(), urlencode() before embedding anywhere.
1093 // Remove some nasties.
1094 $param = preg_replace('~[[:cntrl:]]|[<>`]~u', '', $param);
1095 // Convert many whitespace chars into one.
1096 $param = preg_replace('/\s+/', ' ', $param);
1097 $param = core_text::substr(trim($param), 0, TAG_MAX_LENGTH);
1098 return $param;
1100 case PARAM_TAGLIST:
1101 $param = fix_utf8($param);
1102 $tags = explode(',', $param);
1103 $result = array();
1104 foreach ($tags as $tag) {
1105 $res = clean_param($tag, PARAM_TAG);
1106 if ($res !== '') {
1107 $result[] = $res;
1110 if ($result) {
1111 return implode(',', $result);
1112 } else {
1113 return '';
1116 case PARAM_CAPABILITY:
1117 if (get_capability_info($param)) {
1118 return $param;
1119 } else {
1120 return '';
1123 case PARAM_PERMISSION:
1124 $param = (int)$param;
1125 if (in_array($param, array(CAP_INHERIT, CAP_ALLOW, CAP_PREVENT, CAP_PROHIBIT))) {
1126 return $param;
1127 } else {
1128 return CAP_INHERIT;
1131 case PARAM_AUTH:
1132 $param = clean_param($param, PARAM_PLUGIN);
1133 if (empty($param)) {
1134 return '';
1135 } else if (exists_auth_plugin($param)) {
1136 return $param;
1137 } else {
1138 return '';
1141 case PARAM_LANG:
1142 $param = clean_param($param, PARAM_SAFEDIR);
1143 if (get_string_manager()->translation_exists($param)) {
1144 return $param;
1145 } else {
1146 // Specified language is not installed or param malformed.
1147 return '';
1150 case PARAM_THEME:
1151 $param = clean_param($param, PARAM_PLUGIN);
1152 if (empty($param)) {
1153 return '';
1154 } else if (file_exists("$CFG->dirroot/theme/$param/config.php")) {
1155 return $param;
1156 } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$param/config.php")) {
1157 return $param;
1158 } else {
1159 // Specified theme is not installed.
1160 return '';
1163 case PARAM_USERNAME:
1164 $param = fix_utf8($param);
1165 $param = str_replace(" " , "", $param);
1166 // Convert uppercase to lowercase MDL-16919.
1167 $param = core_text::strtolower($param);
1168 if (empty($CFG->extendedusernamechars)) {
1169 // Regular expression, eliminate all chars EXCEPT:
1170 // alphanum, dash (-), underscore (_), at sign (@) and period (.) characters.
1171 $param = preg_replace('/[^-\.@_a-z0-9]/', '', $param);
1173 return $param;
1175 case PARAM_EMAIL:
1176 $param = fix_utf8($param);
1177 if (validate_email($param)) {
1178 return $param;
1179 } else {
1180 return '';
1183 case PARAM_STRINGID:
1184 if (preg_match('|^[a-zA-Z][a-zA-Z0-9\.:/_-]*$|', $param)) {
1185 return $param;
1186 } else {
1187 return '';
1190 case PARAM_TIMEZONE:
1191 // Can be int, float(with .5 or .0) or string seperated by '/' and can have '-_'.
1192 $param = fix_utf8($param);
1193 $timezonepattern = '/^(([+-]?(0?[0-9](\.[5|0])?|1[0-3](\.0)?|1[0-2]\.5))|(99)|[[:alnum:]]+(\/?[[:alpha:]_-])+)$/';
1194 if (preg_match($timezonepattern, $param)) {
1195 return $param;
1196 } else {
1197 return '';
1200 default:
1201 // Doh! throw error, switched parameters in optional_param or another serious problem.
1202 print_error("unknownparamtype", '', '', $type);
1207 * Makes sure the data is using valid utf8, invalid characters are discarded.
1209 * Note: this function is not intended for full objects with methods and private properties.
1211 * @param mixed $value
1212 * @return mixed with proper utf-8 encoding
1214 function fix_utf8($value) {
1215 if (is_null($value) or $value === '') {
1216 return $value;
1218 } else if (is_string($value)) {
1219 if ((string)(int)$value === $value) {
1220 // Shortcut.
1221 return $value;
1224 // Lower error reporting because glibc throws bogus notices.
1225 $olderror = error_reporting();
1226 if ($olderror & E_NOTICE) {
1227 error_reporting($olderror ^ E_NOTICE);
1230 // Note: this duplicates min_fix_utf8() intentionally.
1231 static $buggyiconv = null;
1232 if ($buggyiconv === null) {
1233 $buggyiconv = (!function_exists('iconv') or iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€');
1236 if ($buggyiconv) {
1237 if (function_exists('mb_convert_encoding')) {
1238 $subst = mb_substitute_character();
1239 mb_substitute_character('');
1240 $result = mb_convert_encoding($value, 'utf-8', 'utf-8');
1241 mb_substitute_character($subst);
1243 } else {
1244 // Warn admins on admin/index.php page.
1245 $result = $value;
1248 } else {
1249 $result = iconv('UTF-8', 'UTF-8//IGNORE', $value);
1252 if ($olderror & E_NOTICE) {
1253 error_reporting($olderror);
1256 return $result;
1258 } else if (is_array($value)) {
1259 foreach ($value as $k => $v) {
1260 $value[$k] = fix_utf8($v);
1262 return $value;
1264 } else if (is_object($value)) {
1265 // Do not modify original.
1266 $value = clone($value);
1267 foreach ($value as $k => $v) {
1268 $value->$k = fix_utf8($v);
1270 return $value;
1272 } else {
1273 // This is some other type, no utf-8 here.
1274 return $value;
1279 * Return true if given value is integer or string with integer value
1281 * @param mixed $value String or Int
1282 * @return bool true if number, false if not
1284 function is_number($value) {
1285 if (is_int($value)) {
1286 return true;
1287 } else if (is_string($value)) {
1288 return ((string)(int)$value) === $value;
1289 } else {
1290 return false;
1295 * Returns host part from url.
1297 * @param string $url full url
1298 * @return string host, null if not found
1300 function get_host_from_url($url) {
1301 preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches);
1302 if ($matches) {
1303 return $matches[1];
1305 return null;
1309 * Tests whether anything was returned by text editor
1311 * This function is useful for testing whether something you got back from
1312 * the HTML editor actually contains anything. Sometimes the HTML editor
1313 * appear to be empty, but actually you get back a <br> tag or something.
1315 * @param string $string a string containing HTML.
1316 * @return boolean does the string contain any actual content - that is text,
1317 * images, objects, etc.
1319 function html_is_blank($string) {
1320 return trim(strip_tags($string, '<img><object><applet><input><select><textarea><hr>')) == '';
1324 * Set a key in global configuration
1326 * Set a key/value pair in both this session's {@link $CFG} global variable
1327 * and in the 'config' database table for future sessions.
1329 * Can also be used to update keys for plugin-scoped configs in config_plugin table.
1330 * In that case it doesn't affect $CFG.
1332 * A NULL value will delete the entry.
1334 * @param string $name the key to set
1335 * @param string $value the value to set (without magic quotes)
1336 * @param string $plugin (optional) the plugin scope, default null
1337 * @return bool true or exception
1339 function set_config($name, $value, $plugin=null) {
1340 global $CFG, $DB;
1342 if (empty($plugin)) {
1343 if (!array_key_exists($name, $CFG->config_php_settings)) {
1344 // So it's defined for this invocation at least.
1345 if (is_null($value)) {
1346 unset($CFG->$name);
1347 } else {
1348 // Settings from db are always strings.
1349 $CFG->$name = (string)$value;
1353 if ($DB->get_field('config', 'name', array('name' => $name))) {
1354 if ($value === null) {
1355 $DB->delete_records('config', array('name' => $name));
1356 } else {
1357 $DB->set_field('config', 'value', $value, array('name' => $name));
1359 } else {
1360 if ($value !== null) {
1361 $config = new stdClass();
1362 $config->name = $name;
1363 $config->value = $value;
1364 $DB->insert_record('config', $config, false);
1367 if ($name === 'siteidentifier') {
1368 cache_helper::update_site_identifier($value);
1370 cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1371 } else {
1372 // Plugin scope.
1373 if ($id = $DB->get_field('config_plugins', 'id', array('name' => $name, 'plugin' => $plugin))) {
1374 if ($value===null) {
1375 $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
1376 } else {
1377 $DB->set_field('config_plugins', 'value', $value, array('id' => $id));
1379 } else {
1380 if ($value !== null) {
1381 $config = new stdClass();
1382 $config->plugin = $plugin;
1383 $config->name = $name;
1384 $config->value = $value;
1385 $DB->insert_record('config_plugins', $config, false);
1388 cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1391 return true;
1395 * Get configuration values from the global config table
1396 * or the config_plugins table.
1398 * If called with one parameter, it will load all the config
1399 * variables for one plugin, and return them as an object.
1401 * If called with 2 parameters it will return a string single
1402 * value or false if the value is not found.
1404 * @static string|false $siteidentifier The site identifier is not cached. We use this static cache so
1405 * that we need only fetch it once per request.
1406 * @param string $plugin full component name
1407 * @param string $name default null
1408 * @return mixed hash-like object or single value, return false no config found
1409 * @throws dml_exception
1411 function get_config($plugin, $name = null) {
1412 global $CFG, $DB;
1414 static $siteidentifier = null;
1416 if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) {
1417 $forced =& $CFG->config_php_settings;
1418 $iscore = true;
1419 $plugin = 'core';
1420 } else {
1421 if (array_key_exists($plugin, $CFG->forced_plugin_settings)) {
1422 $forced =& $CFG->forced_plugin_settings[$plugin];
1423 } else {
1424 $forced = array();
1426 $iscore = false;
1429 if ($siteidentifier === null) {
1430 try {
1431 // This may fail during installation.
1432 // If you have a look at {@link initialise_cfg()} you will see that this is how we detect the need to
1433 // install the database.
1434 $siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
1435 } catch (dml_exception $ex) {
1436 // Set siteidentifier to false. We don't want to trip this continually.
1437 $siteidentifier = false;
1438 throw $ex;
1442 if (!empty($name)) {
1443 if (array_key_exists($name, $forced)) {
1444 return (string)$forced[$name];
1445 } else if ($name === 'siteidentifier' && $plugin == 'core') {
1446 return $siteidentifier;
1450 $cache = cache::make('core', 'config');
1451 $result = $cache->get($plugin);
1452 if ($result === false) {
1453 // The user is after a recordset.
1454 if (!$iscore) {
1455 $result = $DB->get_records_menu('config_plugins', array('plugin' => $plugin), '', 'name,value');
1456 } else {
1457 // This part is not really used any more, but anyway...
1458 $result = $DB->get_records_menu('config', array(), '', 'name,value');;
1460 $cache->set($plugin, $result);
1463 if (!empty($name)) {
1464 if (array_key_exists($name, $result)) {
1465 return $result[$name];
1467 return false;
1470 if ($plugin === 'core') {
1471 $result['siteidentifier'] = $siteidentifier;
1474 foreach ($forced as $key => $value) {
1475 if (is_null($value) or is_array($value) or is_object($value)) {
1476 // We do not want any extra mess here, just real settings that could be saved in db.
1477 unset($result[$key]);
1478 } else {
1479 // Convert to string as if it went through the DB.
1480 $result[$key] = (string)$value;
1484 return (object)$result;
1488 * Removes a key from global configuration.
1490 * @param string $name the key to set
1491 * @param string $plugin (optional) the plugin scope
1492 * @return boolean whether the operation succeeded.
1494 function unset_config($name, $plugin=null) {
1495 global $CFG, $DB;
1497 if (empty($plugin)) {
1498 unset($CFG->$name);
1499 $DB->delete_records('config', array('name' => $name));
1500 cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1501 } else {
1502 $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
1503 cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1506 return true;
1510 * Remove all the config variables for a given plugin.
1512 * NOTE: this function is called from lib/db/upgrade.php
1514 * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice';
1515 * @return boolean whether the operation succeeded.
1517 function unset_all_config_for_plugin($plugin) {
1518 global $DB;
1519 // Delete from the obvious config_plugins first.
1520 $DB->delete_records('config_plugins', array('plugin' => $plugin));
1521 // Next delete any suspect settings from config.
1522 $like = $DB->sql_like('name', '?', true, true, false, '|');
1523 $params = array($DB->sql_like_escape($plugin.'_', '|') . '%');
1524 $DB->delete_records_select('config', $like, $params);
1525 // Finally clear both the plugin cache and the core cache (suspect settings now removed from core).
1526 cache_helper::invalidate_by_definition('core', 'config', array(), array('core', $plugin));
1528 return true;
1532 * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability.
1534 * All users are verified if they still have the necessary capability.
1536 * @param string $value the value of the config setting.
1537 * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor.
1538 * @param bool $includeadmins include administrators.
1539 * @return array of user objects.
1541 function get_users_from_config($value, $capability, $includeadmins = true) {
1542 if (empty($value) or $value === '$@NONE@$') {
1543 return array();
1546 // We have to make sure that users still have the necessary capability,
1547 // it should be faster to fetch them all first and then test if they are present
1548 // instead of validating them one-by-one.
1549 $users = get_users_by_capability(context_system::instance(), $capability);
1550 if ($includeadmins) {
1551 $admins = get_admins();
1552 foreach ($admins as $admin) {
1553 $users[$admin->id] = $admin;
1557 if ($value === '$@ALL@$') {
1558 return $users;
1561 $result = array(); // Result in correct order.
1562 $allowed = explode(',', $value);
1563 foreach ($allowed as $uid) {
1564 if (isset($users[$uid])) {
1565 $user = $users[$uid];
1566 $result[$user->id] = $user;
1570 return $result;
1575 * Invalidates browser caches and cached data in temp.
1577 * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
1578 * {@link phpunit_util::reset_dataroot()}
1580 * @return void
1582 function purge_all_caches() {
1583 global $CFG;
1585 reset_text_filters_cache();
1586 js_reset_all_caches();
1587 theme_reset_all_caches();
1588 get_string_manager()->reset_caches();
1589 core_text::reset_caches();
1591 cache_helper::purge_all();
1593 // Purge all other caches: rss, simplepie, etc.
1594 remove_dir($CFG->cachedir.'', true);
1596 // Make sure cache dir is writable, throws exception if not.
1597 make_cache_directory('');
1599 // This is the only place where we purge local caches, we are only adding files there.
1600 // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes.
1601 remove_dir($CFG->localcachedir, true);
1602 set_config('localcachedirpurged', time());
1603 make_localcache_directory('', true);
1607 * Get volatile flags
1609 * @param string $type
1610 * @param int $changedsince default null
1611 * @return array records array
1613 function get_cache_flags($type, $changedsince = null) {
1614 global $DB;
1616 $params = array('type' => $type, 'expiry' => time());
1617 $sqlwhere = "flagtype = :type AND expiry >= :expiry";
1618 if ($changedsince !== null) {
1619 $params['changedsince'] = $changedsince;
1620 $sqlwhere .= " AND timemodified > :changedsince";
1622 $cf = array();
1623 if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) {
1624 foreach ($flags as $flag) {
1625 $cf[$flag->name] = $flag->value;
1628 return $cf;
1632 * Get volatile flags
1634 * @param string $type
1635 * @param string $name
1636 * @param int $changedsince default null
1637 * @return string|false The cache flag value or false
1639 function get_cache_flag($type, $name, $changedsince=null) {
1640 global $DB;
1642 $params = array('type' => $type, 'name' => $name, 'expiry' => time());
1644 $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry";
1645 if ($changedsince !== null) {
1646 $params['changedsince'] = $changedsince;
1647 $sqlwhere .= " AND timemodified > :changedsince";
1650 return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params);
1654 * Set a volatile flag
1656 * @param string $type the "type" namespace for the key
1657 * @param string $name the key to set
1658 * @param string $value the value to set (without magic quotes) - null will remove the flag
1659 * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs
1660 * @return bool Always returns true
1662 function set_cache_flag($type, $name, $value, $expiry = null) {
1663 global $DB;
1665 $timemodified = time();
1666 if ($expiry === null || $expiry < $timemodified) {
1667 $expiry = $timemodified + 24 * 60 * 60;
1668 } else {
1669 $expiry = (int)$expiry;
1672 if ($value === null) {
1673 unset_cache_flag($type, $name);
1674 return true;
1677 if ($f = $DB->get_record('cache_flags', array('name' => $name, 'flagtype' => $type), '*', IGNORE_MULTIPLE)) {
1678 // This is a potential problem in DEBUG_DEVELOPER.
1679 if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) {
1680 return true; // No need to update.
1682 $f->value = $value;
1683 $f->expiry = $expiry;
1684 $f->timemodified = $timemodified;
1685 $DB->update_record('cache_flags', $f);
1686 } else {
1687 $f = new stdClass();
1688 $f->flagtype = $type;
1689 $f->name = $name;
1690 $f->value = $value;
1691 $f->expiry = $expiry;
1692 $f->timemodified = $timemodified;
1693 $DB->insert_record('cache_flags', $f);
1695 return true;
1699 * Removes a single volatile flag
1701 * @param string $type the "type" namespace for the key
1702 * @param string $name the key to set
1703 * @return bool
1705 function unset_cache_flag($type, $name) {
1706 global $DB;
1707 $DB->delete_records('cache_flags', array('name' => $name, 'flagtype' => $type));
1708 return true;
1712 * Garbage-collect volatile flags
1714 * @return bool Always returns true
1716 function gc_cache_flags() {
1717 global $DB;
1718 $DB->delete_records_select('cache_flags', 'expiry < ?', array(time()));
1719 return true;
1722 // USER PREFERENCE API.
1725 * Refresh user preference cache. This is used most often for $USER
1726 * object that is stored in session, but it also helps with performance in cron script.
1728 * Preferences for each user are loaded on first use on every page, then again after the timeout expires.
1730 * @package core
1731 * @category preference
1732 * @access public
1733 * @param stdClass $user User object. Preferences are preloaded into 'preference' property
1734 * @param int $cachelifetime Cache life time on the current page (in seconds)
1735 * @throws coding_exception
1736 * @return null
1738 function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120) {
1739 global $DB;
1740 // Static cache, we need to check on each page load, not only every 2 minutes.
1741 static $loadedusers = array();
1743 if (!isset($user->id)) {
1744 throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field');
1747 if (empty($user->id) or isguestuser($user->id)) {
1748 // No permanent storage for not-logged-in users and guest.
1749 if (!isset($user->preference)) {
1750 $user->preference = array();
1752 return;
1755 $timenow = time();
1757 if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) {
1758 // Already loaded at least once on this page. Are we up to date?
1759 if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) {
1760 // No need to reload - we are on the same page and we loaded prefs just a moment ago.
1761 return;
1763 } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) {
1764 // No change since the lastcheck on this page.
1765 $user->preference['_lastloaded'] = $timenow;
1766 return;
1770 // OK, so we have to reload all preferences.
1771 $loadedusers[$user->id] = true;
1772 $user->preference = $DB->get_records_menu('user_preferences', array('userid' => $user->id), '', 'name,value'); // All values.
1773 $user->preference['_lastloaded'] = $timenow;
1777 * Called from set/unset_user_preferences, so that the prefs can be correctly reloaded in different sessions.
1779 * NOTE: internal function, do not call from other code.
1781 * @package core
1782 * @access private
1783 * @param integer $userid the user whose prefs were changed.
1785 function mark_user_preferences_changed($userid) {
1786 global $CFG;
1788 if (empty($userid) or isguestuser($userid)) {
1789 // No cache flags for guest and not-logged-in users.
1790 return;
1793 set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout);
1797 * Sets a preference for the specified user.
1799 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1801 * @package core
1802 * @category preference
1803 * @access public
1804 * @param string $name The key to set as preference for the specified user
1805 * @param string $value The value to set for the $name key in the specified user's
1806 * record, null means delete current value.
1807 * @param stdClass|int|null $user A moodle user object or id, null means current user
1808 * @throws coding_exception
1809 * @return bool Always true or exception
1811 function set_user_preference($name, $value, $user = null) {
1812 global $USER, $DB;
1814 if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1815 throw new coding_exception('Invalid preference name in set_user_preference() call');
1818 if (is_null($value)) {
1819 // Null means delete current.
1820 return unset_user_preference($name, $user);
1821 } else if (is_object($value)) {
1822 throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed');
1823 } else if (is_array($value)) {
1824 throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed');
1826 // Value column maximum length is 1333 characters.
1827 $value = (string)$value;
1828 if (core_text::strlen($value) > 1333) {
1829 throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column');
1832 if (is_null($user)) {
1833 $user = $USER;
1834 } else if (isset($user->id)) {
1835 // It is a valid object.
1836 } else if (is_numeric($user)) {
1837 $user = (object)array('id' => (int)$user);
1838 } else {
1839 throw new coding_exception('Invalid $user parameter in set_user_preference() call');
1842 check_user_preferences_loaded($user);
1844 if (empty($user->id) or isguestuser($user->id)) {
1845 // No permanent storage for not-logged-in users and guest.
1846 $user->preference[$name] = $value;
1847 return true;
1850 if ($preference = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => $name))) {
1851 if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) {
1852 // Preference already set to this value.
1853 return true;
1855 $DB->set_field('user_preferences', 'value', $value, array('id' => $preference->id));
1857 } else {
1858 $preference = new stdClass();
1859 $preference->userid = $user->id;
1860 $preference->name = $name;
1861 $preference->value = $value;
1862 $DB->insert_record('user_preferences', $preference);
1865 // Update value in cache.
1866 $user->preference[$name] = $value;
1868 // Set reload flag for other sessions.
1869 mark_user_preferences_changed($user->id);
1871 return true;
1875 * Sets a whole array of preferences for the current user
1877 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1879 * @package core
1880 * @category preference
1881 * @access public
1882 * @param array $prefarray An array of key/value pairs to be set
1883 * @param stdClass|int|null $user A moodle user object or id, null means current user
1884 * @return bool Always true or exception
1886 function set_user_preferences(array $prefarray, $user = null) {
1887 foreach ($prefarray as $name => $value) {
1888 set_user_preference($name, $value, $user);
1890 return true;
1894 * Unsets a preference completely by deleting it from the database
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 string $name The key to unset as preference for the specified user
1902 * @param stdClass|int|null $user A moodle user object or id, null means current user
1903 * @throws coding_exception
1904 * @return bool Always true or exception
1906 function unset_user_preference($name, $user = null) {
1907 global $USER, $DB;
1909 if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1910 throw new coding_exception('Invalid preference name in unset_user_preference() call');
1913 if (is_null($user)) {
1914 $user = $USER;
1915 } else if (isset($user->id)) {
1916 // It is a valid object.
1917 } else if (is_numeric($user)) {
1918 $user = (object)array('id' => (int)$user);
1919 } else {
1920 throw new coding_exception('Invalid $user parameter in unset_user_preference() call');
1923 check_user_preferences_loaded($user);
1925 if (empty($user->id) or isguestuser($user->id)) {
1926 // No permanent storage for not-logged-in user and guest.
1927 unset($user->preference[$name]);
1928 return true;
1931 // Delete from DB.
1932 $DB->delete_records('user_preferences', array('userid' => $user->id, 'name' => $name));
1934 // Delete the preference from cache.
1935 unset($user->preference[$name]);
1937 // Set reload flag for other sessions.
1938 mark_user_preferences_changed($user->id);
1940 return true;
1944 * Used to fetch user preference(s)
1946 * If no arguments are supplied this function will return
1947 * all of the current user preferences as an array.
1949 * If a name is specified then this function
1950 * attempts to return that particular preference value. If
1951 * none is found, then the optional value $default is returned,
1952 * otherwise null.
1954 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1956 * @package core
1957 * @category preference
1958 * @access public
1959 * @param string $name Name of the key to use in finding a preference value
1960 * @param mixed|null $default Value to be returned if the $name key is not set in the user preferences
1961 * @param stdClass|int|null $user A moodle user object or id, null means current user
1962 * @throws coding_exception
1963 * @return string|mixed|null A string containing the value of a single preference. An
1964 * array with all of the preferences or null
1966 function get_user_preferences($name = null, $default = null, $user = null) {
1967 global $USER;
1969 if (is_null($name)) {
1970 // All prefs.
1971 } else if (is_numeric($name) or $name === '_lastloaded') {
1972 throw new coding_exception('Invalid preference name in get_user_preferences() call');
1975 if (is_null($user)) {
1976 $user = $USER;
1977 } else if (isset($user->id)) {
1978 // Is a valid object.
1979 } else if (is_numeric($user)) {
1980 $user = (object)array('id' => (int)$user);
1981 } else {
1982 throw new coding_exception('Invalid $user parameter in get_user_preferences() call');
1985 check_user_preferences_loaded($user);
1987 if (empty($name)) {
1988 // All values.
1989 return $user->preference;
1990 } else if (isset($user->preference[$name])) {
1991 // The single string value.
1992 return $user->preference[$name];
1993 } else {
1994 // Default value (null if not specified).
1995 return $default;
1999 // FUNCTIONS FOR HANDLING TIME.
2002 * Given date parts in user time produce a GMT timestamp.
2004 * @package core
2005 * @category time
2006 * @param int $year The year part to create timestamp of
2007 * @param int $month The month part to create timestamp of
2008 * @param int $day The day part to create timestamp of
2009 * @param int $hour The hour part to create timestamp of
2010 * @param int $minute The minute part to create timestamp of
2011 * @param int $second The second part to create timestamp of
2012 * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset.
2013 * if 99 then default user's timezone is used {@link http://docs.moodle.org/dev/Time_API#Timezone}
2014 * @param bool $applydst Toggle Daylight Saving Time, default true, will be
2015 * applied only if timezone is 99 or string.
2016 * @return int GMT timestamp
2018 function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) {
2020 // Save input timezone, required for dst offset check.
2021 $passedtimezone = $timezone;
2023 $timezone = get_user_timezone_offset($timezone);
2025 if (abs($timezone) > 13) {
2026 // Server time.
2027 $time = mktime((int)$hour, (int)$minute, (int)$second, (int)$month, (int)$day, (int)$year);
2028 } else {
2029 $time = gmmktime((int)$hour, (int)$minute, (int)$second, (int)$month, (int)$day, (int)$year);
2030 $time = usertime($time, $timezone);
2032 // Apply dst for string timezones or if 99 then try dst offset with user's default timezone.
2033 if ($applydst && ((99 == $passedtimezone) || !is_numeric($passedtimezone))) {
2034 $time -= dst_offset_on($time, $passedtimezone);
2038 return $time;
2043 * Format a date/time (seconds) as weeks, days, hours etc as needed
2045 * Given an amount of time in seconds, returns string
2046 * formatted nicely as weeks, days, hours etc as needed
2048 * @package core
2049 * @category time
2050 * @uses MINSECS
2051 * @uses HOURSECS
2052 * @uses DAYSECS
2053 * @uses YEARSECS
2054 * @param int $totalsecs Time in seconds
2055 * @param stdClass $str Should be a time object
2056 * @return string A nicely formatted date/time string
2058 function format_time($totalsecs, $str = null) {
2060 $totalsecs = abs($totalsecs);
2062 if (!$str) {
2063 // Create the str structure the slow way.
2064 $str = new stdClass();
2065 $str->day = get_string('day');
2066 $str->days = get_string('days');
2067 $str->hour = get_string('hour');
2068 $str->hours = get_string('hours');
2069 $str->min = get_string('min');
2070 $str->mins = get_string('mins');
2071 $str->sec = get_string('sec');
2072 $str->secs = get_string('secs');
2073 $str->year = get_string('year');
2074 $str->years = get_string('years');
2077 $years = floor($totalsecs/YEARSECS);
2078 $remainder = $totalsecs - ($years*YEARSECS);
2079 $days = floor($remainder/DAYSECS);
2080 $remainder = $totalsecs - ($days*DAYSECS);
2081 $hours = floor($remainder/HOURSECS);
2082 $remainder = $remainder - ($hours*HOURSECS);
2083 $mins = floor($remainder/MINSECS);
2084 $secs = $remainder - ($mins*MINSECS);
2086 $ss = ($secs == 1) ? $str->sec : $str->secs;
2087 $sm = ($mins == 1) ? $str->min : $str->mins;
2088 $sh = ($hours == 1) ? $str->hour : $str->hours;
2089 $sd = ($days == 1) ? $str->day : $str->days;
2090 $sy = ($years == 1) ? $str->year : $str->years;
2092 $oyears = '';
2093 $odays = '';
2094 $ohours = '';
2095 $omins = '';
2096 $osecs = '';
2098 if ($years) {
2099 $oyears = $years .' '. $sy;
2101 if ($days) {
2102 $odays = $days .' '. $sd;
2104 if ($hours) {
2105 $ohours = $hours .' '. $sh;
2107 if ($mins) {
2108 $omins = $mins .' '. $sm;
2110 if ($secs) {
2111 $osecs = $secs .' '. $ss;
2114 if ($years) {
2115 return trim($oyears .' '. $odays);
2117 if ($days) {
2118 return trim($odays .' '. $ohours);
2120 if ($hours) {
2121 return trim($ohours .' '. $omins);
2123 if ($mins) {
2124 return trim($omins .' '. $osecs);
2126 if ($secs) {
2127 return $osecs;
2129 return get_string('now');
2133 * Returns a formatted string that represents a date in user time
2135 * Returns a formatted string that represents a date in user time
2136 * <b>WARNING: note that the format is for strftime(), not date().</b>
2137 * Because of a bug in most Windows time libraries, we can't use
2138 * the nicer %e, so we have to use %d which has leading zeroes.
2139 * A lot of the fuss in the function is just getting rid of these leading
2140 * zeroes as efficiently as possible.
2142 * If parameter fixday = true (default), then take off leading
2143 * zero from %d, else maintain it.
2145 * @package core
2146 * @category time
2147 * @param int $date the timestamp in UTC, as obtained from the database.
2148 * @param string $format strftime format. You should probably get this using
2149 * get_string('strftime...', 'langconfig');
2150 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
2151 * not 99 then daylight saving will not be added.
2152 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2153 * @param bool $fixday If true (default) then the leading zero from %d is removed.
2154 * If false then the leading zero is maintained.
2155 * @param bool $fixhour If true (default) then the leading zero from %I is removed.
2156 * @return string the formatted date/time.
2158 function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
2160 global $CFG;
2162 if (empty($format)) {
2163 $format = get_string('strftimedaydatetime', 'langconfig');
2166 if (!empty($CFG->nofixday)) {
2167 // Config.php can force %d not to be fixed.
2168 $fixday = false;
2169 } else if ($fixday) {
2170 $formatnoday = str_replace('%d', 'DD', $format);
2171 $fixday = ($formatnoday != $format);
2172 $format = $formatnoday;
2175 // Note: This logic about fixing 12-hour time to remove unnecessary leading
2176 // zero is required because on Windows, PHP strftime function does not
2177 // support the correct 'hour without leading zero' parameter (%l).
2178 if (!empty($CFG->nofixhour)) {
2179 // Config.php can force %I not to be fixed.
2180 $fixhour = false;
2181 } else if ($fixhour) {
2182 $formatnohour = str_replace('%I', 'HH', $format);
2183 $fixhour = ($formatnohour != $format);
2184 $format = $formatnohour;
2187 // Add daylight saving offset for string timezones only, as we can't get dst for
2188 // float values. if timezone is 99 (user default timezone), then try update dst.
2189 if ((99 == $timezone) || !is_numeric($timezone)) {
2190 $date += dst_offset_on($date, $timezone);
2193 $timezone = get_user_timezone_offset($timezone);
2195 // If we are running under Windows convert to windows encoding and then back to UTF-8
2196 // (because it's impossible to specify UTF-8 to fetch locale info in Win32).
2198 if (abs($timezone) > 13) {
2199 // Server time.
2200 $datestring = date_format_string($date, $format, $timezone);
2201 if ($fixday) {
2202 $daystring = ltrim(str_replace(array(' 0', ' '), '', strftime(' %d', $date)));
2203 $datestring = str_replace('DD', $daystring, $datestring);
2205 if ($fixhour) {
2206 $hourstring = ltrim(str_replace(array(' 0', ' '), '', strftime(' %I', $date)));
2207 $datestring = str_replace('HH', $hourstring, $datestring);
2210 } else {
2211 $date += (int)($timezone * 3600);
2212 $datestring = date_format_string($date, $format, $timezone);
2213 if ($fixday) {
2214 $daystring = ltrim(str_replace(array(' 0', ' '), '', gmstrftime(' %d', $date)));
2215 $datestring = str_replace('DD', $daystring, $datestring);
2217 if ($fixhour) {
2218 $hourstring = ltrim(str_replace(array(' 0', ' '), '', gmstrftime(' %I', $date)));
2219 $datestring = str_replace('HH', $hourstring, $datestring);
2223 return $datestring;
2227 * Returns a formatted date ensuring it is UTF-8.
2229 * If we are running under Windows convert to Windows encoding and then back to UTF-8
2230 * (because it's impossible to specify UTF-8 to fetch locale info in Win32).
2232 * This function does not do any calculation regarding the user preferences and should
2233 * therefore receive the final date timestamp, format and timezone. Timezone being only used
2234 * to differentiate the use of server time or not (strftime() against gmstrftime()).
2236 * @param int $date the timestamp.
2237 * @param string $format strftime format.
2238 * @param int|float $tz the numerical timezone, typically returned by {@link get_user_timezone_offset()}.
2239 * @return string the formatted date/time.
2240 * @since 2.3.3
2242 function date_format_string($date, $format, $tz = 99) {
2243 global $CFG;
2244 if (abs($tz) > 13) {
2245 if ($CFG->ostype == 'WINDOWS' and $localewincharset = get_string('localewincharset', 'langconfig')) {
2246 $format = core_text::convert($format, 'utf-8', $localewincharset);
2247 $datestring = strftime($format, $date);
2248 $datestring = core_text::convert($datestring, $localewincharset, 'utf-8');
2249 } else {
2250 $datestring = strftime($format, $date);
2252 } else {
2253 if ($CFG->ostype == 'WINDOWS' and $localewincharset = get_string('localewincharset', 'langconfig')) {
2254 $format = core_text::convert($format, 'utf-8', $localewincharset);
2255 $datestring = gmstrftime($format, $date);
2256 $datestring = core_text::convert($datestring, $localewincharset, 'utf-8');
2257 } else {
2258 $datestring = gmstrftime($format, $date);
2261 return $datestring;
2265 * Given a $time timestamp in GMT (seconds since epoch),
2266 * returns an array that represents the date in user time
2268 * @package core
2269 * @category time
2270 * @uses HOURSECS
2271 * @param int $time Timestamp in GMT
2272 * @param float|int|string $timezone offset's time with timezone, if float and not 99, then no
2273 * dst offset is applied {@link http://docs.moodle.org/dev/Time_API#Timezone}
2274 * @return array An array that represents the date in user time
2276 function usergetdate($time, $timezone=99) {
2278 // Save input timezone, required for dst offset check.
2279 $passedtimezone = $timezone;
2281 $timezone = get_user_timezone_offset($timezone);
2283 if (abs($timezone) > 13) {
2284 // Server time.
2285 return getdate($time);
2288 // Add daylight saving offset for string timezones only, as we can't get dst for
2289 // float values. if timezone is 99 (user default timezone), then try update dst.
2290 if ($passedtimezone == 99 || !is_numeric($passedtimezone)) {
2291 $time += dst_offset_on($time, $passedtimezone);
2294 $time += intval((float)$timezone * HOURSECS);
2296 $datestring = gmstrftime('%B_%A_%j_%Y_%m_%w_%d_%H_%M_%S', $time);
2298 // Be careful to ensure the returned array matches that produced by getdate() above.
2299 list(
2300 $getdate['month'],
2301 $getdate['weekday'],
2302 $getdate['yday'],
2303 $getdate['year'],
2304 $getdate['mon'],
2305 $getdate['wday'],
2306 $getdate['mday'],
2307 $getdate['hours'],
2308 $getdate['minutes'],
2309 $getdate['seconds']
2310 ) = explode('_', $datestring);
2312 // Set correct datatype to match with getdate().
2313 $getdate['seconds'] = (int)$getdate['seconds'];
2314 $getdate['yday'] = (int)$getdate['yday'] - 1; // The function gmstrftime returns 0 through 365.
2315 $getdate['year'] = (int)$getdate['year'];
2316 $getdate['mon'] = (int)$getdate['mon'];
2317 $getdate['wday'] = (int)$getdate['wday'];
2318 $getdate['mday'] = (int)$getdate['mday'];
2319 $getdate['hours'] = (int)$getdate['hours'];
2320 $getdate['minutes'] = (int)$getdate['minutes'];
2321 return $getdate;
2325 * Given a GMT timestamp (seconds since epoch), offsets it by
2326 * the timezone. eg 3pm in India is 3pm GMT - 7 * 3600 seconds
2328 * @package core
2329 * @category time
2330 * @uses HOURSECS
2331 * @param int $date Timestamp in GMT
2332 * @param float|int|string $timezone timezone to calculate GMT time offset before
2333 * calculating user time, 99 is default user timezone
2334 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2335 * @return int
2337 function usertime($date, $timezone=99) {
2339 $timezone = get_user_timezone_offset($timezone);
2341 if (abs($timezone) > 13) {
2342 return $date;
2344 return $date - (int)($timezone * HOURSECS);
2348 * Given a time, return the GMT timestamp of the most recent midnight
2349 * for the current user.
2351 * @package core
2352 * @category time
2353 * @param int $date Timestamp in GMT
2354 * @param float|int|string $timezone timezone to calculate GMT time offset before
2355 * calculating user midnight time, 99 is default user timezone
2356 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2357 * @return int Returns a GMT timestamp
2359 function usergetmidnight($date, $timezone=99) {
2361 $userdate = usergetdate($date, $timezone);
2363 // Time of midnight of this user's day, in GMT.
2364 return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone);
2369 * Returns a string that prints the user's timezone
2371 * @package core
2372 * @category time
2373 * @param float|int|string $timezone timezone to calculate GMT time offset before
2374 * calculating user timezone, 99 is default user timezone
2375 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2376 * @return string
2378 function usertimezone($timezone=99) {
2380 $tz = get_user_timezone($timezone);
2382 if (!is_float($tz)) {
2383 return $tz;
2386 if (abs($tz) > 13) {
2387 // Server time.
2388 return get_string('serverlocaltime');
2391 if ($tz == intval($tz)) {
2392 // Don't show .0 for whole hours.
2393 $tz = intval($tz);
2396 if ($tz == 0) {
2397 return 'UTC';
2398 } else if ($tz > 0) {
2399 return 'UTC+'.$tz;
2400 } else {
2401 return 'UTC'.$tz;
2407 * Returns a float which represents the user's timezone difference from GMT in hours
2408 * Checks various settings and picks the most dominant of those which have a value
2410 * @package core
2411 * @category time
2412 * @param float|int|string $tz timezone to calculate GMT time offset for user,
2413 * 99 is default user timezone
2414 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2415 * @return float
2417 function get_user_timezone_offset($tz = 99) {
2418 $tz = get_user_timezone($tz);
2420 if (is_float($tz)) {
2421 return $tz;
2422 } else {
2423 $tzrecord = get_timezone_record($tz);
2424 if (empty($tzrecord)) {
2425 return 99.0;
2427 return (float)$tzrecord->gmtoff / HOURMINS;
2432 * Returns an int which represents the systems's timezone difference from GMT in seconds
2434 * @package core
2435 * @category time
2436 * @param float|int|string $tz timezone for which offset is required.
2437 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2438 * @return int|bool if found, false is timezone 99 or error
2440 function get_timezone_offset($tz) {
2441 if ($tz == 99) {
2442 return false;
2445 if (is_numeric($tz)) {
2446 return intval($tz * 60*60);
2449 if (!$tzrecord = get_timezone_record($tz)) {
2450 return false;
2452 return intval($tzrecord->gmtoff * 60);
2456 * Returns a float or a string which denotes the user's timezone
2457 * 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)
2458 * means that for this timezone there are also DST rules to be taken into account
2459 * Checks various settings and picks the most dominant of those which have a value
2461 * @package core
2462 * @category time
2463 * @param float|int|string $tz timezone to calculate GMT time offset before
2464 * calculating user timezone, 99 is default user timezone
2465 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2466 * @return float|string
2468 function get_user_timezone($tz = 99) {
2469 global $USER, $CFG;
2471 $timezones = array(
2472 $tz,
2473 isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99,
2474 isset($USER->timezone) ? $USER->timezone : 99,
2475 isset($CFG->timezone) ? $CFG->timezone : 99,
2478 $tz = 99;
2480 // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array.
2481 while (((empty($tz) && !is_numeric($tz)) || $tz == 99) && $next = each($timezones)) {
2482 $tz = $next['value'];
2484 return is_numeric($tz) ? (float) $tz : $tz;
2488 * Returns cached timezone record for given $timezonename
2490 * @package core
2491 * @param string $timezonename name of the timezone
2492 * @return stdClass|bool timezonerecord or false
2494 function get_timezone_record($timezonename) {
2495 global $DB;
2496 static $cache = null;
2498 if ($cache === null) {
2499 $cache = array();
2502 if (isset($cache[$timezonename])) {
2503 return $cache[$timezonename];
2506 return $cache[$timezonename] = $DB->get_record_sql('SELECT * FROM {timezone}
2507 WHERE name = ? ORDER BY year DESC', array($timezonename), IGNORE_MULTIPLE);
2511 * Build and store the users Daylight Saving Time (DST) table
2513 * @package core
2514 * @param int $fromyear Start year for the table, defaults to 1971
2515 * @param int $toyear End year for the table, defaults to 2035
2516 * @param int|float|string $strtimezone timezone to check if dst should be applied.
2517 * @return bool
2519 function calculate_user_dst_table($fromyear = null, $toyear = null, $strtimezone = null) {
2520 global $SESSION, $DB;
2522 $usertz = get_user_timezone($strtimezone);
2524 if (is_float($usertz)) {
2525 // Trivial timezone, no DST.
2526 return false;
2529 if (!empty($SESSION->dst_offsettz) && $SESSION->dst_offsettz != $usertz) {
2530 // We have pre-calculated values, but the user's effective TZ has changed in the meantime, so reset.
2531 unset($SESSION->dst_offsets);
2532 unset($SESSION->dst_range);
2535 if (!empty($SESSION->dst_offsets) && empty($fromyear) && empty($toyear)) {
2536 // Repeat calls which do not request specific year ranges stop here, we have already calculated the table.
2537 // This will be the return path most of the time, pretty light computationally.
2538 return true;
2541 // Reaching here means we either need to extend our table or create it from scratch.
2543 // Remember which TZ we calculated these changes for.
2544 $SESSION->dst_offsettz = $usertz;
2546 if (empty($SESSION->dst_offsets)) {
2547 // If we 're creating from scratch, put the two guard elements in there.
2548 $SESSION->dst_offsets = array(1 => null, 0 => null);
2550 if (empty($SESSION->dst_range)) {
2551 // If creating from scratch.
2552 $from = max((empty($fromyear) ? intval(date('Y')) - 3 : $fromyear), 1971);
2553 $to = min((empty($toyear) ? intval(date('Y')) + 3 : $toyear), 2035);
2555 // Fill in the array with the extra years we need to process.
2556 $yearstoprocess = array();
2557 for ($i = $from; $i <= $to; ++$i) {
2558 $yearstoprocess[] = $i;
2561 // Take note of which years we have processed for future calls.
2562 $SESSION->dst_range = array($from, $to);
2563 } else {
2564 // If needing to extend the table, do the same.
2565 $yearstoprocess = array();
2567 $from = max((empty($fromyear) ? $SESSION->dst_range[0] : $fromyear), 1971);
2568 $to = min((empty($toyear) ? $SESSION->dst_range[1] : $toyear), 2035);
2570 if ($from < $SESSION->dst_range[0]) {
2571 // Take note of which years we need to process and then note that we have processed them for future calls.
2572 for ($i = $from; $i < $SESSION->dst_range[0]; ++$i) {
2573 $yearstoprocess[] = $i;
2575 $SESSION->dst_range[0] = $from;
2577 if ($to > $SESSION->dst_range[1]) {
2578 // Take note of which years we need to process and then note that we have processed them for future calls.
2579 for ($i = $SESSION->dst_range[1] + 1; $i <= $to; ++$i) {
2580 $yearstoprocess[] = $i;
2582 $SESSION->dst_range[1] = $to;
2586 if (empty($yearstoprocess)) {
2587 // This means that there was a call requesting a SMALLER range than we have already calculated.
2588 return true;
2591 // From now on, we know that the array has at least the two guard elements, and $yearstoprocess has the years we need
2592 // Also, the array is sorted in descending timestamp order!
2594 // Get DB data.
2596 static $presetscache = array();
2597 if (!isset($presetscache[$usertz])) {
2598 $presetscache[$usertz] = $DB->get_records('timezone', array('name' => $usertz),
2599 'year DESC', 'year, gmtoff, dstoff, dst_month, dst_startday, dst_weekday, dst_skipweeks, dst_time, std_month, '.
2600 'std_startday, std_weekday, std_skipweeks, std_time');
2602 if (empty($presetscache[$usertz])) {
2603 return false;
2606 // Remove ending guard (first element of the array).
2607 reset($SESSION->dst_offsets);
2608 unset($SESSION->dst_offsets[key($SESSION->dst_offsets)]);
2610 // Add all required change timestamps.
2611 foreach ($yearstoprocess as $y) {
2612 // Find the record which is in effect for the year $y.
2613 foreach ($presetscache[$usertz] as $year => $preset) {
2614 if ($year <= $y) {
2615 break;
2619 $changes = dst_changes_for_year($y, $preset);
2621 if ($changes === null) {
2622 continue;
2624 if ($changes['dst'] != 0) {
2625 $SESSION->dst_offsets[$changes['dst']] = $preset->dstoff * MINSECS;
2627 if ($changes['std'] != 0) {
2628 $SESSION->dst_offsets[$changes['std']] = 0;
2632 // Put in a guard element at the top.
2633 $maxtimestamp = max(array_keys($SESSION->dst_offsets));
2634 $SESSION->dst_offsets[($maxtimestamp + DAYSECS)] = null; // DAYSECS is arbitrary, any "small" number will do.
2636 // Sort again.
2637 krsort($SESSION->dst_offsets);
2639 return true;
2643 * Calculates the required DST change and returns a Timestamp Array
2645 * @package core
2646 * @category time
2647 * @uses HOURSECS
2648 * @uses MINSECS
2649 * @param int|string $year Int or String Year to focus on
2650 * @param object $timezone Instatiated Timezone object
2651 * @return array|null Array dst => xx, 0 => xx, std => yy, 1 => yy or null
2653 function dst_changes_for_year($year, $timezone) {
2655 if ($timezone->dst_startday == 0 && $timezone->dst_weekday == 0 &&
2656 $timezone->std_startday == 0 && $timezone->std_weekday == 0) {
2657 return null;
2660 $monthdaydst = find_day_in_month($timezone->dst_startday, $timezone->dst_weekday, $timezone->dst_month, $year);
2661 $monthdaystd = find_day_in_month($timezone->std_startday, $timezone->std_weekday, $timezone->std_month, $year);
2663 list($dsthour, $dstmin) = explode(':', $timezone->dst_time);
2664 list($stdhour, $stdmin) = explode(':', $timezone->std_time);
2666 $timedst = make_timestamp($year, $timezone->dst_month, $monthdaydst, 0, 0, 0, 99, false);
2667 $timestd = make_timestamp($year, $timezone->std_month, $monthdaystd, 0, 0, 0, 99, false);
2669 // Instead of putting hour and minute in make_timestamp(), we add them afterwards.
2670 // This has the advantage of being able to have negative values for hour, i.e. for timezones
2671 // where GMT time would be in the PREVIOUS day than the local one on which DST changes.
2673 $timedst += $dsthour * HOURSECS + $dstmin * MINSECS;
2674 $timestd += $stdhour * HOURSECS + $stdmin * MINSECS;
2676 return array('dst' => $timedst, 0 => $timedst, 'std' => $timestd, 1 => $timestd);
2680 * Calculates the Daylight Saving Offset for a given date/time (timestamp)
2681 * - Note: Daylight saving only works for string timezones and not for float.
2683 * @package core
2684 * @category time
2685 * @param int $time must NOT be compensated at all, it has to be a pure timestamp
2686 * @param int|float|string $strtimezone timezone for which offset is expected, if 99 or null
2687 * then user's default timezone is used. {@link http://docs.moodle.org/dev/Time_API#Timezone}
2688 * @return int
2690 function dst_offset_on($time, $strtimezone = null) {
2691 global $SESSION;
2693 if (!calculate_user_dst_table(null, null, $strtimezone) || empty($SESSION->dst_offsets)) {
2694 return 0;
2697 reset($SESSION->dst_offsets);
2698 while (list($from, $offset) = each($SESSION->dst_offsets)) {
2699 if ($from <= $time) {
2700 break;
2704 // This is the normal return path.
2705 if ($offset !== null) {
2706 return $offset;
2709 // Reaching this point means we haven't calculated far enough, do it now:
2710 // Calculate extra DST changes if needed and recurse. The recursion always
2711 // moves toward the stopping condition, so will always end.
2713 if ($from == 0) {
2714 // We need a year smaller than $SESSION->dst_range[0].
2715 if ($SESSION->dst_range[0] == 1971) {
2716 return 0;
2718 calculate_user_dst_table($SESSION->dst_range[0] - 5, null, $strtimezone);
2719 return dst_offset_on($time, $strtimezone);
2720 } else {
2721 // We need a year larger than $SESSION->dst_range[1].
2722 if ($SESSION->dst_range[1] == 2035) {
2723 return 0;
2725 calculate_user_dst_table(null, $SESSION->dst_range[1] + 5, $strtimezone);
2726 return dst_offset_on($time, $strtimezone);
2731 * Calculates when the day appears in specific month
2733 * @package core
2734 * @category time
2735 * @param int $startday starting day of the month
2736 * @param int $weekday The day when week starts (normally taken from user preferences)
2737 * @param int $month The month whose day is sought
2738 * @param int $year The year of the month whose day is sought
2739 * @return int
2741 function find_day_in_month($startday, $weekday, $month, $year) {
2743 $daysinmonth = days_in_month($month, $year);
2745 if ($weekday == -1) {
2746 // Don't care about weekday, so return:
2747 // abs($startday) if $startday != -1
2748 // $daysinmonth otherwise.
2749 return ($startday == -1) ? $daysinmonth : abs($startday);
2752 // From now on we 're looking for a specific weekday.
2754 // Give "end of month" its actual value, since we know it.
2755 if ($startday == -1) {
2756 $startday = -1 * $daysinmonth;
2759 // Starting from day $startday, the sign is the direction.
2761 if ($startday < 1) {
2763 $startday = abs($startday);
2764 $lastmonthweekday = strftime('%w', mktime(12, 0, 0, $month, $daysinmonth, $year));
2766 // This is the last such weekday of the month.
2767 $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday;
2768 if ($lastinmonth > $daysinmonth) {
2769 $lastinmonth -= 7;
2772 // Find the first such weekday <= $startday.
2773 while ($lastinmonth > $startday) {
2774 $lastinmonth -= 7;
2777 return $lastinmonth;
2779 } else {
2781 $indexweekday = strftime('%w', mktime(12, 0, 0, $month, $startday, $year));
2783 $diff = $weekday - $indexweekday;
2784 if ($diff < 0) {
2785 $diff += 7;
2788 // This is the first such weekday of the month equal to or after $startday.
2789 $firstfromindex = $startday + $diff;
2791 return $firstfromindex;
2797 * Calculate the number of days in a given month
2799 * @package core
2800 * @category time
2801 * @param int $month The month whose day count is sought
2802 * @param int $year The year of the month whose day count is sought
2803 * @return int
2805 function days_in_month($month, $year) {
2806 return intval(date('t', mktime(12, 0, 0, $month, 1, $year)));
2810 * Calculate the position in the week of a specific calendar day
2812 * @package core
2813 * @category time
2814 * @param int $day The day of the date whose position in the week is sought
2815 * @param int $month The month of the date whose position in the week is sought
2816 * @param int $year The year of the date whose position in the week is sought
2817 * @return int
2819 function dayofweek($day, $month, $year) {
2820 // I wonder if this is any different from strftime('%w', mktime(12, 0, 0, $month, $daysinmonth, $year, 0));.
2821 return intval(date('w', mktime(12, 0, 0, $month, $day, $year)));
2824 // USER AUTHENTICATION AND LOGIN.
2827 * Returns full login url.
2829 * @return string login url
2831 function get_login_url() {
2832 global $CFG;
2834 $url = "$CFG->wwwroot/login/index.php";
2836 if (!empty($CFG->loginhttps)) {
2837 $url = str_replace('http:', 'https:', $url);
2840 return $url;
2844 * This function checks that the current user is logged in and has the
2845 * required privileges
2847 * This function checks that the current user is logged in, and optionally
2848 * whether they are allowed to be in a particular course and view a particular
2849 * course module.
2850 * If they are not logged in, then it redirects them to the site login unless
2851 * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which
2852 * case they are automatically logged in as guests.
2853 * If $courseid is given and the user is not enrolled in that course then the
2854 * user is redirected to the course enrolment page.
2855 * If $cm is given and the course module is hidden and the user is not a teacher
2856 * in the course then the user is redirected to the course home page.
2858 * When $cm parameter specified, this function sets page layout to 'module'.
2859 * You need to change it manually later if some other layout needed.
2861 * @package core_access
2862 * @category access
2864 * @param mixed $courseorid id of the course or course object
2865 * @param bool $autologinguest default true
2866 * @param object $cm course module object
2867 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2868 * true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2869 * in order to keep redirects working properly. MDL-14495
2870 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2871 * @return mixed Void, exit, and die depending on path
2872 * @throws coding_exception
2873 * @throws require_login_exception
2875 function require_login($courseorid = null, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
2876 global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT;
2878 // Must not redirect when byteserving already started.
2879 if (!empty($_SERVER['HTTP_RANGE'])) {
2880 $preventredirect = true;
2883 // Setup global $COURSE, themes, language and locale.
2884 if (!empty($courseorid)) {
2885 if (is_object($courseorid)) {
2886 $course = $courseorid;
2887 } else if ($courseorid == SITEID) {
2888 $course = clone($SITE);
2889 } else {
2890 $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST);
2892 if ($cm) {
2893 if ($cm->course != $course->id) {
2894 throw new coding_exception('course and cm parameters in require_login() call do not match!!');
2896 // Make sure we have a $cm from get_fast_modinfo as this contains activity access details.
2897 if (!($cm instanceof cm_info)) {
2898 // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
2899 // db queries so this is not really a performance concern, however it is obviously
2900 // better if you use get_fast_modinfo to get the cm before calling this.
2901 $modinfo = get_fast_modinfo($course);
2902 $cm = $modinfo->get_cm($cm->id);
2904 $PAGE->set_cm($cm, $course); // Set's up global $COURSE.
2905 $PAGE->set_pagelayout('incourse');
2906 } else {
2907 $PAGE->set_course($course); // Set's up global $COURSE.
2909 } else {
2910 // Do not touch global $COURSE via $PAGE->set_course(),
2911 // the reasons is we need to be able to call require_login() at any time!!
2912 $course = $SITE;
2913 if ($cm) {
2914 throw new coding_exception('cm parameter in require_login() requires valid course parameter!');
2918 // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false.
2919 // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future
2920 // risk leading the user back to the AJAX request URL.
2921 if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) {
2922 $setwantsurltome = false;
2925 // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour.
2926 if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !$preventredirect && !empty($CFG->dbsessions)) {
2927 if ($setwantsurltome) {
2928 $SESSION->wantsurl = qualified_me();
2930 redirect(get_login_url());
2933 // If the user is not even logged in yet then make sure they are.
2934 if (!isloggedin()) {
2935 if ($autologinguest and !empty($CFG->guestloginbutton) and !empty($CFG->autologinguests)) {
2936 if (!$guest = get_complete_user_data('id', $CFG->siteguest)) {
2937 // Misconfigured site guest, just redirect to login page.
2938 redirect(get_login_url());
2939 exit; // Never reached.
2941 $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang;
2942 complete_user_login($guest);
2943 $USER->autologinguest = true;
2944 $SESSION->lang = $lang;
2945 } else {
2946 // NOTE: $USER->site check was obsoleted by session test cookie, $USER->confirmed test is in login/index.php.
2947 if ($preventredirect) {
2948 throw new require_login_exception('You are not logged in');
2951 if ($setwantsurltome) {
2952 $SESSION->wantsurl = qualified_me();
2954 if (!empty($_SERVER['HTTP_REFERER'])) {
2955 $SESSION->fromurl = $_SERVER['HTTP_REFERER'];
2957 redirect(get_login_url());
2958 exit; // Never reached.
2962 // Loginas as redirection if needed.
2963 if ($course->id != SITEID and session_is_loggedinas()) {
2964 if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) {
2965 if ($USER->loginascontext->instanceid != $course->id) {
2966 print_error('loginasonecourse', '', $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid);
2971 // Check whether the user should be changing password (but only if it is REALLY them).
2972 if (get_user_preferences('auth_forcepasswordchange') && !session_is_loggedinas()) {
2973 $userauth = get_auth_plugin($USER->auth);
2974 if ($userauth->can_change_password() and !$preventredirect) {
2975 if ($setwantsurltome) {
2976 $SESSION->wantsurl = qualified_me();
2978 if ($changeurl = $userauth->change_password_url()) {
2979 // Use plugin custom url.
2980 redirect($changeurl);
2981 } else {
2982 // Use moodle internal method.
2983 if (empty($CFG->loginhttps)) {
2984 redirect($CFG->wwwroot .'/login/change_password.php');
2985 } else {
2986 $wwwroot = str_replace('http:', 'https:', $CFG->wwwroot);
2987 redirect($wwwroot .'/login/change_password.php');
2990 } else {
2991 print_error('nopasswordchangeforced', 'auth');
2995 // Check that the user account is properly set up.
2996 if (user_not_fully_set_up($USER)) {
2997 if ($preventredirect) {
2998 throw new require_login_exception('User not fully set-up');
3000 if ($setwantsurltome) {
3001 $SESSION->wantsurl = qualified_me();
3003 redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&amp;course='. SITEID);
3006 // Make sure the USER has a sesskey set up. Used for CSRF protection.
3007 sesskey();
3009 // Do not bother admins with any formalities.
3010 if (is_siteadmin()) {
3011 // Set accesstime or the user will appear offline which messes up messaging.
3012 user_accesstime_log($course->id);
3013 return;
3016 // Check that the user has agreed to a site policy if there is one - do not test in case of admins.
3017 if (!$USER->policyagreed and !is_siteadmin()) {
3018 if (!empty($CFG->sitepolicy) and !isguestuser()) {
3019 if ($preventredirect) {
3020 throw new require_login_exception('Policy not agreed');
3022 if ($setwantsurltome) {
3023 $SESSION->wantsurl = qualified_me();
3025 redirect($CFG->wwwroot .'/user/policy.php');
3026 } else if (!empty($CFG->sitepolicyguest) and isguestuser()) {
3027 if ($preventredirect) {
3028 throw new require_login_exception('Policy not agreed');
3030 if ($setwantsurltome) {
3031 $SESSION->wantsurl = qualified_me();
3033 redirect($CFG->wwwroot .'/user/policy.php');
3037 // Fetch the system context, the course context, and prefetch its child contexts.
3038 $sysctx = context_system::instance();
3039 $coursecontext = context_course::instance($course->id, MUST_EXIST);
3040 if ($cm) {
3041 $cmcontext = context_module::instance($cm->id, MUST_EXIST);
3042 } else {
3043 $cmcontext = null;
3046 // If the site is currently under maintenance, then print a message.
3047 if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:config', $sysctx)) {
3048 if ($preventredirect) {
3049 throw new require_login_exception('Maintenance in progress');
3052 print_maintenance_message();
3055 // Make sure the course itself is not hidden.
3056 if ($course->id == SITEID) {
3057 // Frontpage can not be hidden.
3058 } else {
3059 if (is_role_switched($course->id)) {
3060 // When switching roles ignore the hidden flag - user had to be in course to do the switch.
3061 } else {
3062 if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
3063 // Originally there was also test of parent category visibility, BUT is was very slow in complex queries
3064 // involving "my courses" now it is also possible to simply hide all courses user is not enrolled in :-).
3065 if ($preventredirect) {
3066 throw new require_login_exception('Course is hidden');
3068 // We need to override the navigation URL as the course won't have been added to the navigation and thus
3069 // the navigation will mess up when trying to find it.
3070 navigation_node::override_active_url(new moodle_url('/'));
3071 notice(get_string('coursehidden'), $CFG->wwwroot .'/');
3076 // Is the user enrolled?
3077 if ($course->id == SITEID) {
3078 // Everybody is enrolled on the frontpage.
3079 } else {
3080 if (session_is_loggedinas()) {
3081 // Make sure the REAL person can access this course first.
3082 $realuser = session_get_realuser();
3083 if (!is_enrolled($coursecontext, $realuser->id, '', true) and
3084 !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) {
3085 if ($preventredirect) {
3086 throw new require_login_exception('Invalid course login-as access');
3088 echo $OUTPUT->header();
3089 notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/');
3093 $access = false;
3095 if (is_role_switched($course->id)) {
3096 // Ok, user had to be inside this course before the switch.
3097 $access = true;
3099 } else if (is_viewing($coursecontext, $USER)) {
3100 // Ok, no need to mess with enrol.
3101 $access = true;
3103 } else {
3104 if (isset($USER->enrol['enrolled'][$course->id])) {
3105 if ($USER->enrol['enrolled'][$course->id] > time()) {
3106 $access = true;
3107 if (isset($USER->enrol['tempguest'][$course->id])) {
3108 unset($USER->enrol['tempguest'][$course->id]);
3109 remove_temp_course_roles($coursecontext);
3111 } else {
3112 // Expired.
3113 unset($USER->enrol['enrolled'][$course->id]);
3116 if (isset($USER->enrol['tempguest'][$course->id])) {
3117 if ($USER->enrol['tempguest'][$course->id] == 0) {
3118 $access = true;
3119 } else if ($USER->enrol['tempguest'][$course->id] > time()) {
3120 $access = true;
3121 } else {
3122 // Expired.
3123 unset($USER->enrol['tempguest'][$course->id]);
3124 remove_temp_course_roles($coursecontext);
3128 if (!$access) {
3129 // Cache not ok.
3130 $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id);
3131 if ($until !== false) {
3132 // Active participants may always access, a timestamp in the future, 0 (always) or false.
3133 if ($until == 0) {
3134 $until = ENROL_MAX_TIMESTAMP;
3136 $USER->enrol['enrolled'][$course->id] = $until;
3137 $access = true;
3139 } else {
3140 $params = array('courseid' => $course->id, 'status' => ENROL_INSTANCE_ENABLED);
3141 $instances = $DB->get_records('enrol', $params, 'sortorder, id ASC');
3142 $enrols = enrol_get_plugins(true);
3143 // First ask all enabled enrol instances in course if they want to auto enrol user.
3144 foreach ($instances as $instance) {
3145 if (!isset($enrols[$instance->enrol])) {
3146 continue;
3148 // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false.
3149 $until = $enrols[$instance->enrol]->try_autoenrol($instance);
3150 if ($until !== false) {
3151 if ($until == 0) {
3152 $until = ENROL_MAX_TIMESTAMP;
3154 $USER->enrol['enrolled'][$course->id] = $until;
3155 $access = true;
3156 break;
3159 // If not enrolled yet try to gain temporary guest access.
3160 if (!$access) {
3161 foreach ($instances as $instance) {
3162 if (!isset($enrols[$instance->enrol])) {
3163 continue;
3165 // Get a duration for the guest access, a timestamp in the future or false.
3166 $until = $enrols[$instance->enrol]->try_guestaccess($instance);
3167 if ($until !== false and $until > time()) {
3168 $USER->enrol['tempguest'][$course->id] = $until;
3169 $access = true;
3170 break;
3178 if (!$access) {
3179 if ($preventredirect) {
3180 throw new require_login_exception('Not enrolled');
3182 if ($setwantsurltome) {
3183 $SESSION->wantsurl = qualified_me();
3185 redirect($CFG->wwwroot .'/enrol/index.php?id='. $course->id);
3189 // Check visibility of activity to current user; includes visible flag, groupmembersonly, conditional availability, etc.
3190 if ($cm && !$cm->uservisible) {
3191 if ($preventredirect) {
3192 throw new require_login_exception('Activity is hidden');
3194 if ($course->id != SITEID) {
3195 $url = new moodle_url('/course/view.php', array('id' => $course->id));
3196 } else {
3197 $url = new moodle_url('/');
3199 redirect($url, get_string('activityiscurrentlyhidden'));
3202 // Finally access granted, update lastaccess times.
3203 user_accesstime_log($course->id);
3208 * This function just makes sure a user is logged out.
3210 * @package core_access
3211 * @category access
3213 function require_logout() {
3214 global $USER;
3216 $params = $USER;
3218 if (isloggedin()) {
3219 add_to_log(SITEID, "user", "logout", "view.php?id=$USER->id&course=".SITEID, $USER->id, 0, $USER->id);
3221 $authsequence = get_enabled_auth_plugins(); // Auths, in sequence.
3222 foreach ($authsequence as $authname) {
3223 $authplugin = get_auth_plugin($authname);
3224 $authplugin->prelogout_hook();
3228 events_trigger('user_logout', $params);
3229 session_get_instance()->terminate_current();
3230 unset($params);
3234 * Weaker version of require_login()
3236 * This is a weaker version of {@link require_login()} which only requires login
3237 * when called from within a course rather than the site page, unless
3238 * the forcelogin option is turned on.
3239 * @see require_login()
3241 * @package core_access
3242 * @category access
3244 * @param mixed $courseorid The course object or id in question
3245 * @param bool $autologinguest Allow autologin guests if that is wanted
3246 * @param object $cm Course activity module if known
3247 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
3248 * true. Used to avoid (=false) some scripts (file.php...) to set that variable,
3249 * in order to keep redirects working properly. MDL-14495
3250 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
3251 * @return void
3252 * @throws coding_exception
3254 function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
3255 global $CFG, $PAGE, $SITE;
3256 $issite = (is_object($courseorid) and $courseorid->id == SITEID)
3257 or (!is_object($courseorid) and $courseorid == SITEID);
3258 if ($issite && !empty($cm) && !($cm instanceof cm_info)) {
3259 // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
3260 // db queries so this is not really a performance concern, however it is obviously
3261 // better if you use get_fast_modinfo to get the cm before calling this.
3262 if (is_object($courseorid)) {
3263 $course = $courseorid;
3264 } else {
3265 $course = clone($SITE);
3267 $modinfo = get_fast_modinfo($course);
3268 $cm = $modinfo->get_cm($cm->id);
3270 if (!empty($CFG->forcelogin)) {
3271 // Login required for both SITE and courses.
3272 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3274 } else if ($issite && !empty($cm) and !$cm->uservisible) {
3275 // Always login for hidden activities.
3276 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3278 } else if ($issite) {
3279 // Login for SITE not required.
3280 if ($cm and empty($cm->visible)) {
3281 // Hidden activities are not accessible without login.
3282 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3283 } else if ($cm and !empty($CFG->enablegroupmembersonly) and $cm->groupmembersonly) {
3284 // Not-logged-in users do not have any group membership.
3285 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3286 } else {
3287 // We still need to instatiate PAGE vars properly so that things that rely on it like navigation function correctly.
3288 if (!empty($courseorid)) {
3289 if (is_object($courseorid)) {
3290 $course = $courseorid;
3291 } else {
3292 $course = clone($SITE);
3294 if ($cm) {
3295 if ($cm->course != $course->id) {
3296 throw new coding_exception('course and cm parameters in require_course_login() call do not match!!');
3298 $PAGE->set_cm($cm, $course);
3299 $PAGE->set_pagelayout('incourse');
3300 } else {
3301 $PAGE->set_course($course);
3303 } else {
3304 // If $PAGE->course, and hence $PAGE->context, have not already been set up properly, set them up now.
3305 $PAGE->set_course($PAGE->course);
3307 // TODO: verify conditional activities here.
3308 user_accesstime_log(SITEID);
3309 return;
3312 } else {
3313 // Course login always required.
3314 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3319 * Require key login. Function terminates with error if key not found or incorrect.
3321 * @uses NO_MOODLE_COOKIES
3322 * @uses PARAM_ALPHANUM
3323 * @param string $script unique script identifier
3324 * @param int $instance optional instance id
3325 * @return int Instance ID
3327 function require_user_key_login($script, $instance=null) {
3328 global $DB;
3330 if (!NO_MOODLE_COOKIES) {
3331 print_error('sessioncookiesdisable');
3334 // Extra safety.
3335 @session_write_close();
3337 $keyvalue = required_param('key', PARAM_ALPHANUM);
3339 if (!$key = $DB->get_record('user_private_key', array('script' => $script, 'value' => $keyvalue, 'instance' => $instance))) {
3340 print_error('invalidkey');
3343 if (!empty($key->validuntil) and $key->validuntil < time()) {
3344 print_error('expiredkey');
3347 if ($key->iprestriction) {
3348 $remoteaddr = getremoteaddr(null);
3349 if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) {
3350 print_error('ipmismatch');
3354 if (!$user = $DB->get_record('user', array('id' => $key->userid))) {
3355 print_error('invaliduserid');
3358 // Emulate normal session.
3359 enrol_check_plugins($user);
3360 session_set_user($user);
3362 // Note we are not using normal login.
3363 if (!defined('USER_KEY_LOGIN')) {
3364 define('USER_KEY_LOGIN', true);
3367 // Return instance id - it might be empty.
3368 return $key->instance;
3372 * Creates a new private user access key.
3374 * @param string $script unique target identifier
3375 * @param int $userid
3376 * @param int $instance optional instance id
3377 * @param string $iprestriction optional ip restricted access
3378 * @param timestamp $validuntil key valid only until given data
3379 * @return string access key value
3381 function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3382 global $DB;
3384 $key = new stdClass();
3385 $key->script = $script;
3386 $key->userid = $userid;
3387 $key->instance = $instance;
3388 $key->iprestriction = $iprestriction;
3389 $key->validuntil = $validuntil;
3390 $key->timecreated = time();
3392 // Something long and unique.
3393 $key->value = md5($userid.'_'.time().random_string(40));
3394 while ($DB->record_exists('user_private_key', array('value' => $key->value))) {
3395 // Must be unique.
3396 $key->value = md5($userid.'_'.time().random_string(40));
3398 $DB->insert_record('user_private_key', $key);
3399 return $key->value;
3403 * Delete the user's new private user access keys for a particular script.
3405 * @param string $script unique target identifier
3406 * @param int $userid
3407 * @return void
3409 function delete_user_key($script, $userid) {
3410 global $DB;
3411 $DB->delete_records('user_private_key', array('script' => $script, 'userid' => $userid));
3415 * Gets a private user access key (and creates one if one doesn't exist).
3417 * @param string $script unique target identifier
3418 * @param int $userid
3419 * @param int $instance optional instance id
3420 * @param string $iprestriction optional ip restricted access
3421 * @param timestamp $validuntil key valid only until given data
3422 * @return string access key value
3424 function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3425 global $DB;
3427 if ($key = $DB->get_record('user_private_key', array('script' => $script, 'userid' => $userid,
3428 'instance' => $instance, 'iprestriction' => $iprestriction,
3429 'validuntil' => $validuntil))) {
3430 return $key->value;
3431 } else {
3432 return create_user_key($script, $userid, $instance, $iprestriction, $validuntil);
3438 * Modify the user table by setting the currently logged in user's last login to now.
3440 * @return bool Always returns true
3442 function update_user_login_times() {
3443 global $USER, $DB;
3445 if (isguestuser()) {
3446 // Do not update guest access times/ips for performance.
3447 return true;
3450 $now = time();
3452 $user = new stdClass();
3453 $user->id = $USER->id;
3455 // Make sure all users that logged in have some firstaccess.
3456 if ($USER->firstaccess == 0) {
3457 $USER->firstaccess = $user->firstaccess = $now;
3460 // Store the previous current as lastlogin.
3461 $USER->lastlogin = $user->lastlogin = $USER->currentlogin;
3463 $USER->currentlogin = $user->currentlogin = $now;
3465 // Function user_accesstime_log() may not update immediately, better do it here.
3466 $USER->lastaccess = $user->lastaccess = $now;
3467 $USER->lastip = $user->lastip = getremoteaddr();
3469 $DB->update_record('user', $user);
3470 return true;
3474 * Determines if a user has completed setting up their account.
3476 * @param stdClass $user A {@link $USER} object to test for the existence of a valid name and email
3477 * @return bool
3479 function user_not_fully_set_up($user) {
3480 if (isguestuser($user)) {
3481 return false;
3483 return (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user));
3487 * Check whether the user has exceeded the bounce threshold
3489 * @param stdClass $user A {@link $USER} object
3490 * @return bool true => User has exceeded bounce threshold
3492 function over_bounce_threshold($user) {
3493 global $CFG, $DB;
3495 if (empty($CFG->handlebounces)) {
3496 return false;
3499 if (empty($user->id)) {
3500 // No real (DB) user, nothing to do here.
3501 return false;
3504 // Set sensible defaults.
3505 if (empty($CFG->minbounces)) {
3506 $CFG->minbounces = 10;
3508 if (empty($CFG->bounceratio)) {
3509 $CFG->bounceratio = .20;
3511 $bouncecount = 0;
3512 $sendcount = 0;
3513 if ($bounce = $DB->get_record('user_preferences', array ('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3514 $bouncecount = $bounce->value;
3516 if ($send = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3517 $sendcount = $send->value;
3519 return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio);
3523 * Used to increment or reset email sent count
3525 * @param stdClass $user object containing an id
3526 * @param bool $reset will reset the count to 0
3527 * @return void
3529 function set_send_count($user, $reset=false) {
3530 global $DB;
3532 if (empty($user->id)) {
3533 // No real (DB) user, nothing to do here.
3534 return;
3537 if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3538 $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3539 $DB->update_record('user_preferences', $pref);
3540 } else if (!empty($reset)) {
3541 // If it's not there and we're resetting, don't bother. Make a new one.
3542 $pref = new stdClass();
3543 $pref->name = 'email_send_count';
3544 $pref->value = 1;
3545 $pref->userid = $user->id;
3546 $DB->insert_record('user_preferences', $pref, false);
3551 * Increment or reset user's email bounce count
3553 * @param stdClass $user object containing an id
3554 * @param bool $reset will reset the count to 0
3556 function set_bounce_count($user, $reset=false) {
3557 global $DB;
3559 if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3560 $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3561 $DB->update_record('user_preferences', $pref);
3562 } else if (!empty($reset)) {
3563 // If it's not there and we're resetting, don't bother. Make a new one.
3564 $pref = new stdClass();
3565 $pref->name = 'email_bounce_count';
3566 $pref->value = 1;
3567 $pref->userid = $user->id;
3568 $DB->insert_record('user_preferences', $pref, false);
3573 * Determines if the logged in user is currently moving an activity
3575 * @param int $courseid The id of the course being tested
3576 * @return bool
3578 function ismoving($courseid) {
3579 global $USER;
3581 if (!empty($USER->activitycopy)) {
3582 return ($USER->activitycopycourse == $courseid);
3584 return false;
3588 * Returns a persons full name
3590 * Given an object containing all of the users name values, this function returns a string with the full name of the person.
3591 * The result may depend on system settings or language. 'override' will force both names to be used even if system settings
3592 * specify one.
3594 * @param stdClass $user A {@link $USER} object to get full name of.
3595 * @param bool $override If true then the name will be firstname followed by lastname rather than adhering to fullnamedisplay.
3596 * @return string
3598 function fullname($user, $override=false) {
3599 global $CFG, $SESSION;
3601 if (!isset($user->firstname) and !isset($user->lastname)) {
3602 return '';
3605 if (!$override) {
3606 if (!empty($CFG->forcefirstname)) {
3607 $user->firstname = $CFG->forcefirstname;
3609 if (!empty($CFG->forcelastname)) {
3610 $user->lastname = $CFG->forcelastname;
3614 if (!empty($SESSION->fullnamedisplay)) {
3615 $CFG->fullnamedisplay = $SESSION->fullnamedisplay;
3618 $template = null;
3619 // If the fullnamedisplay setting is available, set the template to that.
3620 if (isset($CFG->fullnamedisplay)) {
3621 $template = $CFG->fullnamedisplay;
3623 // If the template is empty, or set to language, or $override is set, return the language string.
3624 if (empty($template) || $template == 'language' || $override) {
3625 return get_string('fullnamedisplay', null, $user);
3628 // Get all of the name fields.
3629 $allnames = get_all_user_name_fields();
3630 $requirednames = array();
3631 // With each name, see if it is in the display name template, and add it to the required names array if it is.
3632 foreach ($allnames as $allname) {
3633 if (strpos($template, $allname) !== false) {
3634 $requirednames[] = $allname;
3635 // If the field is in the template but not set in the user object then notify the programmer that it needs to be fixed.
3636 if (!array_key_exists($allname, $user)) {
3637 debugging('You need to update your sql to include additional name fields in the user object.', DEBUG_DEVELOPER);
3642 $displayname = $template;
3643 // Switch in the actual data into the template.
3644 foreach ($requirednames as $altname) {
3645 if (isset($user->$altname)) {
3646 // Using empty() on the below if statement causes breakages.
3647 if ((string)$user->$altname == '') {
3648 $displayname = str_replace($altname, 'EMPTY', $displayname);
3649 } else {
3650 $displayname = str_replace($altname, $user->$altname, $displayname);
3652 } else {
3653 $displayname = str_replace($altname, 'EMPTY', $displayname);
3656 // Tidy up any misc. characters (Not perfect, but gets most characters).
3657 // Don't remove the "u" at the end of the first expression unless you want garbled characters when combining hiragana or
3658 // katakana and parenthesis.
3659 $patterns = array();
3660 // This regular expression replacement is to fix problems such as 'James () Kirk' Where 'Tiberius' (middlename) has not been
3661 // filled in by a user.
3662 // The special characters are Japanese brackets that are common enough to make allowances for them (not covered by :punct:).
3663 $patterns[] = '/[[:punct:]「」]*EMPTY[[:punct:]「」]*/u';
3664 // This regular expression is to remove any double spaces in the display name.
3665 $patterns[] = '/\s{2,}/';
3666 foreach ($patterns as $pattern) {
3667 $displayname = preg_replace($pattern, ' ', $displayname);
3670 // Trimming $displayname will help the next check to ensure that we don't have a display name with spaces.
3671 $displayname = trim($displayname);
3672 if (empty($displayname)) {
3673 // Going with just the first name if no alternate fields are filled out. May be changed later depending on what
3674 // people in general feel is a good setting to fall back on.
3675 $displayname = $user->firstname;
3677 return $displayname;
3681 * A centralised location for the all name fields. Returns an array / sql string snippet.
3683 * @param bool $returnsql True for an sql select field snippet.
3684 * @param string $alias table alias to use in front of each field.
3685 * @return array|string All name fields.
3687 function get_all_user_name_fields($returnsql = false, $alias = null) {
3688 $alternatenames = array('firstnamephonetic',
3689 'lastnamephonetic',
3690 'middlename',
3691 'alternatename',
3692 'firstname',
3693 'lastname');
3694 if ($returnsql) {
3695 if ($alias) {
3696 foreach ($alternatenames as $key => $altname) {
3697 $alternatenames[$key] = "$alias.$altname";
3700 $alternatenames = implode(',', $alternatenames);
3702 return $alternatenames;
3706 * Returns an array of values in order of occurance in a provided string.
3707 * The key in the result is the character postion in the string.
3709 * @param array $values Values to be found in the string format
3710 * @param string $stringformat The string which may contain values being searched for.
3711 * @return array An array of values in order according to placement in the string format.
3713 function order_in_string($values, $stringformat) {
3714 $valuearray = array();
3715 foreach ($values as $value) {
3716 $pattern = "/$value\b/";
3717 // Using preg_match as strpos() may match values that are similar e.g. firstname and firstnamephonetic.
3718 if (preg_match($pattern, $stringformat)) {
3719 $replacement = "thing";
3720 // Replace the value with something more unique to ensure we get the right position when using strpos().
3721 $newformat = preg_replace($pattern, $replacement, $stringformat);
3722 $position = strpos($newformat, $replacement);
3723 $valuearray[$position] = $value;
3726 ksort($valuearray);
3727 return $valuearray;
3731 * Checks if current user is shown any extra fields when listing users.
3733 * @param object $context Context
3734 * @param array $already Array of fields that we're going to show anyway
3735 * so don't bother listing them
3736 * @return array Array of field names from user table, not including anything
3737 * listed in $already
3739 function get_extra_user_fields($context, $already = array()) {
3740 global $CFG;
3742 // Only users with permission get the extra fields.
3743 if (!has_capability('moodle/site:viewuseridentity', $context)) {
3744 return array();
3747 // Split showuseridentity on comma.
3748 if (empty($CFG->showuseridentity)) {
3749 // Explode gives wrong result with empty string.
3750 $extra = array();
3751 } else {
3752 $extra = explode(',', $CFG->showuseridentity);
3754 $renumber = false;
3755 foreach ($extra as $key => $field) {
3756 if (in_array($field, $already)) {
3757 unset($extra[$key]);
3758 $renumber = true;
3761 if ($renumber) {
3762 // For consistency, if entries are removed from array, renumber it
3763 // so they are numbered as you would expect.
3764 $extra = array_merge($extra);
3766 return $extra;
3770 * If the current user is to be shown extra user fields when listing or
3771 * selecting users, returns a string suitable for including in an SQL select
3772 * clause to retrieve those fields.
3774 * @param context $context Context
3775 * @param string $alias Alias of user table, e.g. 'u' (default none)
3776 * @param string $prefix Prefix for field names using AS, e.g. 'u_' (default none)
3777 * @param array $already Array of fields that we're going to include anyway so don't list them (default none)
3778 * @return string Partial SQL select clause, beginning with comma, for example ',u.idnumber,u.department' unless it is blank
3780 function get_extra_user_fields_sql($context, $alias='', $prefix='', $already = array()) {
3781 $fields = get_extra_user_fields($context, $already);
3782 $result = '';
3783 // Add punctuation for alias.
3784 if ($alias !== '') {
3785 $alias .= '.';
3787 foreach ($fields as $field) {
3788 $result .= ', ' . $alias . $field;
3789 if ($prefix) {
3790 $result .= ' AS ' . $prefix . $field;
3793 return $result;
3797 * Returns the display name of a field in the user table. Works for most fields that are commonly displayed to users.
3798 * @param string $field Field name, e.g. 'phone1'
3799 * @return string Text description taken from language file, e.g. 'Phone number'
3801 function get_user_field_name($field) {
3802 // Some fields have language strings which are not the same as field name.
3803 switch ($field) {
3804 case 'phone1' : {
3805 return get_string('phone');
3807 case 'url' : {
3808 return get_string('webpage');
3810 case 'icq' : {
3811 return get_string('icqnumber');
3813 case 'skype' : {
3814 return get_string('skypeid');
3816 case 'aim' : {
3817 return get_string('aimid');
3819 case 'yahoo' : {
3820 return get_string('yahooid');
3822 case 'msn' : {
3823 return get_string('msnid');
3826 // Otherwise just use the same lang string.
3827 return get_string($field);
3831 * Returns whether a given authentication plugin exists.
3833 * @param string $auth Form of authentication to check for. Defaults to the global setting in {@link $CFG}.
3834 * @return boolean Whether the plugin is available.
3836 function exists_auth_plugin($auth) {
3837 global $CFG;
3839 if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) {
3840 return is_readable("{$CFG->dirroot}/auth/$auth/auth.php");
3842 return false;
3846 * Checks if a given plugin is in the list of enabled authentication plugins.
3848 * @param string $auth Authentication plugin.
3849 * @return boolean Whether the plugin is enabled.
3851 function is_enabled_auth($auth) {
3852 if (empty($auth)) {
3853 return false;
3856 $enabled = get_enabled_auth_plugins();
3858 return in_array($auth, $enabled);
3862 * Returns an authentication plugin instance.
3864 * @param string $auth name of authentication plugin
3865 * @return auth_plugin_base An instance of the required authentication plugin.
3867 function get_auth_plugin($auth) {
3868 global $CFG;
3870 // Check the plugin exists first.
3871 if (! exists_auth_plugin($auth)) {
3872 print_error('authpluginnotfound', 'debug', '', $auth);
3875 // Return auth plugin instance.
3876 require_once("{$CFG->dirroot}/auth/$auth/auth.php");
3877 $class = "auth_plugin_$auth";
3878 return new $class;
3882 * Returns array of active auth plugins.
3884 * @param bool $fix fix $CFG->auth if needed
3885 * @return array
3887 function get_enabled_auth_plugins($fix=false) {
3888 global $CFG;
3890 $default = array('manual', 'nologin');
3892 if (empty($CFG->auth)) {
3893 $auths = array();
3894 } else {
3895 $auths = explode(',', $CFG->auth);
3898 if ($fix) {
3899 $auths = array_unique($auths);
3900 foreach ($auths as $k => $authname) {
3901 if (!exists_auth_plugin($authname) or in_array($authname, $default)) {
3902 unset($auths[$k]);
3905 $newconfig = implode(',', $auths);
3906 if (!isset($CFG->auth) or $newconfig != $CFG->auth) {
3907 set_config('auth', $newconfig);
3911 return (array_merge($default, $auths));
3915 * Returns true if an internal authentication method is being used.
3916 * if method not specified then, global default is assumed
3918 * @param string $auth Form of authentication required
3919 * @return bool
3921 function is_internal_auth($auth) {
3922 // Throws error if bad $auth.
3923 $authplugin = get_auth_plugin($auth);
3924 return $authplugin->is_internal();
3928 * Returns true if the user is a 'restored' one.
3930 * Used in the login process to inform the user and allow him/her to reset the password
3932 * @param string $username username to be checked
3933 * @return bool
3935 function is_restored_user($username) {
3936 global $CFG, $DB;
3938 return $DB->record_exists('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'password' => 'restored'));
3942 * Returns an array of user fields
3944 * @return array User field/column names
3946 function get_user_fieldnames() {
3947 global $DB;
3949 $fieldarray = $DB->get_columns('user');
3950 unset($fieldarray['id']);
3951 $fieldarray = array_keys($fieldarray);
3953 return $fieldarray;
3957 * Creates a bare-bones user record
3959 * @todo Outline auth types and provide code example
3961 * @param string $username New user's username to add to record
3962 * @param string $password New user's password to add to record
3963 * @param string $auth Form of authentication required
3964 * @return stdClass A complete user object
3966 function create_user_record($username, $password, $auth = 'manual') {
3967 global $CFG, $DB;
3968 require_once($CFG->dirroot."/user/profile/lib.php");
3969 // Just in case check text case.
3970 $username = trim(core_text::strtolower($username));
3972 $authplugin = get_auth_plugin($auth);
3973 $customfields = $authplugin->get_custom_user_profile_fields();
3974 $newuser = new stdClass();
3975 if ($newinfo = $authplugin->get_userinfo($username)) {
3976 $newinfo = truncate_userinfo($newinfo);
3977 foreach ($newinfo as $key => $value) {
3978 if (in_array($key, $authplugin->userfields) || (in_array($key, $customfields))) {
3979 $newuser->$key = $value;
3984 if (!empty($newuser->email)) {
3985 if (email_is_not_allowed($newuser->email)) {
3986 unset($newuser->email);
3990 if (!isset($newuser->city)) {
3991 $newuser->city = '';
3994 $newuser->auth = $auth;
3995 $newuser->username = $username;
3997 // Fix for MDL-8480
3998 // user CFG lang for user if $newuser->lang is empty
3999 // or $user->lang is not an installed language.
4000 if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) {
4001 $newuser->lang = $CFG->lang;
4003 $newuser->confirmed = 1;
4004 $newuser->lastip = getremoteaddr();
4005 $newuser->timecreated = time();
4006 $newuser->timemodified = $newuser->timecreated;
4007 $newuser->mnethostid = $CFG->mnet_localhost_id;
4009 $newuser->id = $DB->insert_record('user', $newuser);
4011 // Save user profile data.
4012 profile_save_data($newuser);
4014 $user = get_complete_user_data('id', $newuser->id);
4015 if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})) {
4016 set_user_preference('auth_forcepasswordchange', 1, $user);
4018 // Set the password.
4019 update_internal_user_password($user, $password);
4021 // Fetch full user record for the event, the complete user data contains too much info
4022 // and we want to be consistent with other places that trigger this event.
4023 events_trigger('user_created', $DB->get_record('user', array('id' => $user->id)));
4025 return $user;
4029 * Will update a local user record from an external source (MNET users can not be updated using this method!).
4031 * @param string $username user's username to update the record
4032 * @return stdClass A complete user object
4034 function update_user_record($username) {
4035 global $DB, $CFG;
4036 require_once($CFG->dirroot."/user/profile/lib.php");
4037 // Just in case check text case.
4038 $username = trim(core_text::strtolower($username));
4040 $oldinfo = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id), '*', MUST_EXIST);
4041 $newuser = array();
4042 $userauth = get_auth_plugin($oldinfo->auth);
4044 if ($newinfo = $userauth->get_userinfo($username)) {
4045 $newinfo = truncate_userinfo($newinfo);
4046 $customfields = $userauth->get_custom_user_profile_fields();
4048 foreach ($newinfo as $key => $value) {
4049 $key = strtolower($key);
4050 $iscustom = in_array($key, $customfields);
4051 if ((!property_exists($oldinfo, $key) && !$iscustom) or $key === 'username' or $key === 'id'
4052 or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') {
4053 // Unknown or must not be changed.
4054 continue;
4056 $confval = $userauth->config->{'field_updatelocal_' . $key};
4057 $lockval = $userauth->config->{'field_lock_' . $key};
4058 if (empty($confval) || empty($lockval)) {
4059 continue;
4061 if ($confval === 'onlogin') {
4062 // MDL-4207 Don't overwrite modified user profile values with
4063 // empty LDAP values when 'unlocked if empty' is set. The purpose
4064 // of the setting 'unlocked if empty' is to allow the user to fill
4065 // in a value for the selected field _if LDAP is giving
4066 // nothing_ for this field. Thus it makes sense to let this value
4067 // stand in until LDAP is giving a value for this field.
4068 if (!(empty($value) && $lockval === 'unlockedifempty')) {
4069 if ($iscustom || (in_array($key, $userauth->userfields) &&
4070 ((string)$oldinfo->$key !== (string)$value))) {
4071 $newuser[$key] = (string)$value;
4076 if ($newuser) {
4077 $newuser['id'] = $oldinfo->id;
4078 $newuser['timemodified'] = time();
4079 $DB->update_record('user', $newuser);
4081 // Save user profile data.
4082 profile_save_data((object) $newuser);
4084 // Fetch full user record for the event, the complete user data contains too much info
4085 // and we want to be consistent with other places that trigger this event.
4086 events_trigger('user_updated', $DB->get_record('user', array('id' => $oldinfo->id)));
4090 return get_complete_user_data('id', $oldinfo->id);
4094 * Will truncate userinfo as it comes from auth_get_userinfo (from external auth) which may have large fields.
4096 * @param array $info Array of user properties to truncate if needed
4097 * @return array The now truncated information that was passed in
4099 function truncate_userinfo(array $info) {
4100 // Define the limits.
4101 $limit = array(
4102 'username' => 100,
4103 'idnumber' => 255,
4104 'firstname' => 100,
4105 'lastname' => 100,
4106 'email' => 100,
4107 'icq' => 15,
4108 'phone1' => 20,
4109 'phone2' => 20,
4110 'institution' => 40,
4111 'department' => 30,
4112 'address' => 70,
4113 'city' => 120,
4114 'country' => 2,
4115 'url' => 255,
4118 // Apply where needed.
4119 foreach (array_keys($info) as $key) {
4120 if (!empty($limit[$key])) {
4121 $info[$key] = trim(core_text::substr($info[$key], 0, $limit[$key]));
4125 return $info;
4129 * Marks user deleted in internal user database and notifies the auth plugin.
4130 * Also unenrols user from all roles and does other cleanup.
4132 * Any plugin that needs to purge user data should register the 'user_deleted' event.
4134 * @param stdClass $user full user object before delete
4135 * @return boolean success
4136 * @throws coding_exception if invalid $user parameter detected
4138 function delete_user(stdClass $user) {
4139 global $CFG, $DB;
4140 require_once($CFG->libdir.'/grouplib.php');
4141 require_once($CFG->libdir.'/gradelib.php');
4142 require_once($CFG->dirroot.'/message/lib.php');
4143 require_once($CFG->dirroot.'/tag/lib.php');
4145 // Make sure nobody sends bogus record type as parameter.
4146 if (!property_exists($user, 'id') or !property_exists($user, 'username')) {
4147 throw new coding_exception('Invalid $user parameter in delete_user() detected');
4150 // Better not trust the parameter and fetch the latest info this will be very expensive anyway.
4151 if (!$user = $DB->get_record('user', array('id' => $user->id))) {
4152 debugging('Attempt to delete unknown user account.');
4153 return false;
4156 // There must be always exactly one guest record, originally the guest account was identified by username only,
4157 // now we use $CFG->siteguest for performance reasons.
4158 if ($user->username === 'guest' or isguestuser($user)) {
4159 debugging('Guest user account can not be deleted.');
4160 return false;
4163 // Admin can be theoretically from different auth plugin, but we want to prevent deletion of internal accoutns only,
4164 // if anything goes wrong ppl may force somebody to be admin via config.php setting $CFG->siteadmins.
4165 if ($user->auth === 'manual' and is_siteadmin($user)) {
4166 debugging('Local administrator accounts can not be deleted.');
4167 return false;
4170 // Delete all grades - backup is kept in grade_grades_history table.
4171 grade_user_delete($user->id);
4173 // Move unread messages from this user to read.
4174 message_move_userfrom_unread2read($user->id);
4176 // TODO: remove from cohorts using standard API here.
4178 // Remove user tags.
4179 tag_set('user', $user->id, array());
4181 // Unconditionally unenrol from all courses.
4182 enrol_user_delete($user);
4184 // Unenrol from all roles in all contexts.
4185 // This might be slow but it is really needed - modules might do some extra cleanup!
4186 role_unassign_all(array('userid' => $user->id));
4188 // Now do a brute force cleanup.
4190 // Remove from all cohorts.
4191 $DB->delete_records('cohort_members', array('userid' => $user->id));
4193 // Remove from all groups.
4194 $DB->delete_records('groups_members', array('userid' => $user->id));
4196 // Brute force unenrol from all courses.
4197 $DB->delete_records('user_enrolments', array('userid' => $user->id));
4199 // Purge user preferences.
4200 $DB->delete_records('user_preferences', array('userid' => $user->id));
4202 // Purge user extra profile info.
4203 $DB->delete_records('user_info_data', array('userid' => $user->id));
4205 // Last course access not necessary either.
4206 $DB->delete_records('user_lastaccess', array('userid' => $user->id));
4207 // Remove all user tokens.
4208 $DB->delete_records('external_tokens', array('userid' => $user->id));
4210 // Unauthorise the user for all services.
4211 $DB->delete_records('external_services_users', array('userid' => $user->id));
4213 // Remove users private keys.
4214 $DB->delete_records('user_private_key', array('userid' => $user->id));
4216 // Force logout - may fail if file based sessions used, sorry.
4217 session_kill_user($user->id);
4219 // Now do a final accesslib cleanup - removes all role assignments in user context and context itself.
4220 context_helper::delete_instance(CONTEXT_USER, $user->id);
4222 // Workaround for bulk deletes of users with the same email address.
4223 $delname = "$user->email.".time();
4224 while ($DB->record_exists('user', array('username' => $delname))) { // No need to use mnethostid here.
4225 $delname++;
4228 // Mark internal user record as "deleted".
4229 $updateuser = new stdClass();
4230 $updateuser->id = $user->id;
4231 $updateuser->deleted = 1;
4232 $updateuser->username = $delname; // Remember it just in case.
4233 $updateuser->email = md5($user->username);// Store hash of username, useful importing/restoring users.
4234 $updateuser->idnumber = ''; // Clear this field to free it up.
4235 $updateuser->picture = 0;
4236 $updateuser->timemodified = time();
4238 $DB->update_record('user', $updateuser);
4239 // Add this action to log.
4240 add_to_log(SITEID, 'user', 'delete', "view.php?id=$user->id", $user->firstname.' '.$user->lastname);
4242 // We will update the user's timemodified, as it will be passed to the user_deleted event, which
4243 // should know about this updated property persisted to the user's table.
4244 $user->timemodified = $updateuser->timemodified;
4246 // Notify auth plugin - do not block the delete even when plugin fails.
4247 $authplugin = get_auth_plugin($user->auth);
4248 $authplugin->user_delete($user);
4250 // Any plugin that needs to cleanup should register this event.
4251 events_trigger('user_deleted', $user);
4253 return true;
4257 * Retrieve the guest user object.
4259 * @return stdClass A {@link $USER} object
4261 function guest_user() {
4262 global $CFG, $DB;
4264 if ($newuser = $DB->get_record('user', array('id' => $CFG->siteguest))) {
4265 $newuser->confirmed = 1;
4266 $newuser->lang = $CFG->lang;
4267 $newuser->lastip = getremoteaddr();
4270 return $newuser;
4274 * Authenticates a user against the chosen authentication mechanism
4276 * Given a username and password, this function looks them
4277 * up using the currently selected authentication mechanism,
4278 * and if the authentication is successful, it returns a
4279 * valid $user object from the 'user' table.
4281 * Uses auth_ functions from the currently active auth module
4283 * After authenticate_user_login() returns success, you will need to
4284 * log that the user has logged in, and call complete_user_login() to set
4285 * the session up.
4287 * Note: this function works only with non-mnet accounts!
4289 * @param string $username User's username
4290 * @param string $password User's password
4291 * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO
4292 * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists)
4293 * @return stdClass|false A {@link $USER} object or false if error
4295 function authenticate_user_login($username, $password, $ignorelockout=false, &$failurereason=null) {
4296 global $CFG, $DB;
4297 require_once("$CFG->libdir/authlib.php");
4299 $authsenabled = get_enabled_auth_plugins();
4301 if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) {
4302 // Use manual if auth not set.
4303 $auth = empty($user->auth) ? 'manual' : $user->auth;
4304 if (!empty($user->suspended)) {
4305 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4306 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4307 $failurereason = AUTH_LOGIN_SUSPENDED;
4308 return false;
4310 if ($auth=='nologin' or !is_enabled_auth($auth)) {
4311 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4312 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Disabled Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4313 // Legacy way to suspend user.
4314 $failurereason = AUTH_LOGIN_SUSPENDED;
4315 return false;
4317 $auths = array($auth);
4319 } else {
4320 // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user().
4321 if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'deleted' => 1))) {
4322 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Deleted Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4323 $failurereason = AUTH_LOGIN_NOUSER;
4324 return false;
4327 // Do not try to authenticate non-existent accounts when user creation is not disabled.
4328 if (!empty($CFG->authpreventaccountcreation)) {
4329 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4330 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Unknown user, can not create new accounts: $username ".$_SERVER['HTTP_USER_AGENT']);
4331 $failurereason = AUTH_LOGIN_NOUSER;
4332 return false;
4335 // User does not exist.
4336 $auths = $authsenabled;
4337 $user = new stdClass();
4338 $user->id = 0;
4341 if ($ignorelockout) {
4342 // Some other mechanism protects against brute force password guessing, for example login form might include reCAPTCHA
4343 // or this function is called from a SSO script.
4344 } else if ($user->id) {
4345 // Verify login lockout after other ways that may prevent user login.
4346 if (login_is_lockedout($user)) {
4347 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4348 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Login lockout: $username ".$_SERVER['HTTP_USER_AGENT']);
4349 $failurereason = AUTH_LOGIN_LOCKOUT;
4350 return false;
4352 } else {
4353 // We can not lockout non-existing accounts.
4356 foreach ($auths as $auth) {
4357 $authplugin = get_auth_plugin($auth);
4359 // On auth fail fall through to the next plugin.
4360 if (!$authplugin->user_login($username, $password)) {
4361 continue;
4364 // Successful authentication.
4365 if ($user->id) {
4366 // User already exists in database.
4367 if (empty($user->auth)) {
4368 // For some reason auth isn't set yet.
4369 $DB->set_field('user', 'auth', $auth, array('username' => $username));
4370 $user->auth = $auth;
4373 // If the existing hash is using an out-of-date algorithm (or the legacy md5 algorithm), then we should update to
4374 // the current hash algorithm while we have access to the user's password.
4375 update_internal_user_password($user, $password);
4377 if ($authplugin->is_synchronised_with_external()) {
4378 // Update user record from external DB.
4379 $user = update_user_record($username);
4381 } else {
4382 // Create account, we verified above that user creation is allowed.
4383 $user = create_user_record($username, $password, $auth);
4386 $authplugin->sync_roles($user);
4388 foreach ($authsenabled as $hau) {
4389 $hauth = get_auth_plugin($hau);
4390 $hauth->user_authenticated_hook($user, $username, $password);
4393 if (empty($user->id)) {
4394 $failurereason = AUTH_LOGIN_NOUSER;
4395 return false;
4398 if (!empty($user->suspended)) {
4399 // Just in case some auth plugin suspended account.
4400 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4401 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4402 $failurereason = AUTH_LOGIN_SUSPENDED;
4403 return false;
4406 login_attempt_valid($user);
4407 $failurereason = AUTH_LOGIN_OK;
4408 return $user;
4411 // Failed if all the plugins have failed.
4412 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4413 if (debugging('', DEBUG_ALL)) {
4414 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Failed Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4417 if ($user->id) {
4418 login_attempt_failed($user);
4419 $failurereason = AUTH_LOGIN_FAILED;
4420 } else {
4421 $failurereason = AUTH_LOGIN_NOUSER;
4424 return false;
4428 * Call to complete the user login process after authenticate_user_login()
4429 * has succeeded. It will setup the $USER variable and other required bits
4430 * and pieces.
4432 * NOTE:
4433 * - It will NOT log anything -- up to the caller to decide what to log.
4434 * - this function does not set any cookies any more!
4436 * @param stdClass $user
4437 * @return stdClass A {@link $USER} object - BC only, do not use
4439 function complete_user_login($user) {
4440 global $CFG, $USER;
4442 // Regenerate session id and delete old session,
4443 // this helps prevent session fixation attacks from the same domain.
4444 session_regenerate_id(true);
4446 // Let enrol plugins deal with new enrolments if necessary.
4447 enrol_check_plugins($user);
4449 // Check enrolments, load caps and setup $USER object.
4450 session_set_user($user);
4452 // Reload preferences from DB.
4453 unset($USER->preference);
4454 check_user_preferences_loaded($USER);
4456 // Update login times.
4457 update_user_login_times();
4459 // Extra session prefs init.
4460 set_login_session_preferences();
4462 if (isguestuser()) {
4463 // No need to continue when user is THE guest.
4464 return $USER;
4467 // Select password change url.
4468 $userauth = get_auth_plugin($USER->auth);
4470 // Check whether the user should be changing password.
4471 if (get_user_preferences('auth_forcepasswordchange', false)) {
4472 if ($userauth->can_change_password()) {
4473 if ($changeurl = $userauth->change_password_url()) {
4474 redirect($changeurl);
4475 } else {
4476 redirect($CFG->httpswwwroot.'/login/change_password.php');
4478 } else {
4479 print_error('nopasswordchangeforced', 'auth');
4482 return $USER;
4486 * Check a password hash to see if it was hashed using the legacy hash algorithm (md5).
4488 * @param string $password String to check.
4489 * @return boolean True if the $password matches the format of an md5 sum.
4491 function password_is_legacy_hash($password) {
4492 return (bool) preg_match('/^[0-9a-f]{32}$/', $password);
4496 * Checks whether the password compatibility library will work with the current
4497 * version of PHP. This cannot be done using PHP version numbers since the fix
4498 * has been backported to earlier versions in some distributions.
4500 * See https://github.com/ircmaxell/password_compat/issues/10 for more details.
4502 * @return bool True if the library is NOT supported.
4504 function password_compat_not_supported() {
4506 $hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG';
4508 // Create a one off application cache to store bcrypt support status as
4509 // the support status doesn't change and crypt() is slow.
4510 $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'password_compat');
4512 if (!$bcryptsupport = $cache->get('bcryptsupport')) {
4513 $test = crypt('password', $hash);
4514 // Cache string instead of boolean to avoid MDL-37472.
4515 if ($test == $hash) {
4516 $bcryptsupport = 'supported';
4517 } else {
4518 $bcryptsupport = 'not supported';
4520 $cache->set('bcryptsupport', $bcryptsupport);
4523 // Return true if bcrypt *not* supported.
4524 return ($bcryptsupport !== 'supported');
4528 * Compare password against hash stored in user object to determine if it is valid.
4530 * If necessary it also updates the stored hash to the current format.
4532 * @param stdClass $user (Password property may be updated).
4533 * @param string $password Plain text password.
4534 * @return bool True if password is valid.
4536 function validate_internal_user_password($user, $password) {
4537 global $CFG;
4538 require_once($CFG->libdir.'/password_compat/lib/password.php');
4540 if ($user->password === AUTH_PASSWORD_NOT_CACHED) {
4541 // Internal password is not used at all, it can not validate.
4542 return false;
4545 // If hash isn't a legacy (md5) hash, validate using the library function.
4546 if (!password_is_legacy_hash($user->password)) {
4547 return password_verify($password, $user->password);
4550 // Otherwise we need to check for a legacy (md5) hash instead. If the hash
4551 // is valid we can then update it to the new algorithm.
4553 $sitesalt = isset($CFG->passwordsaltmain) ? $CFG->passwordsaltmain : '';
4554 $validated = false;
4556 if ($user->password === md5($password.$sitesalt)
4557 or $user->password === md5($password)
4558 or $user->password === md5(addslashes($password).$sitesalt)
4559 or $user->password === md5(addslashes($password))) {
4560 // Note: we are intentionally using the addslashes() here because we
4561 // need to accept old password hashes of passwords with magic quotes.
4562 $validated = true;
4564 } else {
4565 for ($i=1; $i<=20; $i++) { // 20 alternative salts should be enough, right?
4566 $alt = 'passwordsaltalt'.$i;
4567 if (!empty($CFG->$alt)) {
4568 if ($user->password === md5($password.$CFG->$alt) or $user->password === md5(addslashes($password).$CFG->$alt)) {
4569 $validated = true;
4570 break;
4576 if ($validated) {
4577 // If the password matches the existing md5 hash, update to the
4578 // current hash algorithm while we have access to the user's password.
4579 update_internal_user_password($user, $password);
4582 return $validated;
4586 * Calculate hash for a plain text password.
4588 * @param string $password Plain text password to be hashed.
4589 * @param bool $fasthash If true, use a low cost factor when generating the hash
4590 * This is much faster to generate but makes the hash
4591 * less secure. It is used when lots of hashes need to
4592 * be generated quickly.
4593 * @return string The hashed password.
4595 * @throws moodle_exception If a problem occurs while generating the hash.
4597 function hash_internal_user_password($password, $fasthash = false) {
4598 global $CFG;
4599 require_once($CFG->libdir.'/password_compat/lib/password.php');
4601 // Use the legacy hashing algorithm (md5) if PHP is not new enough to support bcrypt properly.
4602 if (password_compat_not_supported()) {
4603 if (isset($CFG->passwordsaltmain)) {
4604 return md5($password.$CFG->passwordsaltmain);
4605 } else {
4606 return md5($password);
4610 // Set the cost factor to 4 for fast hashing, otherwise use default cost.
4611 $options = ($fasthash) ? array('cost' => 4) : array();
4613 $generatedhash = password_hash($password, PASSWORD_DEFAULT, $options);
4615 if ($generatedhash === false || $generatedhash === null) {
4616 throw new moodle_exception('Failed to generate password hash.');
4619 return $generatedhash;
4623 * Update password hash in user object (if necessary).
4625 * The password is updated if:
4626 * 1. The password has changed (the hash of $user->password is different
4627 * to the hash of $password).
4628 * 2. The existing hash is using an out-of-date algorithm (or the legacy
4629 * md5 algorithm).
4631 * Updating the password will modify the $user object and the database
4632 * record to use the current hashing algorithm.
4634 * @param stdClass $user User object (password property may be updated).
4635 * @param string $password Plain text password.
4636 * @return bool Always returns true.
4638 function update_internal_user_password($user, $password) {
4639 global $CFG, $DB;
4640 require_once($CFG->libdir.'/password_compat/lib/password.php');
4642 // Use the legacy hashing algorithm (md5) if PHP doesn't support bcrypt properly.
4643 $legacyhash = password_compat_not_supported();
4645 // Figure out what the hashed password should be.
4646 $authplugin = get_auth_plugin($user->auth);
4647 if ($authplugin->prevent_local_passwords()) {
4648 $hashedpassword = AUTH_PASSWORD_NOT_CACHED;
4649 } else {
4650 $hashedpassword = hash_internal_user_password($password);
4653 if ($legacyhash) {
4654 $passwordchanged = ($user->password !== $hashedpassword);
4655 $algorithmchanged = false;
4656 } else {
4657 // If verification fails then it means the password has changed.
4658 $passwordchanged = !password_verify($password, $user->password);
4659 $algorithmchanged = password_needs_rehash($user->password, PASSWORD_DEFAULT);
4662 if ($passwordchanged || $algorithmchanged) {
4663 $DB->set_field('user', 'password', $hashedpassword, array('id' => $user->id));
4664 $user->password = $hashedpassword;
4667 return true;
4671 * Get a complete user record, which includes all the info in the user record.
4673 * Intended for setting as $USER session variable
4675 * @param string $field The user field to be checked for a given value.
4676 * @param string $value The value to match for $field.
4677 * @param int $mnethostid
4678 * @return mixed False, or A {@link $USER} object.
4680 function get_complete_user_data($field, $value, $mnethostid = null) {
4681 global $CFG, $DB;
4683 if (!$field || !$value) {
4684 return false;
4687 // Build the WHERE clause for an SQL query.
4688 $params = array('fieldval' => $value);
4689 $constraints = "$field = :fieldval AND deleted <> 1";
4691 // If we are loading user data based on anything other than id,
4692 // we must also restrict our search based on mnet host.
4693 if ($field != 'id') {
4694 if (empty($mnethostid)) {
4695 // If empty, we restrict to local users.
4696 $mnethostid = $CFG->mnet_localhost_id;
4699 if (!empty($mnethostid)) {
4700 $params['mnethostid'] = $mnethostid;
4701 $constraints .= " AND mnethostid = :mnethostid";
4704 // Get all the basic user data.
4705 if (! $user = $DB->get_record_select('user', $constraints, $params)) {
4706 return false;
4709 // Get various settings and preferences.
4711 // Preload preference cache.
4712 check_user_preferences_loaded($user);
4714 // Load course enrolment related stuff.
4715 $user->lastcourseaccess = array(); // During last session.
4716 $user->currentcourseaccess = array(); // During current session.
4717 if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid' => $user->id))) {
4718 foreach ($lastaccesses as $lastaccess) {
4719 $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess;
4723 $sql = "SELECT g.id, g.courseid
4724 FROM {groups} g, {groups_members} gm
4725 WHERE gm.groupid=g.id AND gm.userid=?";
4727 // This is a special hack to speedup calendar display.
4728 $user->groupmember = array();
4729 if (!isguestuser($user)) {
4730 if ($groups = $DB->get_records_sql($sql, array($user->id))) {
4731 foreach ($groups as $group) {
4732 if (!array_key_exists($group->courseid, $user->groupmember)) {
4733 $user->groupmember[$group->courseid] = array();
4735 $user->groupmember[$group->courseid][$group->id] = $group->id;
4740 // Add the custom profile fields to the user record.
4741 $user->profile = array();
4742 if (!isguestuser($user)) {
4743 require_once($CFG->dirroot.'/user/profile/lib.php');
4744 profile_load_custom_fields($user);
4747 // Rewrite some variables if necessary.
4748 if (!empty($user->description)) {
4749 // No need to cart all of it around.
4750 $user->description = true;
4752 if (isguestuser($user)) {
4753 // Guest language always same as site.
4754 $user->lang = $CFG->lang;
4755 // Name always in current language.
4756 $user->firstname = get_string('guestuser');
4757 $user->lastname = ' ';
4760 return $user;
4764 * Validate a password against the configured password policy
4766 * @param string $password the password to be checked against the password policy
4767 * @param string $errmsg the error message to display when the password doesn't comply with the policy.
4768 * @return bool true if the password is valid according to the policy. false otherwise.
4770 function check_password_policy($password, &$errmsg) {
4771 global $CFG;
4773 if (empty($CFG->passwordpolicy)) {
4774 return true;
4777 $errmsg = '';
4778 if (core_text::strlen($password) < $CFG->minpasswordlength) {
4779 $errmsg .= '<div>'. get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength) .'</div>';
4782 if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) {
4783 $errmsg .= '<div>'. get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits) .'</div>';
4786 if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) {
4787 $errmsg .= '<div>'. get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower) .'</div>';
4790 if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) {
4791 $errmsg .= '<div>'. get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper) .'</div>';
4794 if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) {
4795 $errmsg .= '<div>'. get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum) .'</div>';
4797 if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) {
4798 $errmsg .= '<div>'. get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars) .'</div>';
4801 if ($errmsg == '') {
4802 return true;
4803 } else {
4804 return false;
4810 * When logging in, this function is run to set certain preferences for the current SESSION.
4812 function set_login_session_preferences() {
4813 global $SESSION;
4815 $SESSION->justloggedin = true;
4817 unset($SESSION->lang);
4822 * Delete a course, including all related data from the database, and any associated files.
4824 * @param mixed $courseorid The id of the course or course object to delete.
4825 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4826 * @return bool true if all the removals succeeded. false if there were any failures. If this
4827 * method returns false, some of the removals will probably have succeeded, and others
4828 * failed, but you have no way of knowing which.
4830 function delete_course($courseorid, $showfeedback = true) {
4831 global $DB;
4833 if (is_object($courseorid)) {
4834 $courseid = $courseorid->id;
4835 $course = $courseorid;
4836 } else {
4837 $courseid = $courseorid;
4838 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
4839 return false;
4842 $context = context_course::instance($courseid);
4844 // Frontpage course can not be deleted!!
4845 if ($courseid == SITEID) {
4846 return false;
4849 // Make the course completely empty.
4850 remove_course_contents($courseid, $showfeedback);
4852 // Delete the course and related context instance.
4853 context_helper::delete_instance(CONTEXT_COURSE, $courseid);
4855 // We will update the course's timemodified, as it will be passed to the course_deleted event,
4856 // which should know about this updated property, as this event is meant to pass the full course record.
4857 $course->timemodified = time();
4859 $DB->delete_records("course", array("id" => $courseid));
4860 $DB->delete_records("course_format_options", array("courseid" => $courseid));
4862 // Trigger a course deleted event.
4863 $event = \core\event\course_deleted::create(array(
4864 'objectid' => $course->id,
4865 'context' => $context,
4866 'other' => array('shortname' => $course->shortname,
4867 'fullname' => $course->fullname)
4869 $event->add_record_snapshot('course', $course);
4870 $event->trigger();
4872 return true;
4876 * Clear a course out completely, deleting all content but don't delete the course itself.
4878 * This function does not verify any permissions.
4880 * Please note this function also deletes all user enrolments,
4881 * enrolment instances and role assignments by default.
4883 * $options:
4884 * - 'keep_roles_and_enrolments' - false by default
4885 * - 'keep_groups_and_groupings' - false by default
4887 * @param int $courseid The id of the course that is being deleted
4888 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4889 * @param array $options extra options
4890 * @return bool true if all the removals succeeded. false if there were any failures. If this
4891 * method returns false, some of the removals will probably have succeeded, and others
4892 * failed, but you have no way of knowing which.
4894 function remove_course_contents($courseid, $showfeedback = true, array $options = null) {
4895 global $CFG, $DB, $OUTPUT;
4897 require_once($CFG->libdir.'/badgeslib.php');
4898 require_once($CFG->libdir.'/completionlib.php');
4899 require_once($CFG->libdir.'/questionlib.php');
4900 require_once($CFG->libdir.'/gradelib.php');
4901 require_once($CFG->dirroot.'/group/lib.php');
4902 require_once($CFG->dirroot.'/tag/coursetagslib.php');
4903 require_once($CFG->dirroot.'/comment/lib.php');
4904 require_once($CFG->dirroot.'/rating/lib.php');
4906 // Handle course badges.
4907 badges_handle_course_deletion($courseid);
4909 // NOTE: these concatenated strings are suboptimal, but it is just extra info...
4910 $strdeleted = get_string('deleted').' - ';
4912 // Some crazy wishlist of stuff we should skip during purging of course content.
4913 $options = (array)$options;
4915 $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
4916 $coursecontext = context_course::instance($courseid);
4917 $fs = get_file_storage();
4919 // Delete course completion information, this has to be done before grades and enrols.
4920 $cc = new completion_info($course);
4921 $cc->clear_criteria();
4922 if ($showfeedback) {
4923 echo $OUTPUT->notification($strdeleted.get_string('completion', 'completion'), 'notifysuccess');
4926 // Remove all data from gradebook - this needs to be done before course modules
4927 // because while deleting this information, the system may need to reference
4928 // the course modules that own the grades.
4929 remove_course_grades($courseid, $showfeedback);
4930 remove_grade_letters($coursecontext, $showfeedback);
4932 // Delete course blocks in any all child contexts,
4933 // they may depend on modules so delete them first.
4934 $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
4935 foreach ($childcontexts as $childcontext) {
4936 blocks_delete_all_for_context($childcontext->id);
4938 unset($childcontexts);
4939 blocks_delete_all_for_context($coursecontext->id);
4940 if ($showfeedback) {
4941 echo $OUTPUT->notification($strdeleted.get_string('type_block_plural', 'plugin'), 'notifysuccess');
4944 // Delete every instance of every module,
4945 // this has to be done before deleting of course level stuff.
4946 $locations = core_component::get_plugin_list('mod');
4947 foreach ($locations as $modname => $moddir) {
4948 if ($modname === 'NEWMODULE') {
4949 continue;
4951 if ($module = $DB->get_record('modules', array('name' => $modname))) {
4952 include_once("$moddir/lib.php"); // Shows php warning only if plugin defective.
4953 $moddelete = $modname .'_delete_instance'; // Delete everything connected to an instance.
4954 $moddeletecourse = $modname .'_delete_course'; // Delete other stray stuff (uncommon).
4956 if ($instances = $DB->get_records($modname, array('course' => $course->id))) {
4957 foreach ($instances as $instance) {
4958 if ($cm = get_coursemodule_from_instance($modname, $instance->id, $course->id)) {
4959 // Delete activity context questions and question categories.
4960 question_delete_activity($cm, $showfeedback);
4962 if (function_exists($moddelete)) {
4963 // This purges all module data in related tables, extra user prefs, settings, etc.
4964 $moddelete($instance->id);
4965 } else {
4966 // NOTE: we should not allow installation of modules with missing delete support!
4967 debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!");
4968 $DB->delete_records($modname, array('id' => $instance->id));
4971 if ($cm) {
4972 // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition.
4973 context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4974 $DB->delete_records('course_modules', array('id' => $cm->id));
4978 if (function_exists($moddeletecourse)) {
4979 // Execute ptional course cleanup callback.
4980 $moddeletecourse($course, $showfeedback);
4982 if ($instances and $showfeedback) {
4983 echo $OUTPUT->notification($strdeleted.get_string('pluginname', $modname), 'notifysuccess');
4985 } else {
4986 // Ooops, this module is not properly installed, force-delete it in the next block.
4990 // We have tried to delete everything the nice way - now let's force-delete any remaining module data.
4992 // Remove all data from availability and completion tables that is associated
4993 // with course-modules belonging to this course. Note this is done even if the
4994 // features are not enabled now, in case they were enabled previously.
4995 $DB->delete_records_select('course_modules_completion',
4996 'coursemoduleid IN (SELECT id from {course_modules} WHERE course=?)',
4997 array($courseid));
4998 $DB->delete_records_select('course_modules_availability',
4999 'coursemoduleid IN (SELECT id from {course_modules} WHERE course=?)',
5000 array($courseid));
5001 $DB->delete_records_select('course_modules_avail_fields',
5002 'coursemoduleid IN (SELECT id from {course_modules} WHERE course=?)',
5003 array($courseid));
5005 // Remove course-module data.
5006 $cms = $DB->get_records('course_modules', array('course' => $course->id));
5007 foreach ($cms as $cm) {
5008 if ($module = $DB->get_record('modules', array('id' => $cm->module))) {
5009 try {
5010 $DB->delete_records($module->name, array('id' => $cm->instance));
5011 } catch (Exception $e) {
5012 // Ignore weird or missing table problems.
5015 context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
5016 $DB->delete_records('course_modules', array('id' => $cm->id));
5019 if ($showfeedback) {
5020 echo $OUTPUT->notification($strdeleted.get_string('type_mod_plural', 'plugin'), 'notifysuccess');
5023 // Cleanup the rest of plugins.
5024 $cleanuplugintypes = array('report', 'coursereport', 'format');
5025 foreach ($cleanuplugintypes as $type) {
5026 $plugins = get_plugin_list_with_function($type, 'delete_course', 'lib.php');
5027 foreach ($plugins as $plugin => $pluginfunction) {
5028 $pluginfunction($course->id, $showfeedback);
5030 if ($showfeedback) {
5031 echo $OUTPUT->notification($strdeleted.get_string('type_'.$type.'_plural', 'plugin'), 'notifysuccess');
5035 // Delete questions and question categories.
5036 question_delete_course($course, $showfeedback);
5037 if ($showfeedback) {
5038 echo $OUTPUT->notification($strdeleted.get_string('questions', 'question'), 'notifysuccess');
5041 // Make sure there are no subcontexts left - all valid blocks and modules should be already gone.
5042 $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
5043 foreach ($childcontexts as $childcontext) {
5044 $childcontext->delete();
5046 unset($childcontexts);
5048 // Remove all roles and enrolments by default.
5049 if (empty($options['keep_roles_and_enrolments'])) {
5050 // This hack is used in restore when deleting contents of existing course.
5051 role_unassign_all(array('contextid' => $coursecontext->id, 'component' => ''), true);
5052 enrol_course_delete($course);
5053 if ($showfeedback) {
5054 echo $OUTPUT->notification($strdeleted.get_string('type_enrol_plural', 'plugin'), 'notifysuccess');
5058 // Delete any groups, removing members and grouping/course links first.
5059 if (empty($options['keep_groups_and_groupings'])) {
5060 groups_delete_groupings($course->id, $showfeedback);
5061 groups_delete_groups($course->id, $showfeedback);
5064 // Filters be gone!
5065 filter_delete_all_for_context($coursecontext->id);
5067 // Die comments!
5068 comment::delete_comments($coursecontext->id);
5070 // Ratings are history too.
5071 $delopt = new stdclass();
5072 $delopt->contextid = $coursecontext->id;
5073 $rm = new rating_manager();
5074 $rm->delete_ratings($delopt);
5076 // Delete course tags.
5077 coursetag_delete_course_tags($course->id, $showfeedback);
5079 // Delete calendar events.
5080 $DB->delete_records('event', array('courseid' => $course->id));
5081 $fs->delete_area_files($coursecontext->id, 'calendar');
5083 // Delete all related records in other core tables that may have a courseid
5084 // This array stores the tables that need to be cleared, as
5085 // table_name => column_name that contains the course id.
5086 $tablestoclear = array(
5087 'log' => 'course', // Course logs (NOTE: this might be changed in the future).
5088 'backup_courses' => 'courseid', // Scheduled backup stuff.
5089 'user_lastaccess' => 'courseid', // User access info.
5091 foreach ($tablestoclear as $table => $col) {
5092 $DB->delete_records($table, array($col => $course->id));
5095 // Delete all course backup files.
5096 $fs->delete_area_files($coursecontext->id, 'backup');
5098 // Cleanup course record - remove links to deleted stuff.
5099 $oldcourse = new stdClass();
5100 $oldcourse->id = $course->id;
5101 $oldcourse->summary = '';
5102 $oldcourse->modinfo = null;
5103 $oldcourse->legacyfiles = 0;
5104 $oldcourse->enablecompletion = 0;
5105 if (!empty($options['keep_groups_and_groupings'])) {
5106 $oldcourse->defaultgroupingid = 0;
5108 $DB->update_record('course', $oldcourse);
5110 // Delete course sections and availability options.
5111 $DB->delete_records_select('course_sections_availability',
5112 'coursesectionid IN (SELECT id from {course_sections} WHERE course=?)',
5113 array($course->id));
5114 $DB->delete_records_select('course_sections_avail_fields',
5115 'coursesectionid IN (SELECT id from {course_sections} WHERE course=?)',
5116 array($course->id));
5117 $DB->delete_records('course_sections', array('course' => $course->id));
5119 // Delete legacy, section and any other course files.
5120 $fs->delete_area_files($coursecontext->id, 'course'); // Files from summary and section.
5122 // Delete all remaining stuff linked to context such as files, comments, ratings, etc.
5123 if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) {
5124 // Easy, do not delete the context itself...
5125 $coursecontext->delete_content();
5126 } else {
5127 // Hack alert!!!!
5128 // We can not drop all context stuff because it would bork enrolments and roles,
5129 // there might be also files used by enrol plugins...
5132 // Delete legacy files - just in case some files are still left there after conversion to new file api,
5133 // also some non-standard unsupported plugins may try to store something there.
5134 fulldelete($CFG->dataroot.'/'.$course->id);
5136 // Trigger a course content deleted event.
5137 $event = \core\event\course_content_deleted::create(array(
5138 'objectid' => $course->id,
5139 'context' => $coursecontext,
5140 'other' => array('shortname' => $course->shortname,
5141 'fullname' => $course->fullname,
5142 'options' => $options) // Passing this for legacy reasons.
5144 $event->add_record_snapshot('course', $course);
5145 $event->trigger();
5147 return true;
5151 * Change dates in module - used from course reset.
5153 * @param string $modname forum, assignment, etc
5154 * @param array $fields array of date fields from mod table
5155 * @param int $timeshift time difference
5156 * @param int $courseid
5157 * @return bool success
5159 function shift_course_mod_dates($modname, $fields, $timeshift, $courseid) {
5160 global $CFG, $DB;
5161 include_once($CFG->dirroot.'/mod/'.$modname.'/lib.php');
5163 $return = true;
5164 foreach ($fields as $field) {
5165 $updatesql = "UPDATE {".$modname."}
5166 SET $field = $field + ?
5167 WHERE course=? AND $field<>0";
5168 $return = $DB->execute($updatesql, array($timeshift, $courseid)) && $return;
5171 $refreshfunction = $modname.'_refresh_events';
5172 if (function_exists($refreshfunction)) {
5173 $refreshfunction($courseid);
5176 return $return;
5180 * This function will empty a course of user data.
5181 * It will retain the activities and the structure of the course.
5183 * @param object $data an object containing all the settings including courseid (without magic quotes)
5184 * @return array status array of array component, item, error
5186 function reset_course_userdata($data) {
5187 global $CFG, $DB;
5188 require_once($CFG->libdir.'/gradelib.php');
5189 require_once($CFG->libdir.'/completionlib.php');
5190 require_once($CFG->dirroot.'/group/lib.php');
5192 $data->courseid = $data->id;
5193 $context = context_course::instance($data->courseid);
5195 // Calculate the time shift of dates.
5196 if (!empty($data->reset_start_date)) {
5197 // Time part of course startdate should be zero.
5198 $data->timeshift = $data->reset_start_date - usergetmidnight($data->reset_start_date_old);
5199 } else {
5200 $data->timeshift = 0;
5203 // Result array: component, item, error.
5204 $status = array();
5206 // Start the resetting.
5207 $componentstr = get_string('general');
5209 // Move the course start time.
5210 if (!empty($data->reset_start_date) and $data->timeshift) {
5211 // Change course start data.
5212 $DB->set_field('course', 'startdate', $data->reset_start_date, array('id' => $data->courseid));
5213 // Update all course and group events - do not move activity events.
5214 $updatesql = "UPDATE {event}
5215 SET timestart = timestart + ?
5216 WHERE courseid=? AND instance=0";
5217 $DB->execute($updatesql, array($data->timeshift, $data->courseid));
5219 $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false);
5222 if (!empty($data->reset_logs)) {
5223 $DB->delete_records('log', array('course' => $data->courseid));
5224 $status[] = array('component' => $componentstr, 'item' => get_string('deletelogs'), 'error' => false);
5227 if (!empty($data->reset_events)) {
5228 $DB->delete_records('event', array('courseid' => $data->courseid));
5229 $status[] = array('component' => $componentstr, 'item' => get_string('deleteevents', 'calendar'), 'error' => false);
5232 if (!empty($data->reset_notes)) {
5233 require_once($CFG->dirroot.'/notes/lib.php');
5234 note_delete_all($data->courseid);
5235 $status[] = array('component' => $componentstr, 'item' => get_string('deletenotes', 'notes'), 'error' => false);
5238 if (!empty($data->delete_blog_associations)) {
5239 require_once($CFG->dirroot.'/blog/lib.php');
5240 blog_remove_associations_for_course($data->courseid);
5241 $status[] = array('component' => $componentstr, 'item' => get_string('deleteblogassociations', 'blog'), 'error' => false);
5244 if (!empty($data->reset_completion)) {
5245 // Delete course and activity completion information.
5246 $course = $DB->get_record('course', array('id' => $data->courseid));
5247 $cc = new completion_info($course);
5248 $cc->delete_all_completion_data();
5249 $status[] = array('component' => $componentstr,
5250 'item' => get_string('deletecompletiondata', 'completion'), 'error' => false);
5253 $componentstr = get_string('roles');
5255 if (!empty($data->reset_roles_overrides)) {
5256 $children = $context->get_child_contexts();
5257 foreach ($children as $child) {
5258 $DB->delete_records('role_capabilities', array('contextid' => $child->id));
5260 $DB->delete_records('role_capabilities', array('contextid' => $context->id));
5261 // Force refresh for logged in users.
5262 $context->mark_dirty();
5263 $status[] = array('component' => $componentstr, 'item' => get_string('deletecourseoverrides', 'role'), 'error' => false);
5266 if (!empty($data->reset_roles_local)) {
5267 $children = $context->get_child_contexts();
5268 foreach ($children as $child) {
5269 role_unassign_all(array('contextid' => $child->id));
5271 // Force refresh for logged in users.
5272 $context->mark_dirty();
5273 $status[] = array('component' => $componentstr, 'item' => get_string('deletelocalroles', 'role'), 'error' => false);
5276 // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc.
5277 $data->unenrolled = array();
5278 if (!empty($data->unenrol_users)) {
5279 $plugins = enrol_get_plugins(true);
5280 $instances = enrol_get_instances($data->courseid, true);
5281 foreach ($instances as $key => $instance) {
5282 if (!isset($plugins[$instance->enrol])) {
5283 unset($instances[$key]);
5284 continue;
5288 foreach ($data->unenrol_users as $withroleid) {
5289 if ($withroleid) {
5290 $sql = "SELECT ue.*
5291 FROM {user_enrolments} ue
5292 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5293 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5294 JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)";
5295 $params = array('courseid' => $data->courseid, 'roleid' => $withroleid, 'courselevel' => CONTEXT_COURSE);
5297 } else {
5298 // Without any role assigned at course context.
5299 $sql = "SELECT ue.*
5300 FROM {user_enrolments} ue
5301 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5302 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5303 LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid)
5304 WHERE ra.id IS null";
5305 $params = array('courseid' => $data->courseid, 'courselevel' => CONTEXT_COURSE);
5308 $rs = $DB->get_recordset_sql($sql, $params);
5309 foreach ($rs as $ue) {
5310 if (!isset($instances[$ue->enrolid])) {
5311 continue;
5313 $instance = $instances[$ue->enrolid];
5314 $plugin = $plugins[$instance->enrol];
5315 if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) {
5316 continue;
5319 $plugin->unenrol_user($instance, $ue->userid);
5320 $data->unenrolled[$ue->userid] = $ue->userid;
5322 $rs->close();
5325 if (!empty($data->unenrolled)) {
5326 $status[] = array(
5327 'component' => $componentstr,
5328 'item' => get_string('unenrol', 'enrol').' ('.count($data->unenrolled).')',
5329 'error' => false
5333 $componentstr = get_string('groups');
5335 // Remove all group members.
5336 if (!empty($data->reset_groups_members)) {
5337 groups_delete_group_members($data->courseid);
5338 $status[] = array('component' => $componentstr, 'item' => get_string('removegroupsmembers', 'group'), 'error' => false);
5341 // Remove all groups.
5342 if (!empty($data->reset_groups_remove)) {
5343 groups_delete_groups($data->courseid, false);
5344 $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroups', 'group'), 'error' => false);
5347 // Remove all grouping members.
5348 if (!empty($data->reset_groupings_members)) {
5349 groups_delete_groupings_groups($data->courseid, false);
5350 $status[] = array('component' => $componentstr, 'item' => get_string('removegroupingsmembers', 'group'), 'error' => false);
5353 // Remove all groupings.
5354 if (!empty($data->reset_groupings_remove)) {
5355 groups_delete_groupings($data->courseid, false);
5356 $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroupings', 'group'), 'error' => false);
5359 // Look in every instance of every module for data to delete.
5360 $unsupportedmods = array();
5361 if ($allmods = $DB->get_records('modules') ) {
5362 foreach ($allmods as $mod) {
5363 $modname = $mod->name;
5364 $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php';
5365 $moddeleteuserdata = $modname.'_reset_userdata'; // Function to delete user data.
5366 if (file_exists($modfile)) {
5367 if (!$DB->count_records($modname, array('course' => $data->courseid))) {
5368 continue; // Skip mods with no instances.
5370 include_once($modfile);
5371 if (function_exists($moddeleteuserdata)) {
5372 $modstatus = $moddeleteuserdata($data);
5373 if (is_array($modstatus)) {
5374 $status = array_merge($status, $modstatus);
5375 } else {
5376 debugging('Module '.$modname.' returned incorrect staus - must be an array!');
5378 } else {
5379 $unsupportedmods[] = $mod;
5381 } else {
5382 debugging('Missing lib.php in '.$modname.' module!');
5387 // Mention unsupported mods.
5388 if (!empty($unsupportedmods)) {
5389 foreach ($unsupportedmods as $mod) {
5390 $status[] = array(
5391 'component' => get_string('modulenameplural', $mod->name),
5392 'item' => '',
5393 'error' => get_string('resetnotimplemented')
5398 $componentstr = get_string('gradebook', 'grades');
5399 // Reset gradebook,.
5400 if (!empty($data->reset_gradebook_items)) {
5401 remove_course_grades($data->courseid, false);
5402 grade_grab_course_grades($data->courseid);
5403 grade_regrade_final_grades($data->courseid);
5404 $status[] = array('component' => $componentstr, 'item' => get_string('removeallcourseitems', 'grades'), 'error' => false);
5406 } else if (!empty($data->reset_gradebook_grades)) {
5407 grade_course_reset($data->courseid);
5408 $status[] = array('component' => $componentstr, 'item' => get_string('removeallcoursegrades', 'grades'), 'error' => false);
5410 // Reset comments.
5411 if (!empty($data->reset_comments)) {
5412 require_once($CFG->dirroot.'/comment/lib.php');
5413 comment::reset_course_page_comments($context);
5416 return $status;
5420 * Generate an email processing address.
5422 * @param int $modid
5423 * @param string $modargs
5424 * @return string Returns email processing address
5426 function generate_email_processing_address($modid, $modargs) {
5427 global $CFG;
5429 $header = $CFG->mailprefix . substr(base64_encode(pack('C', $modid)), 0, 2).$modargs;
5430 return $header . substr(md5($header.get_site_identifier()), 0, 16).'@'.$CFG->maildomain;
5436 * @todo Finish documenting this function
5438 * @param string $modargs
5439 * @param string $body Currently unused
5441 function moodle_process_email($modargs, $body) {
5442 global $DB;
5444 // The first char should be an unencoded letter. We'll take this as an action.
5445 switch ($modargs{0}) {
5446 case 'B': { // Bounce.
5447 list(, $userid) = unpack('V', base64_decode(substr($modargs, 1, 8)));
5448 if ($user = $DB->get_record("user", array('id' => $userid), "id,email")) {
5449 // Check the half md5 of their email.
5450 $md5check = substr(md5($user->email), 0, 16);
5451 if ($md5check == substr($modargs, -16)) {
5452 set_bounce_count($user);
5454 // Else maybe they've already changed it?
5457 break;
5458 // Maybe more later?
5462 // CORRESPONDENCE.
5465 * Get mailer instance, enable buffering, flush buffer or disable buffering.
5467 * @param string $action 'get', 'buffer', 'close' or 'flush'
5468 * @return moodle_phpmailer|null mailer instance if 'get' used or nothing
5470 function get_mailer($action='get') {
5471 global $CFG;
5473 static $mailer = null;
5474 static $counter = 0;
5476 if (!isset($CFG->smtpmaxbulk)) {
5477 $CFG->smtpmaxbulk = 1;
5480 if ($action == 'get') {
5481 $prevkeepalive = false;
5483 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5484 if ($counter < $CFG->smtpmaxbulk and !$mailer->IsError()) {
5485 $counter++;
5486 // Reset the mailer.
5487 $mailer->Priority = 3;
5488 $mailer->CharSet = 'UTF-8'; // Our default.
5489 $mailer->ContentType = "text/plain";
5490 $mailer->Encoding = "8bit";
5491 $mailer->From = "root@localhost";
5492 $mailer->FromName = "Root User";
5493 $mailer->Sender = "";
5494 $mailer->Subject = "";
5495 $mailer->Body = "";
5496 $mailer->AltBody = "";
5497 $mailer->ConfirmReadingTo = "";
5499 $mailer->ClearAllRecipients();
5500 $mailer->ClearReplyTos();
5501 $mailer->ClearAttachments();
5502 $mailer->ClearCustomHeaders();
5503 return $mailer;
5506 $prevkeepalive = $mailer->SMTPKeepAlive;
5507 get_mailer('flush');
5510 include_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php');
5511 $mailer = new moodle_phpmailer();
5513 $counter = 1;
5515 // Mailer version.
5516 $mailer->Version = 'Moodle '.$CFG->version;
5517 // Plugin directory (eg smtp plugin).
5518 $mailer->PluginDir = $CFG->libdir.'/phpmailer/';
5519 $mailer->CharSet = 'UTF-8';
5521 // Some MTAs may do double conversion of LF if CRLF used, CRLF is required line ending in RFC 822bis.
5522 if (isset($CFG->mailnewline) and $CFG->mailnewline == 'CRLF') {
5523 $mailer->LE = "\r\n";
5524 } else {
5525 $mailer->LE = "\n";
5528 if ($CFG->smtphosts == 'qmail') {
5529 // Use Qmail system.
5530 $mailer->IsQmail();
5532 } else if (empty($CFG->smtphosts)) {
5533 // Use PHP mail() = sendmail.
5534 $mailer->IsMail();
5536 } else {
5537 // Use SMTP directly.
5538 $mailer->IsSMTP();
5539 if (!empty($CFG->debugsmtp)) {
5540 $mailer->SMTPDebug = true;
5542 // Specify main and backup servers.
5543 $mailer->Host = $CFG->smtphosts;
5544 // Specify secure connection protocol.
5545 $mailer->SMTPSecure = $CFG->smtpsecure;
5546 // Use previous keepalive.
5547 $mailer->SMTPKeepAlive = $prevkeepalive;
5549 if ($CFG->smtpuser) {
5550 // Use SMTP authentication.
5551 $mailer->SMTPAuth = true;
5552 $mailer->Username = $CFG->smtpuser;
5553 $mailer->Password = $CFG->smtppass;
5557 return $mailer;
5560 $nothing = null;
5562 // Keep smtp session open after sending.
5563 if ($action == 'buffer') {
5564 if (!empty($CFG->smtpmaxbulk)) {
5565 get_mailer('flush');
5566 $m = get_mailer();
5567 if ($m->Mailer == 'smtp') {
5568 $m->SMTPKeepAlive = true;
5571 return $nothing;
5574 // Close smtp session, but continue buffering.
5575 if ($action == 'flush') {
5576 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5577 if (!empty($mailer->SMTPDebug)) {
5578 echo '<pre>'."\n";
5580 $mailer->SmtpClose();
5581 if (!empty($mailer->SMTPDebug)) {
5582 echo '</pre>';
5585 return $nothing;
5588 // Close smtp session, do not buffer anymore.
5589 if ($action == 'close') {
5590 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5591 get_mailer('flush');
5592 $mailer->SMTPKeepAlive = false;
5594 $mailer = null; // Better force new instance.
5595 return $nothing;
5600 * Send an email to a specified user
5602 * @param stdClass $user A {@link $USER} object
5603 * @param stdClass $from A {@link $USER} object
5604 * @param string $subject plain text subject line of the email
5605 * @param string $messagetext plain text version of the message
5606 * @param string $messagehtml complete html version of the message (optional)
5607 * @param string $attachment a file on the filesystem, relative to $CFG->dataroot
5608 * @param string $attachname the name of the file (extension indicates MIME)
5609 * @param bool $usetrueaddress determines whether $from email address should
5610 * be sent out. Will be overruled by user profile setting for maildisplay
5611 * @param string $replyto Email address to reply to
5612 * @param string $replytoname Name of reply to recipient
5613 * @param int $wordwrapwidth custom word wrap width, default 79
5614 * @return bool Returns true if mail was sent OK and false if there was an error.
5616 function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '', $attachment = '', $attachname = '',
5617 $usetrueaddress = true, $replyto = '', $replytoname = '', $wordwrapwidth = 79) {
5619 global $CFG;
5621 if (empty($user) || empty($user->email)) {
5622 $nulluser = 'User is null or has no email';
5623 error_log($nulluser);
5624 if (CLI_SCRIPT) {
5625 mtrace('Error: lib/moodlelib.php email_to_user(): '.$nulluser);
5627 return false;
5630 if (!empty($user->deleted)) {
5631 // Do not mail deleted users.
5632 $userdeleted = 'User is deleted';
5633 error_log($userdeleted);
5634 if (CLI_SCRIPT) {
5635 mtrace('Error: lib/moodlelib.php email_to_user(): '.$userdeleted);
5637 return false;
5640 if (!empty($CFG->noemailever)) {
5641 // Hidden setting for development sites, set in config.php if needed.
5642 $noemail = 'Not sending email due to noemailever config setting';
5643 error_log($noemail);
5644 if (CLI_SCRIPT) {
5645 mtrace('Error: lib/moodlelib.php email_to_user(): '.$noemail);
5647 return true;
5650 if (!empty($CFG->divertallemailsto)) {
5651 $subject = "[DIVERTED {$user->email}] $subject";
5652 $user = clone($user);
5653 $user->email = $CFG->divertallemailsto;
5656 // Skip mail to suspended users.
5657 if ((isset($user->auth) && $user->auth=='nologin') or (isset($user->suspended) && $user->suspended)) {
5658 return true;
5661 if (!validate_email($user->email)) {
5662 // We can not send emails to invalid addresses - it might create security issue or confuse the mailer.
5663 $invalidemail = "User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending.";
5664 error_log($invalidemail);
5665 if (CLI_SCRIPT) {
5666 mtrace('Error: lib/moodlelib.php email_to_user(): '.$invalidemail);
5668 return false;
5671 if (over_bounce_threshold($user)) {
5672 $bouncemsg = "User $user->id (".fullname($user).") is over bounce threshold! Not sending.";
5673 error_log($bouncemsg);
5674 if (CLI_SCRIPT) {
5675 mtrace('Error: lib/moodlelib.php email_to_user(): '.$bouncemsg);
5677 return false;
5680 // If the user is a remote mnet user, parse the email text for URL to the
5681 // wwwroot and modify the url to direct the user's browser to login at their
5682 // home site (identity provider - idp) before hitting the link itself.
5683 if (is_mnet_remote_user($user)) {
5684 require_once($CFG->dirroot.'/mnet/lib.php');
5686 $jumpurl = mnet_get_idp_jump_url($user);
5687 $callback = partial('mnet_sso_apply_indirection', $jumpurl);
5689 $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%",
5690 $callback,
5691 $messagetext);
5692 $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%",
5693 $callback,
5694 $messagehtml);
5696 $mail = get_mailer();
5698 if (!empty($mail->SMTPDebug)) {
5699 echo '<pre>' . "\n";
5702 $temprecipients = array();
5703 $tempreplyto = array();
5705 $supportuser = generate_email_supportuser();
5707 // Make up an email address for handling bounces.
5708 if (!empty($CFG->handlebounces)) {
5709 $modargs = 'B'.base64_encode(pack('V', $user->id)).substr(md5($user->email), 0, 16);
5710 $mail->Sender = generate_email_processing_address(0, $modargs);
5711 } else {
5712 $mail->Sender = $supportuser->email;
5715 if (is_string($from)) { // So we can pass whatever we want if there is need.
5716 $mail->From = $CFG->noreplyaddress;
5717 $mail->FromName = $from;
5718 } else if ($usetrueaddress and $from->maildisplay) {
5719 $mail->From = $from->email;
5720 $mail->FromName = fullname($from);
5721 } else {
5722 $mail->From = $CFG->noreplyaddress;
5723 $mail->FromName = fullname($from);
5724 if (empty($replyto)) {
5725 $tempreplyto[] = array($CFG->noreplyaddress, get_string('noreplyname'));
5729 if (!empty($replyto)) {
5730 $tempreplyto[] = array($replyto, $replytoname);
5733 $mail->Subject = substr($subject, 0, 900);
5735 $temprecipients[] = array($user->email, fullname($user));
5737 // Set word wrap.
5738 $mail->WordWrap = $wordwrapwidth;
5740 if (!empty($from->customheaders)) {
5741 // Add custom headers.
5742 if (is_array($from->customheaders)) {
5743 foreach ($from->customheaders as $customheader) {
5744 $mail->AddCustomHeader($customheader);
5746 } else {
5747 $mail->AddCustomHeader($from->customheaders);
5751 if (!empty($from->priority)) {
5752 $mail->Priority = $from->priority;
5755 if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) {
5756 // Don't ever send HTML to users who don't want it.
5757 $mail->IsHTML(true);
5758 $mail->Encoding = 'quoted-printable';
5759 $mail->Body = $messagehtml;
5760 $mail->AltBody = "\n$messagetext\n";
5761 } else {
5762 $mail->IsHTML(false);
5763 $mail->Body = "\n$messagetext\n";
5766 if ($attachment && $attachname) {
5767 if (preg_match( "~\\.\\.~" , $attachment )) {
5768 // Security check for ".." in dir path.
5769 $temprecipients[] = array($supportuser->email, fullname($supportuser, true));
5770 $mail->AddStringAttachment('Error in attachment. User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain');
5771 } else {
5772 require_once($CFG->libdir.'/filelib.php');
5773 $mimetype = mimeinfo('type', $attachname);
5774 $mail->AddAttachment($CFG->dataroot .'/'. $attachment, $attachname, 'base64', $mimetype);
5778 // Check if the email should be sent in an other charset then the default UTF-8.
5779 if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) {
5781 // Use the defined site mail charset or eventually the one preferred by the recipient.
5782 $charset = $CFG->sitemailcharset;
5783 if (!empty($CFG->allowusermailcharset)) {
5784 if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) {
5785 $charset = $useremailcharset;
5789 // Convert all the necessary strings if the charset is supported.
5790 $charsets = get_list_of_charsets();
5791 unset($charsets['UTF-8']);
5792 if (in_array($charset, $charsets)) {
5793 $mail->CharSet = $charset;
5794 $mail->FromName = core_text::convert($mail->FromName, 'utf-8', strtolower($charset));
5795 $mail->Subject = core_text::convert($mail->Subject, 'utf-8', strtolower($charset));
5796 $mail->Body = core_text::convert($mail->Body, 'utf-8', strtolower($charset));
5797 $mail->AltBody = core_text::convert($mail->AltBody, 'utf-8', strtolower($charset));
5799 foreach ($temprecipients as $key => $values) {
5800 $temprecipients[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
5802 foreach ($tempreplyto as $key => $values) {
5803 $tempreplyto[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
5808 foreach ($temprecipients as $values) {
5809 $mail->AddAddress($values[0], $values[1]);
5811 foreach ($tempreplyto as $values) {
5812 $mail->AddReplyTo($values[0], $values[1]);
5815 if ($mail->Send()) {
5816 set_send_count($user);
5817 if (!empty($mail->SMTPDebug)) {
5818 echo '</pre>';
5820 return true;
5821 } else {
5822 add_to_log(SITEID, 'library', 'mailer', qualified_me(), 'ERROR: '. $mail->ErrorInfo);
5823 if (CLI_SCRIPT) {
5824 mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo);
5826 if (!empty($mail->SMTPDebug)) {
5827 echo '</pre>';
5829 return false;
5834 * Generate a signoff for emails based on support settings
5836 * @return string
5838 function generate_email_signoff() {
5839 global $CFG;
5841 $signoff = "\n";
5842 if (!empty($CFG->supportname)) {
5843 $signoff .= $CFG->supportname."\n";
5845 if (!empty($CFG->supportemail)) {
5846 $signoff .= $CFG->supportemail."\n";
5848 if (!empty($CFG->supportpage)) {
5849 $signoff .= $CFG->supportpage."\n";
5851 return $signoff;
5855 * Generate a fake user for emails based on support settings
5857 * @return stdClass user info
5859 function generate_email_supportuser() {
5860 global $CFG;
5862 static $supportuser;
5864 if (!empty($supportuser)) {
5865 return $supportuser;
5868 $supportuser = new stdClass();
5869 $supportuser->email = $CFG->supportemail ? $CFG->supportemail : $CFG->noreplyaddress;
5870 $supportuser->firstname = $CFG->supportname ? $CFG->supportname : get_string('noreplyname');
5871 $supportuser->lastname = '';
5872 $supportuser->maildisplay = true;
5874 return $supportuser;
5879 * Sets specified user's password and send the new password to the user via email.
5881 * @param stdClass $user A {@link $USER} object
5882 * @param bool $fasthash If true, use a low cost factor when generating the hash for speed.
5883 * @return bool|string Returns "true" if mail was sent OK and "false" if there was an error
5885 function setnew_password_and_mail($user, $fasthash = false) {
5886 global $CFG, $DB;
5888 // We try to send the mail in language the user understands,
5889 // unfortunately the filter_string() does not support alternative langs yet
5890 // so multilang will not work properly for site->fullname.
5891 $lang = empty($user->lang) ? $CFG->lang : $user->lang;
5893 $site = get_site();
5895 $supportuser = generate_email_supportuser();
5897 $newpassword = generate_password();
5899 $hashedpassword = hash_internal_user_password($newpassword, $fasthash);
5900 $DB->set_field('user', 'password', $hashedpassword, array('id' => $user->id));
5902 $a = new stdClass();
5903 $a->firstname = fullname($user, true);
5904 $a->sitename = format_string($site->fullname);
5905 $a->username = $user->username;
5906 $a->newpassword = $newpassword;
5907 $a->link = $CFG->wwwroot .'/login/';
5908 $a->signoff = generate_email_signoff();
5910 $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang);
5912 $subject = format_string($site->fullname) .': '. (string)new lang_string('newusernewpasswordsubj', '', $a, $lang);
5914 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
5915 return email_to_user($user, $supportuser, $subject, $message);
5920 * Resets specified user's password and send the new password to the user via email.
5922 * @param stdClass $user A {@link $USER} object
5923 * @return bool Returns true if mail was sent OK and false if there was an error.
5925 function reset_password_and_mail($user) {
5926 global $CFG;
5928 $site = get_site();
5929 $supportuser = generate_email_supportuser();
5931 $userauth = get_auth_plugin($user->auth);
5932 if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) {
5933 trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth.");
5934 return false;
5937 $newpassword = generate_password();
5939 if (!$userauth->user_update_password($user, $newpassword)) {
5940 print_error("cannotsetpassword");
5943 $a = new stdClass();
5944 $a->firstname = $user->firstname;
5945 $a->lastname = $user->lastname;
5946 $a->sitename = format_string($site->fullname);
5947 $a->username = $user->username;
5948 $a->newpassword = $newpassword;
5949 $a->link = $CFG->httpswwwroot .'/login/change_password.php';
5950 $a->signoff = generate_email_signoff();
5952 $message = get_string('newpasswordtext', '', $a);
5954 $subject = format_string($site->fullname) .': '. get_string('changedpassword');
5956 unset_user_preference('create_password', $user); // Prevent cron from generating the password.
5958 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
5959 return email_to_user($user, $supportuser, $subject, $message);
5963 * Send email to specified user with confirmation text and activation link.
5965 * @param stdClass $user A {@link $USER} object
5966 * @return bool Returns true if mail was sent OK and false if there was an error.
5968 function send_confirmation_email($user) {
5969 global $CFG;
5971 $site = get_site();
5972 $supportuser = generate_email_supportuser();
5974 $data = new stdClass();
5975 $data->firstname = fullname($user);
5976 $data->sitename = format_string($site->fullname);
5977 $data->admin = generate_email_signoff();
5979 $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname));
5981 $username = urlencode($user->username);
5982 $username = str_replace('.', '%2E', $username); // Prevent problems with trailing dots.
5983 $data->link = $CFG->wwwroot .'/login/confirm.php?data='. $user->secret .'/'. $username;
5984 $message = get_string('emailconfirmation', '', $data);
5985 $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true);
5987 $user->mailformat = 1; // Always send HTML version as well.
5989 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
5990 return email_to_user($user, $supportuser, $subject, $message, $messagehtml);
5994 * Sends a password change confirmation email.
5996 * @param stdClass $user A {@link $USER} object
5997 * @return bool Returns true if mail was sent OK and false if there was an error.
5999 function send_password_change_confirmation_email($user) {
6000 global $CFG;
6002 $site = get_site();
6003 $supportuser = generate_email_supportuser();
6005 $data = new stdClass();
6006 $data->firstname = $user->firstname;
6007 $data->lastname = $user->lastname;
6008 $data->sitename = format_string($site->fullname);
6009 $data->link = $CFG->httpswwwroot .'/login/forgot_password.php?p='. $user->secret .'&s='. urlencode($user->username);
6010 $data->admin = generate_email_signoff();
6012 $message = get_string('emailpasswordconfirmation', '', $data);
6013 $subject = get_string('emailpasswordconfirmationsubject', '', format_string($site->fullname));
6015 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6016 return email_to_user($user, $supportuser, $subject, $message);
6021 * Sends an email containinginformation on how to change your password.
6023 * @param stdClass $user A {@link $USER} object
6024 * @return bool Returns true if mail was sent OK and false if there was an error.
6026 function send_password_change_info($user) {
6027 global $CFG;
6029 $site = get_site();
6030 $supportuser = generate_email_supportuser();
6031 $systemcontext = context_system::instance();
6033 $data = new stdClass();
6034 $data->firstname = $user->firstname;
6035 $data->lastname = $user->lastname;
6036 $data->sitename = format_string($site->fullname);
6037 $data->admin = generate_email_signoff();
6039 $userauth = get_auth_plugin($user->auth);
6041 if (!is_enabled_auth($user->auth) or $user->auth == 'nologin') {
6042 $message = get_string('emailpasswordchangeinfodisabled', '', $data);
6043 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
6044 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6045 return email_to_user($user, $supportuser, $subject, $message);
6048 if ($userauth->can_change_password() and $userauth->change_password_url()) {
6049 // We have some external url for password changing.
6050 $data->link .= $userauth->change_password_url();
6052 } else {
6053 // No way to change password, sorry.
6054 $data->link = '';
6057 if (!empty($data->link) and has_capability('moodle/user:changeownpassword', $systemcontext, $user->id)) {
6058 $message = get_string('emailpasswordchangeinfo', '', $data);
6059 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
6060 } else {
6061 $message = get_string('emailpasswordchangeinfofail', '', $data);
6062 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
6065 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6066 return email_to_user($user, $supportuser, $subject, $message);
6071 * Check that an email is allowed. It returns an error message if there was a problem.
6073 * @param string $email Content of email
6074 * @return string|false
6076 function email_is_not_allowed($email) {
6077 global $CFG;
6079 if (!empty($CFG->allowemailaddresses)) {
6080 $allowed = explode(' ', $CFG->allowemailaddresses);
6081 foreach ($allowed as $allowedpattern) {
6082 $allowedpattern = trim($allowedpattern);
6083 if (!$allowedpattern) {
6084 continue;
6086 if (strpos($allowedpattern, '.') === 0) {
6087 if (strpos(strrev($email), strrev($allowedpattern)) === 0) {
6088 // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6089 return false;
6092 } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) {
6093 return false;
6096 return get_string('emailonlyallowed', '', $CFG->allowemailaddresses);
6098 } else if (!empty($CFG->denyemailaddresses)) {
6099 $denied = explode(' ', $CFG->denyemailaddresses);
6100 foreach ($denied as $deniedpattern) {
6101 $deniedpattern = trim($deniedpattern);
6102 if (!$deniedpattern) {
6103 continue;
6105 if (strpos($deniedpattern, '.') === 0) {
6106 if (strpos(strrev($email), strrev($deniedpattern)) === 0) {
6107 // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6108 return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6111 } else if (strpos(strrev($email), strrev('@'.$deniedpattern)) === 0) {
6112 return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6117 return false;
6120 // FILE HANDLING.
6123 * Returns local file storage instance
6125 * @return file_storage
6127 function get_file_storage() {
6128 global $CFG;
6130 static $fs = null;
6132 if ($fs) {
6133 return $fs;
6136 require_once("$CFG->libdir/filelib.php");
6138 if (isset($CFG->filedir)) {
6139 $filedir = $CFG->filedir;
6140 } else {
6141 $filedir = $CFG->dataroot.'/filedir';
6144 if (isset($CFG->trashdir)) {
6145 $trashdirdir = $CFG->trashdir;
6146 } else {
6147 $trashdirdir = $CFG->dataroot.'/trashdir';
6150 $fs = new file_storage($filedir, $trashdirdir, "$CFG->tempdir/filestorage", $CFG->directorypermissions, $CFG->filepermissions);
6152 return $fs;
6156 * Returns local file storage instance
6158 * @return file_browser
6160 function get_file_browser() {
6161 global $CFG;
6163 static $fb = null;
6165 if ($fb) {
6166 return $fb;
6169 require_once("$CFG->libdir/filelib.php");
6171 $fb = new file_browser();
6173 return $fb;
6177 * Returns file packer
6179 * @param string $mimetype default application/zip
6180 * @return file_packer
6182 function get_file_packer($mimetype='application/zip') {
6183 global $CFG;
6185 static $fp = array();
6187 if (isset($fp[$mimetype])) {
6188 return $fp[$mimetype];
6191 switch ($mimetype) {
6192 case 'application/zip':
6193 case 'application/vnd.moodle.backup':
6194 case 'application/vnd.moodle.profiling':
6195 $classname = 'zip_packer';
6196 break;
6197 case 'application/x-tar':
6198 // One day we hope to support tar - for the time being it is a pipe dream.
6199 default:
6200 return false;
6203 require_once("$CFG->libdir/filestorage/$classname.php");
6204 $fp[$mimetype] = new $classname();
6206 return $fp[$mimetype];
6210 * Returns current name of file on disk if it exists.
6212 * @param string $newfile File to be verified
6213 * @return string Current name of file on disk if true
6215 function valid_uploaded_file($newfile) {
6216 if (empty($newfile)) {
6217 return '';
6219 if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) {
6220 return $newfile['tmp_name'];
6221 } else {
6222 return '';
6227 * Returns the maximum size for uploading files.
6229 * There are seven possible upload limits:
6230 * 1. in Apache using LimitRequestBody (no way of checking or changing this)
6231 * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
6232 * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
6233 * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
6234 * 5. by the Moodle admin in $CFG->maxbytes
6235 * 6. by the teacher in the current course $course->maxbytes
6236 * 7. by the teacher for the current module, eg $assignment->maxbytes
6238 * These last two are passed to this function as arguments (in bytes).
6239 * Anything defined as 0 is ignored.
6240 * The smallest of all the non-zero numbers is returned.
6242 * @todo Finish documenting this function
6244 * @param int $sitebytes Set maximum size
6245 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6246 * @param int $modulebytes Current module ->maxbytes (in bytes)
6247 * @return int The maximum size for uploading files.
6249 function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0) {
6251 if (! $filesize = ini_get('upload_max_filesize')) {
6252 $filesize = '5M';
6254 $minimumsize = get_real_size($filesize);
6256 if ($postsize = ini_get('post_max_size')) {
6257 $postsize = get_real_size($postsize);
6258 if ($postsize < $minimumsize) {
6259 $minimumsize = $postsize;
6263 if (($sitebytes > 0) and ($sitebytes < $minimumsize)) {
6264 $minimumsize = $sitebytes;
6267 if (($coursebytes > 0) and ($coursebytes < $minimumsize)) {
6268 $minimumsize = $coursebytes;
6271 if (($modulebytes > 0) and ($modulebytes < $minimumsize)) {
6272 $minimumsize = $modulebytes;
6275 return $minimumsize;
6279 * Returns the maximum size for uploading files for the current user
6281 * This function takes in account {@link get_max_upload_file_size()} the user's capabilities
6283 * @param context $context The context in which to check user capabilities
6284 * @param int $sitebytes Set maximum size
6285 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6286 * @param int $modulebytes Current module ->maxbytes (in bytes)
6287 * @param stdClass $user The user
6288 * @return int The maximum size for uploading files.
6290 function get_user_max_upload_file_size($context, $sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $user = null) {
6291 global $USER;
6293 if (empty($user)) {
6294 $user = $USER;
6297 if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) {
6298 return USER_CAN_IGNORE_FILE_SIZE_LIMITS;
6301 return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes);
6305 * Returns an array of possible sizes in local language
6307 * Related to {@link get_max_upload_file_size()} - this function returns an
6308 * array of possible sizes in an array, translated to the
6309 * local language.
6311 * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes.
6313 * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)"
6314 * with the value set to 0. This option will be the first in the list.
6316 * @uses SORT_NUMERIC
6317 * @param int $sitebytes Set maximum size
6318 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6319 * @param int $modulebytes Current module ->maxbytes (in bytes)
6320 * @param int|array $custombytes custom upload size/s which will be added to list,
6321 * Only value/s smaller then maxsize will be added to list.
6322 * @return array
6324 function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null) {
6325 global $CFG;
6327 if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) {
6328 return array();
6331 if ($sitebytes == 0) {
6332 // Will get the minimum of upload_max_filesize or post_max_size.
6333 $sitebytes = get_max_upload_file_size();
6336 $filesize = array();
6337 $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152,
6338 5242880, 10485760, 20971520, 52428800, 104857600);
6340 // If custombytes is given and is valid then add it to the list.
6341 if (is_number($custombytes) and $custombytes > 0) {
6342 $custombytes = (int)$custombytes;
6343 if (!in_array($custombytes, $sizelist)) {
6344 $sizelist[] = $custombytes;
6346 } else if (is_array($custombytes)) {
6347 $sizelist = array_unique(array_merge($sizelist, $custombytes));
6350 // Allow maxbytes to be selected if it falls outside the above boundaries.
6351 if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) {
6352 // Note: get_real_size() is used in order to prevent problems with invalid values.
6353 $sizelist[] = get_real_size($CFG->maxbytes);
6356 foreach ($sizelist as $sizebytes) {
6357 if ($sizebytes < $maxsize && $sizebytes > 0) {
6358 $filesize[(string)intval($sizebytes)] = display_size($sizebytes);
6362 $limitlevel = '';
6363 $displaysize = '';
6364 if ($modulebytes &&
6365 (($modulebytes < $coursebytes || $coursebytes == 0) &&
6366 ($modulebytes < $sitebytes || $sitebytes == 0))) {
6367 $limitlevel = get_string('activity', 'core');
6368 $displaysize = display_size($modulebytes);
6369 $filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list.
6371 } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) {
6372 $limitlevel = get_string('course', 'core');
6373 $displaysize = display_size($coursebytes);
6374 $filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list.
6376 } else if ($sitebytes) {
6377 $limitlevel = get_string('site', 'core');
6378 $displaysize = display_size($sitebytes);
6379 $filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list.
6382 krsort($filesize, SORT_NUMERIC);
6383 if ($limitlevel) {
6384 $params = (object) array('contextname' => $limitlevel, 'displaysize' => $displaysize);
6385 $filesize = array('0' => get_string('uploadlimitwithsize', 'core', $params)) + $filesize;
6388 return $filesize;
6392 * Returns an array with all the filenames in all subdirectories, relative to the given rootdir.
6394 * If excludefiles is defined, then that file/directory is ignored
6395 * If getdirs is true, then (sub)directories are included in the output
6396 * If getfiles is true, then files are included in the output
6397 * (at least one of these must be true!)
6399 * @todo Finish documenting this function. Add examples of $excludefile usage.
6401 * @param string $rootdir A given root directory to start from
6402 * @param string|array $excludefiles If defined then the specified file/directory is ignored
6403 * @param bool $descend If true then subdirectories are recursed as well
6404 * @param bool $getdirs If true then (sub)directories are included in the output
6405 * @param bool $getfiles If true then files are included in the output
6406 * @return array An array with all the filenames in all subdirectories, relative to the given rootdir
6408 function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) {
6410 $dirs = array();
6412 if (!$getdirs and !$getfiles) { // Nothing to show.
6413 return $dirs;
6416 if (!is_dir($rootdir)) { // Must be a directory.
6417 return $dirs;
6420 if (!$dir = opendir($rootdir)) { // Can't open it for some reason.
6421 return $dirs;
6424 if (!is_array($excludefiles)) {
6425 $excludefiles = array($excludefiles);
6428 while (false !== ($file = readdir($dir))) {
6429 $firstchar = substr($file, 0, 1);
6430 if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) {
6431 continue;
6433 $fullfile = $rootdir .'/'. $file;
6434 if (filetype($fullfile) == 'dir') {
6435 if ($getdirs) {
6436 $dirs[] = $file;
6438 if ($descend) {
6439 $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles);
6440 foreach ($subdirs as $subdir) {
6441 $dirs[] = $file .'/'. $subdir;
6444 } else if ($getfiles) {
6445 $dirs[] = $file;
6448 closedir($dir);
6450 asort($dirs);
6452 return $dirs;
6457 * Adds up all the files in a directory and works out the size.
6459 * @param string $rootdir The directory to start from
6460 * @param string $excludefile A file to exclude when summing directory size
6461 * @return int The summed size of all files and subfiles within the root directory
6463 function get_directory_size($rootdir, $excludefile='') {
6464 global $CFG;
6466 // Do it this way if we can, it's much faster.
6467 if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) {
6468 $command = trim($CFG->pathtodu).' -sk '.escapeshellarg($rootdir);
6469 $output = null;
6470 $return = null;
6471 exec($command, $output, $return);
6472 if (is_array($output)) {
6473 // We told it to return k.
6474 return get_real_size(intval($output[0]).'k');
6478 if (!is_dir($rootdir)) {
6479 // Must be a directory.
6480 return 0;
6483 if (!$dir = @opendir($rootdir)) {
6484 // Can't open it for some reason.
6485 return 0;
6488 $size = 0;
6490 while (false !== ($file = readdir($dir))) {
6491 $firstchar = substr($file, 0, 1);
6492 if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) {
6493 continue;
6495 $fullfile = $rootdir .'/'. $file;
6496 if (filetype($fullfile) == 'dir') {
6497 $size += get_directory_size($fullfile, $excludefile);
6498 } else {
6499 $size += filesize($fullfile);
6502 closedir($dir);
6504 return $size;
6508 * Converts bytes into display form
6510 * @static string $gb Localized string for size in gigabytes
6511 * @static string $mb Localized string for size in megabytes
6512 * @static string $kb Localized string for size in kilobytes
6513 * @static string $b Localized string for size in bytes
6514 * @param int $size The size to convert to human readable form
6515 * @return string
6517 function display_size($size) {
6519 static $gb, $mb, $kb, $b;
6521 if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) {
6522 return get_string('unlimited');
6525 if (empty($gb)) {
6526 $gb = get_string('sizegb');
6527 $mb = get_string('sizemb');
6528 $kb = get_string('sizekb');
6529 $b = get_string('sizeb');
6532 if ($size >= 1073741824) {
6533 $size = round($size / 1073741824 * 10) / 10 . $gb;
6534 } else if ($size >= 1048576) {
6535 $size = round($size / 1048576 * 10) / 10 . $mb;
6536 } else if ($size >= 1024) {
6537 $size = round($size / 1024 * 10) / 10 . $kb;
6538 } else {
6539 $size = intval($size) .' '. $b; // File sizes over 2GB can not work in 32bit PHP anyway.
6541 return $size;
6545 * Cleans a given filename by removing suspicious or troublesome characters
6547 * @see clean_param()
6548 * @param string $string file name
6549 * @return string cleaned file name
6551 function clean_filename($string) {
6552 return clean_param($string, PARAM_FILE);
6556 // STRING TRANSLATION.
6559 * Returns the code for the current language
6561 * @category string
6562 * @return string
6564 function current_language() {
6565 global $CFG, $USER, $SESSION, $COURSE;
6567 if (!empty($COURSE->id) and $COURSE->id != SITEID and !empty($COURSE->lang)) {
6568 // Course language can override all other settings for this page.
6569 $return = $COURSE->lang;
6571 } else if (!empty($SESSION->lang)) {
6572 // Session language can override other settings.
6573 $return = $SESSION->lang;
6575 } else if (!empty($USER->lang)) {
6576 $return = $USER->lang;
6578 } else if (isset($CFG->lang)) {
6579 $return = $CFG->lang;
6581 } else {
6582 $return = 'en';
6585 // Just in case this slipped in from somewhere by accident.
6586 $return = str_replace('_utf8', '', $return);
6588 return $return;
6592 * Returns parent language of current active language if defined
6594 * @category string
6595 * @param string $lang null means current language
6596 * @return string
6598 function get_parent_language($lang=null) {
6599 global $COURSE, $SESSION;
6601 // Let's hack around the current language.
6602 if (!empty($lang)) {
6603 $oldcourselang = empty($COURSE->lang) ? '' : $COURSE->lang;
6604 $oldsessionlang = empty($SESSION->lang) ? '' : $SESSION->lang;
6605 $COURSE->lang = '';
6606 $SESSION->lang = $lang;
6609 $parentlang = get_string('parentlanguage', 'langconfig');
6610 if ($parentlang === 'en') {
6611 $parentlang = '';
6614 // Let's hack around the current language.
6615 if (!empty($lang)) {
6616 $COURSE->lang = $oldcourselang;
6617 $SESSION->lang = $oldsessionlang;
6620 return $parentlang;
6624 * Returns current string_manager instance.
6626 * The param $forcereload is needed for CLI installer only where the string_manager instance
6627 * must be replaced during the install.php script life time.
6629 * @category string
6630 * @param bool $forcereload shall the singleton be released and new instance created instead?
6631 * @return core_string_manager
6633 function get_string_manager($forcereload=false) {
6634 global $CFG;
6636 static $singleton = null;
6638 if ($forcereload) {
6639 $singleton = null;
6641 if ($singleton === null) {
6642 if (empty($CFG->early_install_lang)) {
6644 if (empty($CFG->langlist)) {
6645 $translist = array();
6646 } else {
6647 $translist = explode(',', $CFG->langlist);
6650 $singleton = new core_string_manager_standard($CFG->langotherroot, $CFG->langlocalroot, $translist);
6652 } else {
6653 $singleton = new core_string_manager_install();
6657 return $singleton;
6661 * Returns a localized string.
6663 * Returns the translated string specified by $identifier as
6664 * for $module. Uses the same format files as STphp.
6665 * $a is an object, string or number that can be used
6666 * within translation strings
6668 * eg 'hello {$a->firstname} {$a->lastname}'
6669 * or 'hello {$a}'
6671 * If you would like to directly echo the localized string use
6672 * the function {@link print_string()}
6674 * Example usage of this function involves finding the string you would
6675 * like a local equivalent of and using its identifier and module information
6676 * to retrieve it.<br/>
6677 * If you open moodle/lang/en/moodle.php and look near line 278
6678 * you will find a string to prompt a user for their word for 'course'
6679 * <code>
6680 * $string['course'] = 'Course';
6681 * </code>
6682 * So if you want to display the string 'Course'
6683 * in any language that supports it on your site
6684 * you just need to use the identifier 'course'
6685 * <code>
6686 * $mystring = '<strong>'. get_string('course') .'</strong>';
6687 * or
6688 * </code>
6689 * If the string you want is in another file you'd take a slightly
6690 * different approach. Looking in moodle/lang/en/calendar.php you find
6691 * around line 75:
6692 * <code>
6693 * $string['typecourse'] = 'Course event';
6694 * </code>
6695 * If you want to display the string "Course event" in any language
6696 * supported you would use the identifier 'typecourse' and the module 'calendar'
6697 * (because it is in the file calendar.php):
6698 * <code>
6699 * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>';
6700 * </code>
6702 * As a last resort, should the identifier fail to map to a string
6703 * the returned string will be [[ $identifier ]]
6705 * In Moodle 2.3 there is a new argument to this function $lazyload.
6706 * Setting $lazyload to true causes get_string to return a lang_string object
6707 * rather than the string itself. The fetching of the string is then put off until
6708 * the string object is first used. The object can be used by calling it's out
6709 * method or by casting the object to a string, either directly e.g.
6710 * (string)$stringobject
6711 * or indirectly by using the string within another string or echoing it out e.g.
6712 * echo $stringobject
6713 * return "<p>{$stringobject}</p>";
6714 * It is worth noting that using $lazyload and attempting to use the string as an
6715 * array key will cause a fatal error as objects cannot be used as array keys.
6716 * But you should never do that anyway!
6717 * For more information {@link lang_string}
6719 * @category string
6720 * @param string $identifier The key identifier for the localized string
6721 * @param string $component The module where the key identifier is stored,
6722 * usually expressed as the filename in the language pack without the
6723 * .php on the end but can also be written as mod/forum or grade/export/xls.
6724 * If none is specified then moodle.php is used.
6725 * @param string|object|array $a An object, string or number that can be used
6726 * within translation strings
6727 * @param bool $lazyload If set to true a string object is returned instead of
6728 * the string itself. The string then isn't calculated until it is first used.
6729 * @return string The localized string.
6730 * @throws coding_exception
6732 function get_string($identifier, $component = '', $a = null, $lazyload = false) {
6733 global $CFG;
6735 // If the lazy load argument has been supplied return a lang_string object
6736 // instead.
6737 // We need to make sure it is true (and a bool) as you will see below there
6738 // used to be a forth argument at one point.
6739 if ($lazyload === true) {
6740 return new lang_string($identifier, $component, $a);
6743 if ($CFG->debugdeveloper && clean_param($identifier, PARAM_STRINGID) === '') {
6744 throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.', DEBUG_DEVELOPER);
6747 // There is now a forth argument again, this time it is a boolean however so
6748 // we can still check for the old extralocations parameter.
6749 if (!is_bool($lazyload) && !empty($lazyload)) {
6750 debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.');
6753 if (strpos($component, '/') !== false) {
6754 debugging('The module name you passed to get_string is the deprecated format ' .
6755 'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER);
6756 $componentpath = explode('/', $component);
6758 switch ($componentpath[0]) {
6759 case 'mod':
6760 $component = $componentpath[1];
6761 break;
6762 case 'blocks':
6763 case 'block':
6764 $component = 'block_'.$componentpath[1];
6765 break;
6766 case 'enrol':
6767 $component = 'enrol_'.$componentpath[1];
6768 break;
6769 case 'format':
6770 $component = 'format_'.$componentpath[1];
6771 break;
6772 case 'grade':
6773 $component = 'grade'.$componentpath[1].'_'.$componentpath[2];
6774 break;
6778 $result = get_string_manager()->get_string($identifier, $component, $a);
6780 // Debugging feature lets you display string identifier and component.
6781 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
6782 $result .= ' {' . $identifier . '/' . $component . '}';
6784 return $result;
6788 * Converts an array of strings to their localized value.
6790 * @param array $array An array of strings
6791 * @param string $component The language module that these strings can be found in.
6792 * @return stdClass translated strings.
6794 function get_strings($array, $component = '') {
6795 $string = new stdClass;
6796 foreach ($array as $item) {
6797 $string->$item = get_string($item, $component);
6799 return $string;
6803 * Prints out a translated string.
6805 * Prints out a translated string using the return value from the {@link get_string()} function.
6807 * Example usage of this function when the string is in the moodle.php file:<br/>
6808 * <code>
6809 * echo '<strong>';
6810 * print_string('course');
6811 * echo '</strong>';
6812 * </code>
6814 * Example usage of this function when the string is not in the moodle.php file:<br/>
6815 * <code>
6816 * echo '<h1>';
6817 * print_string('typecourse', 'calendar');
6818 * echo '</h1>';
6819 * </code>
6821 * @category string
6822 * @param string $identifier The key identifier for the localized string
6823 * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used.
6824 * @param string|object|array $a An object, string or number that can be used within translation strings
6826 function print_string($identifier, $component = '', $a = null) {
6827 echo get_string($identifier, $component, $a);
6831 * Returns a list of charset codes
6833 * Returns a list of charset codes. It's hardcoded, so they should be added manually
6834 * (checking that such charset is supported by the texlib library!)
6836 * @return array And associative array with contents in the form of charset => charset
6838 function get_list_of_charsets() {
6840 $charsets = array(
6841 'EUC-JP' => 'EUC-JP',
6842 'ISO-2022-JP'=> 'ISO-2022-JP',
6843 'ISO-8859-1' => 'ISO-8859-1',
6844 'SHIFT-JIS' => 'SHIFT-JIS',
6845 'GB2312' => 'GB2312',
6846 'GB18030' => 'GB18030', // GB18030 not supported by typo and mbstring.
6847 'UTF-8' => 'UTF-8');
6849 asort($charsets);
6851 return $charsets;
6855 * Returns a list of valid and compatible themes
6857 * @return array
6859 function get_list_of_themes() {
6860 global $CFG;
6862 $themes = array();
6864 if (!empty($CFG->themelist)) { // Use admin's list of themes.
6865 $themelist = explode(',', $CFG->themelist);
6866 } else {
6867 $themelist = array_keys(core_component::get_plugin_list("theme"));
6870 foreach ($themelist as $key => $themename) {
6871 $theme = theme_config::load($themename);
6872 $themes[$themename] = $theme;
6875 core_collator::asort_objects_by_method($themes, 'get_theme_name');
6877 return $themes;
6881 * Returns a list of timezones in the current language
6883 * @return array
6885 function get_list_of_timezones() {
6886 global $DB;
6888 static $timezones;
6890 if (!empty($timezones)) { // This function has been called recently.
6891 return $timezones;
6894 $timezones = array();
6896 if ($rawtimezones = $DB->get_records_sql("SELECT MAX(id), name FROM {timezone} GROUP BY name")) {
6897 foreach ($rawtimezones as $timezone) {
6898 if (!empty($timezone->name)) {
6899 if (get_string_manager()->string_exists(strtolower($timezone->name), 'timezones')) {
6900 $timezones[$timezone->name] = get_string(strtolower($timezone->name), 'timezones');
6901 } else {
6902 $timezones[$timezone->name] = $timezone->name;
6904 if (substr($timezones[$timezone->name], 0, 1) == '[') { // No translation found.
6905 $timezones[$timezone->name] = $timezone->name;
6911 asort($timezones);
6913 for ($i = -13; $i <= 13; $i += .5) {
6914 $tzstring = 'UTC';
6915 if ($i < 0) {
6916 $timezones[sprintf("%.1f", $i)] = $tzstring . $i;
6917 } else if ($i > 0) {
6918 $timezones[sprintf("%.1f", $i)] = $tzstring . '+' . $i;
6919 } else {
6920 $timezones[sprintf("%.1f", $i)] = $tzstring;
6924 return $timezones;
6928 * Factory function for emoticon_manager
6930 * @return emoticon_manager singleton
6932 function get_emoticon_manager() {
6933 static $singleton = null;
6935 if (is_null($singleton)) {
6936 $singleton = new emoticon_manager();
6939 return $singleton;
6943 * Provides core support for plugins that have to deal with emoticons (like HTML editor or emoticon filter).
6945 * Whenever this manager mentiones 'emoticon object', the following data
6946 * structure is expected: stdClass with properties text, imagename, imagecomponent,
6947 * altidentifier and altcomponent
6949 * @see admin_setting_emoticons
6951 * @copyright 2010 David Mudrak
6952 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6954 class emoticon_manager {
6957 * Returns the currently enabled emoticons
6959 * @return array of emoticon objects
6961 public function get_emoticons() {
6962 global $CFG;
6964 if (empty($CFG->emoticons)) {
6965 return array();
6968 $emoticons = $this->decode_stored_config($CFG->emoticons);
6970 if (!is_array($emoticons)) {
6971 // Something is wrong with the format of stored setting.
6972 debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL);
6973 return array();
6976 return $emoticons;
6980 * Converts emoticon object into renderable pix_emoticon object
6982 * @param stdClass $emoticon emoticon object
6983 * @param array $attributes explicit HTML attributes to set
6984 * @return pix_emoticon
6986 public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array()) {
6987 $stringmanager = get_string_manager();
6988 if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) {
6989 $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent);
6990 } else {
6991 $alt = s($emoticon->text);
6993 return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes);
6997 * Encodes the array of emoticon objects into a string storable in config table
6999 * @see self::decode_stored_config()
7000 * @param array $emoticons array of emtocion objects
7001 * @return string
7003 public function encode_stored_config(array $emoticons) {
7004 return json_encode($emoticons);
7008 * Decodes the string into an array of emoticon objects
7010 * @see self::encode_stored_config()
7011 * @param string $encoded
7012 * @return string|null
7014 public function decode_stored_config($encoded) {
7015 $decoded = json_decode($encoded);
7016 if (!is_array($decoded)) {
7017 return null;
7019 return $decoded;
7023 * Returns default set of emoticons supported by Moodle
7025 * @return array of sdtClasses
7027 public function default_emoticons() {
7028 return array(
7029 $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'),
7030 $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'),
7031 $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'),
7032 $this->prepare_emoticon_object(";-)", 's/wink', 'wink'),
7033 $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'),
7034 $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'),
7035 $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'),
7036 $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'),
7037 $this->prepare_emoticon_object("B-)", 's/cool', 'cool'),
7038 $this->prepare_emoticon_object("^-)", 's/approve', 'approve'),
7039 $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'),
7040 $this->prepare_emoticon_object(":o)", 's/clown', 'clown'),
7041 $this->prepare_emoticon_object(":-(", 's/sad', 'sad'),
7042 $this->prepare_emoticon_object(":(", 's/sad', 'sad'),
7043 $this->prepare_emoticon_object("8-.", 's/shy', 'shy'),
7044 $this->prepare_emoticon_object(":-I", 's/blush', 'blush'),
7045 $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'),
7046 $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'),
7047 $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'),
7048 $this->prepare_emoticon_object("8-[", 's/angry', 'angry'),
7049 $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'),
7050 $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'),
7051 $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'),
7052 $this->prepare_emoticon_object("}-]", 's/evil', 'evil'),
7053 $this->prepare_emoticon_object("(h)", 's/heart', 'heart'),
7054 $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'),
7055 $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'),
7056 $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'),
7057 $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'),
7058 $this->prepare_emoticon_object("( )", 's/egg', 'egg'),
7063 * Helper method preparing the stdClass with the emoticon properties
7065 * @param string|array $text or array of strings
7066 * @param string $imagename to be used by {@link pix_emoticon}
7067 * @param string $altidentifier alternative string identifier, null for no alt
7068 * @param string $altcomponent where the alternative string is defined
7069 * @param string $imagecomponent to be used by {@link pix_emoticon}
7070 * @return stdClass
7072 protected function prepare_emoticon_object($text, $imagename, $altidentifier = null,
7073 $altcomponent = 'core_pix', $imagecomponent = 'core') {
7074 return (object)array(
7075 'text' => $text,
7076 'imagename' => $imagename,
7077 'imagecomponent' => $imagecomponent,
7078 'altidentifier' => $altidentifier,
7079 'altcomponent' => $altcomponent,
7084 // ENCRYPTION.
7087 * rc4encrypt
7089 * @param string $data Data to encrypt.
7090 * @return string The now encrypted data.
7092 function rc4encrypt($data) {
7093 return endecrypt(get_site_identifier(), $data, '');
7097 * rc4decrypt
7099 * @param string $data Data to decrypt.
7100 * @return string The now decrypted data.
7102 function rc4decrypt($data) {
7103 return endecrypt(get_site_identifier(), $data, 'de');
7107 * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com]
7109 * @todo Finish documenting this function
7111 * @param string $pwd The password to use when encrypting or decrypting
7112 * @param string $data The data to be decrypted/encrypted
7113 * @param string $case Either 'de' for decrypt or '' for encrypt
7114 * @return string
7116 function endecrypt ($pwd, $data, $case) {
7118 if ($case == 'de') {
7119 $data = urldecode($data);
7122 $key[] = '';
7123 $box[] = '';
7124 $pwdlength = strlen($pwd);
7126 for ($i = 0; $i <= 255; $i++) {
7127 $key[$i] = ord(substr($pwd, ($i % $pwdlength), 1));
7128 $box[$i] = $i;
7131 $x = 0;
7133 for ($i = 0; $i <= 255; $i++) {
7134 $x = ($x + $box[$i] + $key[$i]) % 256;
7135 $tempswap = $box[$i];
7136 $box[$i] = $box[$x];
7137 $box[$x] = $tempswap;
7140 $cipher = '';
7142 $a = 0;
7143 $j = 0;
7145 for ($i = 0; $i < strlen($data); $i++) {
7146 $a = ($a + 1) % 256;
7147 $j = ($j + $box[$a]) % 256;
7148 $temp = $box[$a];
7149 $box[$a] = $box[$j];
7150 $box[$j] = $temp;
7151 $k = $box[(($box[$a] + $box[$j]) % 256)];
7152 $cipherby = ord(substr($data, $i, 1)) ^ $k;
7153 $cipher .= chr($cipherby);
7156 if ($case == 'de') {
7157 $cipher = urldecode(urlencode($cipher));
7158 } else {
7159 $cipher = urlencode($cipher);
7162 return $cipher;
7165 // ENVIRONMENT CHECKING.
7168 * This method validates a plug name. It is much faster than calling clean_param.
7170 * @param string $name a string that might be a plugin name.
7171 * @return bool if this string is a valid plugin name.
7173 function is_valid_plugin_name($name) {
7174 // This does not work for 'mod', bad luck, use any other type.
7175 return core_component::is_valid_plugin_name('tool', $name);
7179 * Get a list of all the plugins of a given type that define a certain API function
7180 * in a certain file. The plugin component names and function names are returned.
7182 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
7183 * @param string $function the part of the name of the function after the
7184 * frankenstyle prefix. e.g 'hook' if you are looking for functions with
7185 * names like report_courselist_hook.
7186 * @param string $file the name of file within the plugin that defines the
7187 * function. Defaults to lib.php.
7188 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
7189 * and the function names as values (e.g. 'report_courselist_hook', 'forum_hook').
7191 function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php') {
7192 $pluginfunctions = array();
7193 $pluginswithfile = core_component::get_plugin_list_with_file($plugintype, $file, true);
7194 foreach ($pluginswithfile as $plugin => $notused) {
7195 $fullfunction = $plugintype . '_' . $plugin . '_' . $function;
7197 if (function_exists($fullfunction)) {
7198 // Function exists with standard name. Store, indexed by frankenstyle name of plugin.
7199 $pluginfunctions[$plugintype . '_' . $plugin] = $fullfunction;
7201 } else if ($plugintype === 'mod') {
7202 // For modules, we also allow plugin without full frankenstyle but just starting with the module name.
7203 $shortfunction = $plugin . '_' . $function;
7204 if (function_exists($shortfunction)) {
7205 $pluginfunctions[$plugintype . '_' . $plugin] = $shortfunction;
7209 return $pluginfunctions;
7213 * Lists plugin-like directories within specified directory
7215 * This function was originally used for standard Moodle plugins, please use
7216 * new core_component::get_plugin_list() now.
7218 * This function is used for general directory listing and backwards compatility.
7220 * @param string $directory relative directory from root
7221 * @param string $exclude dir name to exclude from the list (defaults to none)
7222 * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot)
7223 * @return array Sorted array of directory names found under the requested parameters
7225 function get_list_of_plugins($directory='mod', $exclude='', $basedir='') {
7226 global $CFG;
7228 $plugins = array();
7230 if (empty($basedir)) {
7231 $basedir = $CFG->dirroot .'/'. $directory;
7233 } else {
7234 $basedir = $basedir .'/'. $directory;
7237 if ($CFG->debugdeveloper and empty($exclude)) {
7238 // Make sure devs do not use this to list normal plugins,
7239 // this is intended for general directories that are not plugins!
7241 $subtypes = core_component::get_plugin_types();
7242 if (in_array($basedir, $subtypes)) {
7243 debugging('get_list_of_plugins() should not be used to list real plugins, use core_component::get_plugin_list() instead!', DEBUG_DEVELOPER);
7245 unset($subtypes);
7248 if (file_exists($basedir) && filetype($basedir) == 'dir') {
7249 if (!$dirhandle = opendir($basedir)) {
7250 debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER);
7251 return array();
7253 while (false !== ($dir = readdir($dirhandle))) {
7254 // Func: strpos is marginally but reliably faster than substr($dir, 0, 1).
7255 if (strpos($dir, '.') === 0 or $dir === 'CVS' or $dir === '_vti_cnf' or $dir === 'simpletest' or $dir === 'yui' or
7256 $dir === 'tests' or $dir === 'classes' or $dir === $exclude) {
7257 continue;
7259 if (filetype($basedir .'/'. $dir) != 'dir') {
7260 continue;
7262 $plugins[] = $dir;
7264 closedir($dirhandle);
7266 if ($plugins) {
7267 asort($plugins);
7269 return $plugins;
7273 * Invoke plugin's callback functions
7275 * @param string $type plugin type e.g. 'mod'
7276 * @param string $name plugin name
7277 * @param string $feature feature name
7278 * @param string $action feature's action
7279 * @param array $params parameters of callback function, should be an array
7280 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
7281 * @return mixed
7283 * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743
7285 function plugin_callback($type, $name, $feature, $action, $params = null, $default = null) {
7286 return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default);
7290 * Invoke component's callback functions
7292 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
7293 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
7294 * @param array $params parameters of callback function
7295 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
7296 * @return mixed
7297 * @throws coding_exception
7299 function component_callback($component, $function, array $params = array(), $default = null) {
7300 global $CFG; // This is needed for require_once() below.
7302 $cleancomponent = clean_param($component, PARAM_COMPONENT);
7303 if (empty($cleancomponent)) {
7304 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
7306 $component = $cleancomponent;
7308 list($type, $name) = core_component::normalize_component($component);
7309 $component = $type . '_' . $name;
7311 $oldfunction = $name.'_'.$function;
7312 $function = $component.'_'.$function;
7314 $dir = core_component::get_component_directory($component);
7315 if (empty($dir)) {
7316 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
7319 // Load library and look for function.
7320 if (file_exists($dir.'/lib.php')) {
7321 require_once($dir.'/lib.php');
7324 if (!function_exists($function) and function_exists($oldfunction)) {
7325 if ($type !== 'mod' and $type !== 'core') {
7326 debugging("Please use new function name $function instead of legacy $oldfunction");
7328 $function = $oldfunction;
7331 if (function_exists($function)) {
7332 // Function exists, so just return function result.
7333 $ret = call_user_func_array($function, $params);
7334 if (is_null($ret)) {
7335 return $default;
7336 } else {
7337 return $ret;
7340 return $default;
7344 * Checks whether a plugin supports a specified feature.
7346 * @param string $type Plugin type e.g. 'mod'
7347 * @param string $name Plugin name e.g. 'forum'
7348 * @param string $feature Feature code (FEATURE_xx constant)
7349 * @param mixed $default default value if feature support unknown
7350 * @return mixed Feature result (false if not supported, null if feature is unknown,
7351 * otherwise usually true but may have other feature-specific value such as array)
7352 * @throws coding_exception
7354 function plugin_supports($type, $name, $feature, $default = null) {
7355 global $CFG;
7357 if ($type === 'mod' and $name === 'NEWMODULE') {
7358 // Somebody forgot to rename the module template.
7359 return false;
7362 $component = clean_param($type . '_' . $name, PARAM_COMPONENT);
7363 if (empty($component)) {
7364 throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name);
7367 $function = null;
7369 if ($type === 'mod') {
7370 // We need this special case because we support subplugins in modules,
7371 // otherwise it would end up in infinite loop.
7372 if (file_exists("$CFG->dirroot/mod/$name/lib.php")) {
7373 include_once("$CFG->dirroot/mod/$name/lib.php");
7374 $function = $component.'_supports';
7375 if (!function_exists($function)) {
7376 // Legacy non-frankenstyle function name.
7377 $function = $name.'_supports';
7381 } else {
7382 if (!$path = core_component::get_plugin_directory($type, $name)) {
7383 // Non existent plugin type.
7384 return false;
7386 if (file_exists("$path/lib.php")) {
7387 include_once("$path/lib.php");
7388 $function = $component.'_supports';
7392 if ($function and function_exists($function)) {
7393 $supports = $function($feature);
7394 if (is_null($supports)) {
7395 // Plugin does not know - use default.
7396 return $default;
7397 } else {
7398 return $supports;
7402 // Plugin does not care, so use default.
7403 return $default;
7407 * Returns true if the current version of PHP is greater that the specified one.
7409 * @todo Check PHP version being required here is it too low?
7411 * @param string $version The version of php being tested.
7412 * @return bool
7414 function check_php_version($version='5.2.4') {
7415 return (version_compare(phpversion(), $version) >= 0);
7419 * Determine if moodle installation requires update.
7421 * Checks version numbers of main code and all plugins to see
7422 * if there are any mismatches.
7424 * @return bool
7426 function moodle_needs_upgrading() {
7427 global $CFG;
7429 if (empty($CFG->version)) {
7430 return true;
7433 // There is no need to purge plugininfo caches here because
7434 // these caches are not used during upgrade and they are purged after
7435 // every upgrade.
7437 if (empty($CFG->allversionshash)) {
7438 return true;
7441 $hash = core_component::get_all_versions_hash();
7443 return ($hash !== $CFG->allversionshash);
7447 * Returns the major version of this site
7449 * Moodle version numbers consist of three numbers separated by a dot, for
7450 * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so
7451 * called major version. This function extracts the major version from either
7452 * $CFG->release (default) or eventually from the $release variable defined in
7453 * the main version.php.
7455 * @param bool $fromdisk should the version if source code files be used
7456 * @return string|false the major version like '2.3', false if could not be determined
7458 function moodle_major_version($fromdisk = false) {
7459 global $CFG;
7461 if ($fromdisk) {
7462 $release = null;
7463 require($CFG->dirroot.'/version.php');
7464 if (empty($release)) {
7465 return false;
7468 } else {
7469 if (empty($CFG->release)) {
7470 return false;
7472 $release = $CFG->release;
7475 if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) {
7476 return $matches[0];
7477 } else {
7478 return false;
7482 // MISCELLANEOUS.
7485 * Sets the system locale
7487 * @category string
7488 * @param string $locale Can be used to force a locale
7490 function moodle_setlocale($locale='') {
7491 global $CFG;
7493 static $currentlocale = ''; // Last locale caching.
7495 $oldlocale = $currentlocale;
7497 // Fetch the correct locale based on ostype.
7498 if ($CFG->ostype == 'WINDOWS') {
7499 $stringtofetch = 'localewin';
7500 } else {
7501 $stringtofetch = 'locale';
7504 // The priority is the same as in get_string() - parameter, config, course, session, user, global language.
7505 if (!empty($locale)) {
7506 $currentlocale = $locale;
7507 } else if (!empty($CFG->locale)) { // Override locale for all language packs.
7508 $currentlocale = $CFG->locale;
7509 } else {
7510 $currentlocale = get_string($stringtofetch, 'langconfig');
7513 // Do nothing if locale already set up.
7514 if ($oldlocale == $currentlocale) {
7515 return;
7518 // Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values,
7519 // set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7
7520 // Some day, numeric, monetary and other categories should be set too, I think. :-/.
7522 // Get current values.
7523 $monetary= setlocale (LC_MONETARY, 0);
7524 $numeric = setlocale (LC_NUMERIC, 0);
7525 $ctype = setlocale (LC_CTYPE, 0);
7526 if ($CFG->ostype != 'WINDOWS') {
7527 $messages= setlocale (LC_MESSAGES, 0);
7529 // Set locale to all.
7530 setlocale (LC_ALL, $currentlocale);
7531 // Set old values.
7532 setlocale (LC_MONETARY, $monetary);
7533 setlocale (LC_NUMERIC, $numeric);
7534 if ($CFG->ostype != 'WINDOWS') {
7535 setlocale (LC_MESSAGES, $messages);
7537 if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') {
7538 // To workaround a well-known PHP problem with Turkish letter Ii.
7539 setlocale (LC_CTYPE, $ctype);
7544 * Count words in a string.
7546 * Words are defined as things between whitespace.
7548 * @category string
7549 * @param string $string The text to be searched for words.
7550 * @return int The count of words in the specified string
7552 function count_words($string) {
7553 $string = strip_tags($string);
7554 return count(preg_split("/\w\b/", $string)) - 1;
7558 * Count letters in a string.
7560 * Letters are defined as chars not in tags and different from whitespace.
7562 * @category string
7563 * @param string $string The text to be searched for letters.
7564 * @return int The count of letters in the specified text.
7566 function count_letters($string) {
7567 $string = strip_tags($string); // Tags are out now.
7568 $string = preg_replace('/[[:space:]]*/', '', $string); // Whitespace are out now.
7570 return core_text::strlen($string);
7574 * Generate and return a random string of the specified length.
7576 * @param int $length The length of the string to be created.
7577 * @return string
7579 function random_string ($length=15) {
7580 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
7581 $pool .= 'abcdefghijklmnopqrstuvwxyz';
7582 $pool .= '0123456789';
7583 $poollen = strlen($pool);
7584 mt_srand ((double) microtime() * 1000000);
7585 $string = '';
7586 for ($i = 0; $i < $length; $i++) {
7587 $string .= substr($pool, (mt_rand()%($poollen)), 1);
7589 return $string;
7593 * Generate a complex random string (useful for md5 salts)
7595 * This function is based on the above {@link random_string()} however it uses a
7596 * larger pool of characters and generates a string between 24 and 32 characters
7598 * @param int $length Optional if set generates a string to exactly this length
7599 * @return string
7601 function complex_random_string($length=null) {
7602 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
7603 $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
7604 $poollen = strlen($pool);
7605 mt_srand ((double) microtime() * 1000000);
7606 if ($length===null) {
7607 $length = floor(rand(24, 32));
7609 $string = '';
7610 for ($i = 0; $i < $length; $i++) {
7611 $string .= $pool[(mt_rand()%$poollen)];
7613 return $string;
7617 * Given some text (which may contain HTML) and an ideal length,
7618 * this function truncates the text neatly on a word boundary if possible
7620 * @category string
7621 * @param string $text text to be shortened
7622 * @param int $ideal ideal string length
7623 * @param boolean $exact if false, $text will not be cut mid-word
7624 * @param string $ending The string to append if the passed string is truncated
7625 * @return string $truncate shortened string
7627 function shorten_text($text, $ideal=30, $exact = false, $ending='...') {
7628 // If the plain text is shorter than the maximum length, return the whole text.
7629 if (core_text::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) {
7630 return $text;
7633 // Splits on HTML tags. Each open/close/empty tag will be the first thing
7634 // and only tag in its 'line'.
7635 preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
7637 $totallength = core_text::strlen($ending);
7638 $truncate = '';
7640 // This array stores information about open and close tags and their position
7641 // in the truncated string. Each item in the array is an object with fields
7642 // ->open (true if open), ->tag (tag name in lower case), and ->pos
7643 // (byte position in truncated text).
7644 $tagdetails = array();
7646 foreach ($lines as $linematchings) {
7647 // If there is any html-tag in this line, handle it and add it (uncounted) to the output.
7648 if (!empty($linematchings[1])) {
7649 // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>).
7650 if (!preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $linematchings[1])) {
7651 if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $linematchings[1], $tagmatchings)) {
7652 // Record closing tag.
7653 $tagdetails[] = (object) array(
7654 'open' => false,
7655 'tag' => core_text::strtolower($tagmatchings[1]),
7656 'pos' => core_text::strlen($truncate),
7659 } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $linematchings[1], $tagmatchings)) {
7660 // Record opening tag.
7661 $tagdetails[] = (object) array(
7662 'open' => true,
7663 'tag' => core_text::strtolower($tagmatchings[1]),
7664 'pos' => core_text::strlen($truncate),
7668 // Add html-tag to $truncate'd text.
7669 $truncate .= $linematchings[1];
7672 // Calculate the length of the plain text part of the line; handle entities as one character.
7673 $contentlength = core_text::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $linematchings[2]));
7674 if ($totallength + $contentlength > $ideal) {
7675 // The number of characters which are left.
7676 $left = $ideal - $totallength;
7677 $entitieslength = 0;
7678 // Search for html entities.
7679 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)) {
7680 // Calculate the real length of all entities in the legal range.
7681 foreach ($entities[0] as $entity) {
7682 if ($entity[1]+1-$entitieslength <= $left) {
7683 $left--;
7684 $entitieslength += core_text::strlen($entity[0]);
7685 } else {
7686 // No more characters left.
7687 break;
7691 $breakpos = $left + $entitieslength;
7693 // If the words shouldn't be cut in the middle...
7694 if (!$exact) {
7695 // Search the last occurence of a space.
7696 for (; $breakpos > 0; $breakpos--) {
7697 if ($char = core_text::substr($linematchings[2], $breakpos, 1)) {
7698 if ($char === '.' or $char === ' ') {
7699 $breakpos += 1;
7700 break;
7701 } else if (strlen($char) > 2) {
7702 // Chinese/Japanese/Korean text can be truncated at any UTF-8 character boundary.
7703 $breakpos += 1;
7704 break;
7709 if ($breakpos == 0) {
7710 // This deals with the test_shorten_text_no_spaces case.
7711 $breakpos = $left + $entitieslength;
7712 } else if ($breakpos > $left + $entitieslength) {
7713 // This deals with the previous for loop breaking on the first char.
7714 $breakpos = $left + $entitieslength;
7717 $truncate .= core_text::substr($linematchings[2], 0, $breakpos);
7718 // Maximum length is reached, so get off the loop.
7719 break;
7720 } else {
7721 $truncate .= $linematchings[2];
7722 $totallength += $contentlength;
7725 // If the maximum length is reached, get off the loop.
7726 if ($totallength >= $ideal) {
7727 break;
7731 // Add the defined ending to the text.
7732 $truncate .= $ending;
7734 // Now calculate the list of open html tags based on the truncate position.
7735 $opentags = array();
7736 foreach ($tagdetails as $taginfo) {
7737 if ($taginfo->open) {
7738 // Add tag to the beginning of $opentags list.
7739 array_unshift($opentags, $taginfo->tag);
7740 } else {
7741 // Can have multiple exact same open tags, close the last one.
7742 $pos = array_search($taginfo->tag, array_reverse($opentags, true));
7743 if ($pos !== false) {
7744 unset($opentags[$pos]);
7749 // Close all unclosed html-tags.
7750 foreach ($opentags as $tag) {
7751 $truncate .= '</' . $tag . '>';
7754 return $truncate;
7759 * Given dates in seconds, how many weeks is the date from startdate
7760 * The first week is 1, the second 2 etc ...
7762 * @param int $startdate Timestamp for the start date
7763 * @param int $thedate Timestamp for the end date
7764 * @return string
7766 function getweek ($startdate, $thedate) {
7767 if ($thedate < $startdate) {
7768 return 0;
7771 return floor(($thedate - $startdate) / WEEKSECS) + 1;
7775 * Returns a randomly generated password of length $maxlen. inspired by
7777 * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and
7778 * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254}
7780 * @param int $maxlen The maximum size of the password being generated.
7781 * @return string
7783 function generate_password($maxlen=10) {
7784 global $CFG;
7786 if (empty($CFG->passwordpolicy)) {
7787 $fillers = PASSWORD_DIGITS;
7788 $wordlist = file($CFG->wordlist);
7789 $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]);
7790 $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]);
7791 $filler1 = $fillers[rand(0, strlen($fillers) - 1)];
7792 $password = $word1 . $filler1 . $word2;
7793 } else {
7794 $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0;
7795 $digits = $CFG->minpassworddigits;
7796 $lower = $CFG->minpasswordlower;
7797 $upper = $CFG->minpasswordupper;
7798 $nonalphanum = $CFG->minpasswordnonalphanum;
7799 $total = $lower + $upper + $digits + $nonalphanum;
7800 // Var minlength should be the greater one of the two ( $minlen and $total ).
7801 $minlen = $minlen < $total ? $total : $minlen;
7802 // Var maxlen can never be smaller than minlen.
7803 $maxlen = $minlen > $maxlen ? $minlen : $maxlen;
7804 $additional = $maxlen - $total;
7806 // Make sure we have enough characters to fulfill
7807 // complexity requirements.
7808 $passworddigits = PASSWORD_DIGITS;
7809 while ($digits > strlen($passworddigits)) {
7810 $passworddigits .= PASSWORD_DIGITS;
7812 $passwordlower = PASSWORD_LOWER;
7813 while ($lower > strlen($passwordlower)) {
7814 $passwordlower .= PASSWORD_LOWER;
7816 $passwordupper = PASSWORD_UPPER;
7817 while ($upper > strlen($passwordupper)) {
7818 $passwordupper .= PASSWORD_UPPER;
7820 $passwordnonalphanum = PASSWORD_NONALPHANUM;
7821 while ($nonalphanum > strlen($passwordnonalphanum)) {
7822 $passwordnonalphanum .= PASSWORD_NONALPHANUM;
7825 // Now mix and shuffle it all.
7826 $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) .
7827 substr(str_shuffle ($passwordupper), 0, $upper) .
7828 substr(str_shuffle ($passworddigits), 0, $digits) .
7829 substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) .
7830 substr(str_shuffle ($passwordlower .
7831 $passwordupper .
7832 $passworddigits .
7833 $passwordnonalphanum), 0 , $additional));
7836 return substr ($password, 0, $maxlen);
7840 * Given a float, prints it nicely.
7841 * Localized floats must not be used in calculations!
7843 * The stripzeros feature is intended for making numbers look nicer in small
7844 * areas where it is not necessary to indicate the degree of accuracy by showing
7845 * ending zeros. If you turn it on with $decimalpoints set to 3, for example,
7846 * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'.
7848 * @param float $float The float to print
7849 * @param int $decimalpoints The number of decimal places to print.
7850 * @param bool $localized use localized decimal separator
7851 * @param bool $stripzeros If true, removes final zeros after decimal point
7852 * @return string locale float
7854 function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=false) {
7855 if (is_null($float)) {
7856 return '';
7858 if ($localized) {
7859 $separator = get_string('decsep', 'langconfig');
7860 } else {
7861 $separator = '.';
7863 $result = number_format($float, $decimalpoints, $separator, '');
7864 if ($stripzeros) {
7865 // Remove zeros and final dot if not needed.
7866 $result = preg_replace('~(' . preg_quote($separator) . ')?0+$~', '', $result);
7868 return $result;
7872 * Converts locale specific floating point/comma number back to standard PHP float value
7873 * Do NOT try to do any math operations before this conversion on any user submitted floats!
7875 * @param string $localefloat locale aware float representation
7876 * @param bool $strict If true, then check the input and return false if it is not a valid number.
7877 * @return mixed float|bool - false or the parsed float.
7879 function unformat_float($localefloat, $strict = false) {
7880 $localefloat = trim($localefloat);
7882 if ($localefloat == '') {
7883 return null;
7886 $localefloat = str_replace(' ', '', $localefloat); // No spaces - those might be used as thousand separators.
7887 $localefloat = str_replace(get_string('decsep', 'langconfig'), '.', $localefloat);
7889 if ($strict && !is_numeric($localefloat)) {
7890 return false;
7893 return (float)$localefloat;
7897 * Given a simple array, this shuffles it up just like shuffle()
7898 * Unlike PHP's shuffle() this function works on any machine.
7900 * @param array $array The array to be rearranged
7901 * @return array
7903 function swapshuffle($array) {
7905 srand ((double) microtime() * 10000000);
7906 $last = count($array) - 1;
7907 for ($i = 0; $i <= $last; $i++) {
7908 $from = rand(0, $last);
7909 $curr = $array[$i];
7910 $array[$i] = $array[$from];
7911 $array[$from] = $curr;
7913 return $array;
7917 * Like {@link swapshuffle()}, but works on associative arrays
7919 * @param array $array The associative array to be rearranged
7920 * @return array
7922 function swapshuffle_assoc($array) {
7924 $newarray = array();
7925 $newkeys = swapshuffle(array_keys($array));
7927 foreach ($newkeys as $newkey) {
7928 $newarray[$newkey] = $array[$newkey];
7930 return $newarray;
7934 * Given an arbitrary array, and a number of draws,
7935 * this function returns an array with that amount
7936 * of items. The indexes are retained.
7938 * @todo Finish documenting this function
7940 * @param array $array
7941 * @param int $draws
7942 * @return array
7944 function draw_rand_array($array, $draws) {
7945 srand ((double) microtime() * 10000000);
7947 $return = array();
7949 $last = count($array);
7951 if ($draws > $last) {
7952 $draws = $last;
7955 while ($draws > 0) {
7956 $last--;
7958 $keys = array_keys($array);
7959 $rand = rand(0, $last);
7961 $return[$keys[$rand]] = $array[$keys[$rand]];
7962 unset($array[$keys[$rand]]);
7964 $draws--;
7967 return $return;
7971 * Calculate the difference between two microtimes
7973 * @param string $a The first Microtime
7974 * @param string $b The second Microtime
7975 * @return string
7977 function microtime_diff($a, $b) {
7978 list($adec, $asec) = explode(' ', $a);
7979 list($bdec, $bsec) = explode(' ', $b);
7980 return $bsec - $asec + $bdec - $adec;
7984 * Given a list (eg a,b,c,d,e) this function returns
7985 * an array of 1->a, 2->b, 3->c etc
7987 * @param string $list The string to explode into array bits
7988 * @param string $separator The separator used within the list string
7989 * @return array The now assembled array
7991 function make_menu_from_list($list, $separator=',') {
7993 $array = array_reverse(explode($separator, $list), true);
7994 foreach ($array as $key => $item) {
7995 $outarray[$key+1] = trim($item);
7997 return $outarray;
8001 * Creates an array that represents all the current grades that
8002 * can be chosen using the given grading type.
8004 * Negative numbers
8005 * are scales, zero is no grade, and positive numbers are maximum
8006 * grades.
8008 * @todo Finish documenting this function or better deprecated this completely!
8010 * @param int $gradingtype
8011 * @return array
8013 function make_grades_menu($gradingtype) {
8014 global $DB;
8016 $grades = array();
8017 if ($gradingtype < 0) {
8018 if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) {
8019 return make_menu_from_list($scale->scale);
8021 } else if ($gradingtype > 0) {
8022 for ($i=$gradingtype; $i>=0; $i--) {
8023 $grades[$i] = $i .' / '. $gradingtype;
8025 return $grades;
8027 return $grades;
8031 * This function returns the number of activities using the given scale in the given course.
8033 * @param int $courseid The course ID to check.
8034 * @param int $scaleid The scale ID to check
8035 * @return int
8037 function course_scale_used($courseid, $scaleid) {
8038 global $CFG, $DB;
8040 $return = 0;
8042 if (!empty($scaleid)) {
8043 if ($cms = get_course_mods($courseid)) {
8044 foreach ($cms as $cm) {
8045 // Check cm->name/lib.php exists.
8046 if (file_exists($CFG->dirroot.'/mod/'.$cm->modname.'/lib.php')) {
8047 include_once($CFG->dirroot.'/mod/'.$cm->modname.'/lib.php');
8048 $functionname = $cm->modname.'_scale_used';
8049 if (function_exists($functionname)) {
8050 if ($functionname($cm->instance, $scaleid)) {
8051 $return++;
8058 // Check if any course grade item makes use of the scale.
8059 $return += $DB->count_records('grade_items', array('courseid' => $courseid, 'scaleid' => $scaleid));
8061 // Check if any outcome in the course makes use of the scale.
8062 $return += $DB->count_records_sql("SELECT COUNT('x')
8063 FROM {grade_outcomes_courses} goc,
8064 {grade_outcomes} go
8065 WHERE go.id = goc.outcomeid
8066 AND go.scaleid = ? AND goc.courseid = ?",
8067 array($scaleid, $courseid));
8069 return $return;
8073 * This function returns the number of activities using scaleid in the entire site
8075 * @param int $scaleid
8076 * @param array $courses
8077 * @return int
8079 function site_scale_used($scaleid, &$courses) {
8080 $return = 0;
8082 if (!is_array($courses) || count($courses) == 0) {
8083 $courses = get_courses("all", false, "c.id, c.shortname");
8086 if (!empty($scaleid)) {
8087 if (is_array($courses) && count($courses) > 0) {
8088 foreach ($courses as $course) {
8089 $return += course_scale_used($course->id, $scaleid);
8093 return $return;
8097 * make_unique_id_code
8099 * @todo Finish documenting this function
8101 * @uses $_SERVER
8102 * @param string $extra Extra string to append to the end of the code
8103 * @return string
8105 function make_unique_id_code($extra = '') {
8107 $hostname = 'unknownhost';
8108 if (!empty($_SERVER['HTTP_HOST'])) {
8109 $hostname = $_SERVER['HTTP_HOST'];
8110 } else if (!empty($_ENV['HTTP_HOST'])) {
8111 $hostname = $_ENV['HTTP_HOST'];
8112 } else if (!empty($_SERVER['SERVER_NAME'])) {
8113 $hostname = $_SERVER['SERVER_NAME'];
8114 } else if (!empty($_ENV['SERVER_NAME'])) {
8115 $hostname = $_ENV['SERVER_NAME'];
8118 $date = gmdate("ymdHis");
8120 $random = random_string(6);
8122 if ($extra) {
8123 return $hostname .'+'. $date .'+'. $random .'+'. $extra;
8124 } else {
8125 return $hostname .'+'. $date .'+'. $random;
8131 * Function to check the passed address is within the passed subnet
8133 * The parameter is a comma separated string of subnet definitions.
8134 * Subnet strings can be in one of three formats:
8135 * 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn (number of bits in net mask)
8136 * 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)
8137 * 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. (incomplete address, a bit non-technical ;-)
8138 * Code for type 1 modified from user posted comments by mediator at
8139 * {@link http://au.php.net/manual/en/function.ip2long.php}
8141 * @param string $addr The address you are checking
8142 * @param string $subnetstr The string of subnet addresses
8143 * @return bool
8145 function address_in_subnet($addr, $subnetstr) {
8147 if ($addr == '0.0.0.0') {
8148 return false;
8150 $subnets = explode(',', $subnetstr);
8151 $found = false;
8152 $addr = trim($addr);
8153 $addr = cleanremoteaddr($addr, false); // Normalise.
8154 if ($addr === null) {
8155 return false;
8157 $addrparts = explode(':', $addr);
8159 $ipv6 = strpos($addr, ':');
8161 foreach ($subnets as $subnet) {
8162 $subnet = trim($subnet);
8163 if ($subnet === '') {
8164 continue;
8167 if (strpos($subnet, '/') !== false) {
8168 // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn.
8169 list($ip, $mask) = explode('/', $subnet);
8170 $mask = trim($mask);
8171 if (!is_number($mask)) {
8172 continue; // Incorect mask number, eh?
8174 $ip = cleanremoteaddr($ip, false); // Normalise.
8175 if ($ip === null) {
8176 continue;
8178 if (strpos($ip, ':') !== false) {
8179 // IPv6.
8180 if (!$ipv6) {
8181 continue;
8183 if ($mask > 128 or $mask < 0) {
8184 continue; // Nonsense.
8186 if ($mask == 0) {
8187 return true; // Any address.
8189 if ($mask == 128) {
8190 if ($ip === $addr) {
8191 return true;
8193 continue;
8195 $ipparts = explode(':', $ip);
8196 $modulo = $mask % 16;
8197 $ipnet = array_slice($ipparts, 0, ($mask-$modulo)/16);
8198 $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16);
8199 if (implode(':', $ipnet) === implode(':', $addrnet)) {
8200 if ($modulo == 0) {
8201 return true;
8203 $pos = ($mask-$modulo)/16;
8204 $ipnet = hexdec($ipparts[$pos]);
8205 $addrnet = hexdec($addrparts[$pos]);
8206 $mask = 0xffff << (16 - $modulo);
8207 if (($addrnet & $mask) == ($ipnet & $mask)) {
8208 return true;
8212 } else {
8213 // IPv4.
8214 if ($ipv6) {
8215 continue;
8217 if ($mask > 32 or $mask < 0) {
8218 continue; // Nonsense.
8220 if ($mask == 0) {
8221 return true;
8223 if ($mask == 32) {
8224 if ($ip === $addr) {
8225 return true;
8227 continue;
8229 $mask = 0xffffffff << (32 - $mask);
8230 if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
8231 return true;
8235 } else if (strpos($subnet, '-') !== false) {
8236 // 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.
8237 $parts = explode('-', $subnet);
8238 if (count($parts) != 2) {
8239 continue;
8242 if (strpos($subnet, ':') !== false) {
8243 // IPv6.
8244 if (!$ipv6) {
8245 continue;
8247 $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
8248 if ($ipstart === null) {
8249 continue;
8251 $ipparts = explode(':', $ipstart);
8252 $start = hexdec(array_pop($ipparts));
8253 $ipparts[] = trim($parts[1]);
8254 $ipend = cleanremoteaddr(implode(':', $ipparts), false); // Normalise.
8255 if ($ipend === null) {
8256 continue;
8258 $ipparts[7] = '';
8259 $ipnet = implode(':', $ipparts);
8260 if (strpos($addr, $ipnet) !== 0) {
8261 continue;
8263 $ipparts = explode(':', $ipend);
8264 $end = hexdec($ipparts[7]);
8266 $addrend = hexdec($addrparts[7]);
8268 if (($addrend >= $start) and ($addrend <= $end)) {
8269 return true;
8272 } else {
8273 // IPv4.
8274 if ($ipv6) {
8275 continue;
8277 $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
8278 if ($ipstart === null) {
8279 continue;
8281 $ipparts = explode('.', $ipstart);
8282 $ipparts[3] = trim($parts[1]);
8283 $ipend = cleanremoteaddr(implode('.', $ipparts), false); // Normalise.
8284 if ($ipend === null) {
8285 continue;
8288 if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
8289 return true;
8293 } else {
8294 // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
8295 if (strpos($subnet, ':') !== false) {
8296 // IPv6.
8297 if (!$ipv6) {
8298 continue;
8300 $parts = explode(':', $subnet);
8301 $count = count($parts);
8302 if ($parts[$count-1] === '') {
8303 unset($parts[$count-1]); // Trim trailing :'s.
8304 $count--;
8305 $subnet = implode('.', $parts);
8307 $isip = cleanremoteaddr($subnet, false); // Normalise.
8308 if ($isip !== null) {
8309 if ($isip === $addr) {
8310 return true;
8312 continue;
8313 } else if ($count > 8) {
8314 continue;
8316 $zeros = array_fill(0, 8-$count, '0');
8317 $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16);
8318 if (address_in_subnet($addr, $subnet)) {
8319 return true;
8322 } else {
8323 // IPv4.
8324 if ($ipv6) {
8325 continue;
8327 $parts = explode('.', $subnet);
8328 $count = count($parts);
8329 if ($parts[$count-1] === '') {
8330 unset($parts[$count-1]); // Trim trailing .
8331 $count--;
8332 $subnet = implode('.', $parts);
8334 if ($count == 4) {
8335 $subnet = cleanremoteaddr($subnet, false); // Normalise.
8336 if ($subnet === $addr) {
8337 return true;
8339 continue;
8340 } else if ($count > 4) {
8341 continue;
8343 $zeros = array_fill(0, 4-$count, '0');
8344 $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8);
8345 if (address_in_subnet($addr, $subnet)) {
8346 return true;
8352 return false;
8356 * For outputting debugging info
8358 * @param string $string The string to write
8359 * @param string $eol The end of line char(s) to use
8360 * @param string $sleep Period to make the application sleep
8361 * This ensures any messages have time to display before redirect
8363 function mtrace($string, $eol="\n", $sleep=0) {
8365 if (defined('STDOUT') and !PHPUNIT_TEST) {
8366 fwrite(STDOUT, $string.$eol);
8367 } else {
8368 echo $string . $eol;
8371 flush();
8373 // Delay to keep message on user's screen in case of subsequent redirect.
8374 if ($sleep) {
8375 sleep($sleep);
8380 * Replace 1 or more slashes or backslashes to 1 slash
8382 * @param string $path The path to strip
8383 * @return string the path with double slashes removed
8385 function cleardoubleslashes ($path) {
8386 return preg_replace('/(\/|\\\){1,}/', '/', $path);
8390 * Is current ip in give list?
8392 * @param string $list
8393 * @return bool
8395 function remoteip_in_list($list) {
8396 $inlist = false;
8397 $clientip = getremoteaddr(null);
8399 if (!$clientip) {
8400 // Ensure access on cli.
8401 return true;
8404 $list = explode("\n", $list);
8405 foreach ($list as $subnet) {
8406 $subnet = trim($subnet);
8407 if (address_in_subnet($clientip, $subnet)) {
8408 $inlist = true;
8409 break;
8412 return $inlist;
8416 * Returns most reliable client address
8418 * @param string $default If an address can't be determined, then return this
8419 * @return string The remote IP address
8421 function getremoteaddr($default='0.0.0.0') {
8422 global $CFG;
8424 if (empty($CFG->getremoteaddrconf)) {
8425 // This will happen, for example, before just after the upgrade, as the
8426 // user is redirected to the admin screen.
8427 $variablestoskip = 0;
8428 } else {
8429 $variablestoskip = $CFG->getremoteaddrconf;
8431 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) {
8432 if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
8433 $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']);
8434 return $address ? $address : $default;
8437 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
8438 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
8439 $address = cleanremoteaddr($_SERVER['HTTP_X_FORWARDED_FOR']);
8440 return $address ? $address : $default;
8443 if (!empty($_SERVER['REMOTE_ADDR'])) {
8444 $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']);
8445 return $address ? $address : $default;
8446 } else {
8447 return $default;
8452 * Cleans an ip address. Internal addresses are now allowed.
8453 * (Originally local addresses were not allowed.)
8455 * @param string $addr IPv4 or IPv6 address
8456 * @param bool $compress use IPv6 address compression
8457 * @return string normalised ip address string, null if error
8459 function cleanremoteaddr($addr, $compress=false) {
8460 $addr = trim($addr);
8462 // TODO: maybe add a separate function is_addr_public() or something like this.
8464 if (strpos($addr, ':') !== false) {
8465 // Can be only IPv6.
8466 $parts = explode(':', $addr);
8467 $count = count($parts);
8469 if (strpos($parts[$count-1], '.') !== false) {
8470 // Legacy ipv4 notation.
8471 $last = array_pop($parts);
8472 $ipv4 = cleanremoteaddr($last, true);
8473 if ($ipv4 === null) {
8474 return null;
8476 $bits = explode('.', $ipv4);
8477 $parts[] = dechex($bits[0]).dechex($bits[1]);
8478 $parts[] = dechex($bits[2]).dechex($bits[3]);
8479 $count = count($parts);
8480 $addr = implode(':', $parts);
8483 if ($count < 3 or $count > 8) {
8484 return null; // Severly malformed.
8487 if ($count != 8) {
8488 if (strpos($addr, '::') === false) {
8489 return null; // Malformed.
8491 // Uncompress.
8492 $insertat = array_search('', $parts, true);
8493 $missing = array_fill(0, 1 + 8 - $count, '0');
8494 array_splice($parts, $insertat, 1, $missing);
8495 foreach ($parts as $key => $part) {
8496 if ($part === '') {
8497 $parts[$key] = '0';
8502 $adr = implode(':', $parts);
8503 if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) {
8504 return null; // Incorrect format - sorry.
8507 // Normalise 0s and case.
8508 $parts = array_map('hexdec', $parts);
8509 $parts = array_map('dechex', $parts);
8511 $result = implode(':', $parts);
8513 if (!$compress) {
8514 return $result;
8517 if ($result === '0:0:0:0:0:0:0:0') {
8518 return '::'; // All addresses.
8521 $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1);
8522 if ($compressed !== $result) {
8523 return $compressed;
8526 $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
8527 if ($compressed !== $result) {
8528 return $compressed;
8531 $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
8532 if ($compressed !== $result) {
8533 return $compressed;
8536 return $result;
8539 // First get all things that look like IPv4 addresses.
8540 $parts = array();
8541 if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
8542 return null;
8544 unset($parts[0]);
8546 foreach ($parts as $key => $match) {
8547 if ($match > 255) {
8548 return null;
8550 $parts[$key] = (int)$match; // Normalise 0s.
8553 return implode('.', $parts);
8557 * This function will make a complete copy of anything it's given,
8558 * regardless of whether it's an object or not.
8560 * @param mixed $thing Something you want cloned
8561 * @return mixed What ever it is you passed it
8563 function fullclone($thing) {
8564 return unserialize(serialize($thing));
8569 * This function expects to called during shutdown should be set via register_shutdown_function() in lib/setup.php .
8571 * @return void
8573 function moodle_request_shutdown() {
8574 global $CFG;
8576 // Help apache server if possible.
8577 $apachereleasemem = false;
8578 if (function_exists('apache_child_terminate') && function_exists('memory_get_usage')
8579 && ini_get_bool('child_terminate')) {
8581 $limit = (empty($CFG->apachemaxmem) ? 64*1024*1024 : $CFG->apachemaxmem); // 64MB default.
8582 if (memory_get_usage() > get_real_size($limit)) {
8583 $apachereleasemem = $limit;
8584 @apache_child_terminate();
8588 // Deal with perf logging.
8589 if (defined('MDL_PERF') || (!empty($CFG->perfdebug) and $CFG->perfdebug > 7)) {
8590 if ($apachereleasemem) {
8591 error_log('Mem usage over '.$apachereleasemem.': marking Apache child for reaping.');
8593 if (defined('MDL_PERFTOLOG')) {
8594 $perf = get_performance_info();
8595 error_log("PERF: " . $perf['txt']);
8597 if (defined('MDL_PERFINC')) {
8598 $inc = get_included_files();
8599 $ts = 0;
8600 foreach ($inc as $f) {
8601 if (preg_match(':^/:', $f)) {
8602 $fs = filesize($f);
8603 $ts += $fs;
8604 $hfs = display_size($fs);
8605 error_log(substr($f, strlen($CFG->dirroot)) . " size: $fs ($hfs)"
8606 , null, null, 0);
8607 } else {
8608 error_log($f , null, null, 0);
8611 if ($ts > 0 ) {
8612 $hts = display_size($ts);
8613 error_log("Total size of files included: $ts ($hts)");
8620 * If new messages are waiting for the current user, then insert
8621 * JavaScript to pop up the messaging window into the page
8623 * @return void
8625 function message_popup_window() {
8626 global $USER, $DB, $PAGE, $CFG;
8628 if (!$PAGE->get_popup_notification_allowed() || empty($CFG->messaging)) {
8629 return;
8632 if (!isloggedin() || isguestuser()) {
8633 return;
8636 if (!isset($USER->message_lastpopup)) {
8637 $USER->message_lastpopup = 0;
8638 } else if ($USER->message_lastpopup > (time()-120)) {
8639 // Don't run the query to check whether to display a popup if its been run in the last 2 minutes.
8640 return;
8643 // A quick query to check whether the user has new messages.
8644 $messagecount = $DB->count_records('message', array('useridto' => $USER->id));
8645 if ($messagecount<1) {
8646 return;
8649 // Got unread messages so now do another query that joins with the user table.
8650 $messagesql = "SELECT m.id, m.smallmessage, m.fullmessageformat, m.notification, u.firstname, u.lastname
8651 FROM {message} m
8652 JOIN {message_working} mw ON m.id=mw.unreadmessageid
8653 JOIN {message_processors} p ON mw.processorid=p.id
8654 JOIN {user} u ON m.useridfrom=u.id
8655 WHERE m.useridto = :userid
8656 AND p.name='popup'";
8658 // If the user was last notified over an hour ago we can re-notify them of old messages
8659 // so don't worry about when the new message was sent.
8660 $lastnotifiedlongago = $USER->message_lastpopup < (time()-3600);
8661 if (!$lastnotifiedlongago) {
8662 $messagesql .= 'AND m.timecreated > :lastpopuptime';
8665 $messageusers = $DB->get_records_sql($messagesql, array('userid' => $USER->id, 'lastpopuptime' => $USER->message_lastpopup));
8667 // If we have new messages to notify the user about.
8668 if (!empty($messageusers)) {
8670 $strmessages = '';
8671 if (count($messageusers)>1) {
8672 $strmessages = get_string('unreadnewmessages', 'message', count($messageusers));
8673 } else {
8674 $messageusers = reset($messageusers);
8676 // Show who the message is from if its not a notification.
8677 if (!$messageusers->notification) {
8678 $strmessages = get_string('unreadnewmessage', 'message', fullname($messageusers) );
8681 // Try to display the small version of the message.
8682 $smallmessage = null;
8683 if (!empty($messageusers->smallmessage)) {
8684 // Display the first 200 chars of the message in the popup.
8685 $smallmessage = null;
8686 if (core_text::strlen($messageusers->smallmessage) > 200) {
8687 $smallmessage = core_text::substr($messageusers->smallmessage, 0, 200).'...';
8688 } else {
8689 $smallmessage = $messageusers->smallmessage;
8692 // Prevent html symbols being displayed.
8693 if ($messageusers->fullmessageformat == FORMAT_HTML) {
8694 $smallmessage = html_to_text($smallmessage);
8695 } else {
8696 $smallmessage = s($smallmessage);
8698 } else if ($messageusers->notification) {
8699 // Its a notification with no smallmessage so just say they have a notification.
8700 $smallmessage = get_string('unreadnewnotification', 'message');
8702 if (!empty($smallmessage)) {
8703 $strmessages .= '<div id="usermessage">'.s($smallmessage).'</div>';
8707 $strgomessage = get_string('gotomessages', 'message');
8708 $strstaymessage = get_string('ignore', 'admin');
8710 $notificationsound = null;
8711 $beep = get_user_preferences('message_beepnewmessage', '');
8712 if (!empty($beep)) {
8713 // Browsers will work down this list until they find something they support.
8714 $sourcetags = html_writer::empty_tag('source', array('src' => $CFG->wwwroot.'/message/bell.wav', 'type' => 'audio/wav'));
8715 $sourcetags .= html_writer::empty_tag('source', array('src' => $CFG->wwwroot.'/message/bell.ogg', 'type' => 'audio/ogg'));
8716 $sourcetags .= html_writer::empty_tag('source', array('src' => $CFG->wwwroot.'/message/bell.mp3', 'type' => 'audio/mpeg'));
8717 $sourcetags .= html_writer::empty_tag('embed', array('src' => $CFG->wwwroot.'/message/bell.wav', 'autostart' => 'true', 'hidden' => 'true'));
8719 $notificationsound = html_writer::tag('audio', $sourcetags, array('preload' => 'auto', 'autoplay' => 'autoplay'));
8722 $url = $CFG->wwwroot.'/message/index.php';
8723 $content = html_writer::start_tag('div', array('id' => 'newmessageoverlay', 'class' => 'mdl-align')).
8724 html_writer::start_tag('div', array('id' => 'newmessagetext')).
8725 $strmessages.
8726 html_writer::end_tag('div').
8728 $notificationsound.
8729 html_writer::start_tag('div', array('id' => 'newmessagelinks')).
8730 html_writer::link($url, $strgomessage, array('id' => 'notificationyes')).'&nbsp;&nbsp;&nbsp;'.
8731 html_writer::link('', $strstaymessage, array('id' => 'notificationno')).
8732 html_writer::end_tag('div');
8733 html_writer::end_tag('div');
8735 $PAGE->requires->js_init_call('M.core_message.init_notification', array('', $content, $url));
8737 $USER->message_lastpopup = time();
8742 * Used to make sure that $min <= $value <= $max
8744 * Make sure that value is between min, and max
8746 * @param int $min The minimum value
8747 * @param int $value The value to check
8748 * @param int $max The maximum value
8749 * @return int
8751 function bounded_number($min, $value, $max) {
8752 if ($value < $min) {
8753 return $min;
8755 if ($value > $max) {
8756 return $max;
8758 return $value;
8762 * Check if there is a nested array within the passed array
8764 * @param array $array
8765 * @return bool true if there is a nested array false otherwise
8767 function array_is_nested($array) {
8768 foreach ($array as $value) {
8769 if (is_array($value)) {
8770 return true;
8773 return false;
8777 * get_performance_info() pairs up with init_performance_info()
8778 * loaded in setup.php. Returns an array with 'html' and 'txt'
8779 * values ready for use, and each of the individual stats provided
8780 * separately as well.
8782 * @return array
8784 function get_performance_info() {
8785 global $CFG, $PERF, $DB, $PAGE;
8787 $info = array();
8788 $info['html'] = ''; // Holds userfriendly HTML representation.
8789 $info['txt'] = me() . ' '; // Holds log-friendly representation.
8791 $info['realtime'] = microtime_diff($PERF->starttime, microtime());
8793 $info['html'] .= '<span class="timeused">'.$info['realtime'].' secs</span> ';
8794 $info['txt'] .= 'time: '.$info['realtime'].'s ';
8796 if (function_exists('memory_get_usage')) {
8797 $info['memory_total'] = memory_get_usage();
8798 $info['memory_growth'] = memory_get_usage() - $PERF->startmemory;
8799 $info['html'] .= '<span class="memoryused">RAM: '.display_size($info['memory_total']).'</span> ';
8800 $info['txt'] .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '.
8801 $info['memory_growth'].'B ('.display_size($info['memory_growth']).') ';
8804 if (function_exists('memory_get_peak_usage')) {
8805 $info['memory_peak'] = memory_get_peak_usage();
8806 $info['html'] .= '<span class="memoryused">RAM peak: '.display_size($info['memory_peak']).'</span> ';
8807 $info['txt'] .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') ';
8810 $inc = get_included_files();
8811 $info['includecount'] = count($inc);
8812 $info['html'] .= '<span class="included">Included '.$info['includecount'].' files</span> ';
8813 $info['txt'] .= 'includecount: '.$info['includecount'].' ';
8815 if (!empty($CFG->early_install_lang) or empty($PAGE)) {
8816 // We can not track more performance before installation or before PAGE init, sorry.
8817 return $info;
8820 $filtermanager = filter_manager::instance();
8821 if (method_exists($filtermanager, 'get_performance_summary')) {
8822 list($filterinfo, $nicenames) = $filtermanager->get_performance_summary();
8823 $info = array_merge($filterinfo, $info);
8824 foreach ($filterinfo as $key => $value) {
8825 $info['html'] .= "<span class='$key'>$nicenames[$key]: $value </span> ";
8826 $info['txt'] .= "$key: $value ";
8830 $stringmanager = get_string_manager();
8831 if (method_exists($stringmanager, 'get_performance_summary')) {
8832 list($filterinfo, $nicenames) = $stringmanager->get_performance_summary();
8833 $info = array_merge($filterinfo, $info);
8834 foreach ($filterinfo as $key => $value) {
8835 $info['html'] .= "<span class='$key'>$nicenames[$key]: $value </span> ";
8836 $info['txt'] .= "$key: $value ";
8840 $jsmodules = $PAGE->requires->get_loaded_modules();
8841 if ($jsmodules) {
8842 $yuicount = 0;
8843 $othercount = 0;
8844 $details = '';
8845 foreach ($jsmodules as $module => $backtraces) {
8846 if (strpos($module, 'yui') === 0) {
8847 $yuicount += 1;
8848 } else {
8849 $othercount += 1;
8851 if (!empty($CFG->yuimoduledebug)) {
8852 // Hidden feature for developers working on YUI module infrastructure.
8853 $details .= "<div class='yui-module'><p>$module</p>";
8854 foreach ($backtraces as $backtrace) {
8855 $details .= "<div class='backtrace'>$backtrace</div>";
8857 $details .= '</div>';
8860 $info['html'] .= "<span class='includedyuimodules'>Included YUI modules: $yuicount</span> ";
8861 $info['txt'] .= "includedyuimodules: $yuicount ";
8862 $info['html'] .= "<span class='includedjsmodules'>Other JavaScript modules: $othercount</span> ";
8863 $info['txt'] .= "includedjsmodules: $othercount ";
8864 if ($details) {
8865 $info['html'] .= '<div id="yui-module-debug" class="notifytiny">'.$details.'</div>';
8869 if (!empty($PERF->logwrites)) {
8870 $info['logwrites'] = $PERF->logwrites;
8871 $info['html'] .= '<span class="logwrites">Log DB writes '.$info['logwrites'].'</span> ';
8872 $info['txt'] .= 'logwrites: '.$info['logwrites'].' ';
8875 $info['dbqueries'] = $DB->perf_get_reads().'/'.($DB->perf_get_writes() - $PERF->logwrites);
8876 $info['html'] .= '<span class="dbqueries">DB reads/writes: '.$info['dbqueries'].'</span> ';
8877 $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' ';
8879 if (function_exists('posix_times')) {
8880 $ptimes = posix_times();
8881 if (is_array($ptimes)) {
8882 foreach ($ptimes as $key => $val) {
8883 $info[$key] = $ptimes[$key] - $PERF->startposixtimes[$key];
8885 $info['html'] .= "<span class=\"posixtimes\">ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</span> ";
8886 $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] ";
8890 // Grab the load average for the last minute.
8891 // /proc will only work under some linux configurations
8892 // while uptime is there under MacOSX/Darwin and other unices.
8893 if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) {
8894 list($serverload) = explode(' ', $loadavg[0]);
8895 unset($loadavg);
8896 } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) {
8897 if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) {
8898 $serverload = $matches[1];
8899 } else {
8900 trigger_error('Could not parse uptime output!');
8903 if (!empty($serverload)) {
8904 $info['serverload'] = $serverload;
8905 $info['html'] .= '<span class="serverload">Load average: '.$info['serverload'].'</span> ';
8906 $info['txt'] .= "serverload: {$info['serverload']} ";
8909 // Display size of session if session started.
8910 if (session_id()) {
8911 $info['sessionsize'] = display_size(strlen(session_encode()));
8912 $info['html'] .= '<span class="sessionsize">Session: ' . $info['sessionsize'] . '</span> ';
8913 $info['txt'] .= "Session: {$info['sessionsize']} ";
8916 if ($stats = cache_helper::get_stats()) {
8917 $html = '<span class="cachesused">';
8918 $html .= '<span class="cache-stats-heading">Caches used (hits/misses/sets)</span>';
8919 $text = 'Caches used (hits/misses/sets): ';
8920 $hits = 0;
8921 $misses = 0;
8922 $sets = 0;
8923 foreach ($stats as $definition => $stores) {
8924 $html .= '<span class="cache-definition-stats">';
8925 $html .= '<span class="cache-definition-stats-heading">'.$definition.'</span>';
8926 $text .= "$definition {";
8927 foreach ($stores as $store => $data) {
8928 $hits += $data['hits'];
8929 $misses += $data['misses'];
8930 $sets += $data['sets'];
8931 if ($data['hits'] == 0 and $data['misses'] > 0) {
8932 $cachestoreclass = 'nohits';
8933 } else if ($data['hits'] < $data['misses']) {
8934 $cachestoreclass = 'lowhits';
8935 } else {
8936 $cachestoreclass = 'hihits';
8938 $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
8939 $html .= "<span class=\"cache-store-stats $cachestoreclass\">$store: $data[hits] / $data[misses] / $data[sets]</span>";
8941 $html .= '</span>';
8942 $text .= '} ';
8944 $html .= "<span class='cache-total-stats'>Total: $hits / $misses / $sets</span>";
8945 $html .= '</span> ';
8946 $info['cachesused'] = "$hits / $misses / $sets";
8947 $info['html'] .= $html;
8948 $info['txt'] .= $text.'. ';
8949 } else {
8950 $info['cachesused'] = '0 / 0 / 0';
8951 $info['html'] .= '<span class="cachesused">Caches used (hits/misses/sets): 0/0/0</span>';
8952 $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 ';
8955 $info['html'] = '<div class="performanceinfo siteinfo">'.$info['html'].'</div>';
8956 return $info;
8960 * Legacy function.
8962 * @todo Document this function linux people
8964 function apd_get_profiling() {
8965 return shell_exec('pprofp -u ' . ini_get('apd.dumpdir') . '/pprof.' . getmypid() . '.*');
8969 * Delete directory or only its content
8971 * @param string $dir directory path
8972 * @param bool $contentonly
8973 * @return bool success, true also if dir does not exist
8975 function remove_dir($dir, $contentonly=false) {
8976 if (!file_exists($dir)) {
8977 // Nothing to do.
8978 return true;
8980 if (!$handle = opendir($dir)) {
8981 return false;
8983 $result = true;
8984 while (false!==($item = readdir($handle))) {
8985 if ($item != '.' && $item != '..') {
8986 if (is_dir($dir.'/'.$item)) {
8987 $result = remove_dir($dir.'/'.$item) && $result;
8988 } else {
8989 $result = unlink($dir.'/'.$item) && $result;
8993 closedir($handle);
8994 if ($contentonly) {
8995 clearstatcache(); // Make sure file stat cache is properly invalidated.
8996 return $result;
8998 $result = rmdir($dir); // If anything left the result will be false, no need for && $result.
8999 clearstatcache(); // Make sure file stat cache is properly invalidated.
9000 return $result;
9004 * Detect if an object or a class contains a given property
9005 * will take an actual object or the name of a class
9007 * @param mix $obj Name of class or real object to test
9008 * @param string $property name of property to find
9009 * @return bool true if property exists
9011 function object_property_exists( $obj, $property ) {
9012 if (is_string( $obj )) {
9013 $properties = get_class_vars( $obj );
9014 } else {
9015 $properties = get_object_vars( $obj );
9017 return array_key_exists( $property, $properties );
9021 * Converts an object into an associative array
9023 * This function converts an object into an associative array by iterating
9024 * over its public properties. Because this function uses the foreach
9025 * construct, Iterators are respected. It works recursively on arrays of objects.
9026 * Arrays and simple values are returned as is.
9028 * If class has magic properties, it can implement IteratorAggregate
9029 * and return all available properties in getIterator()
9031 * @param mixed $var
9032 * @return array
9034 function convert_to_array($var) {
9035 $result = array();
9037 // Loop over elements/properties.
9038 foreach ($var as $key => $value) {
9039 // Recursively convert objects.
9040 if (is_object($value) || is_array($value)) {
9041 $result[$key] = convert_to_array($value);
9042 } else {
9043 // Simple values are untouched.
9044 $result[$key] = $value;
9047 return $result;
9051 * Detect a custom script replacement in the data directory that will
9052 * replace an existing moodle script
9054 * @return string|bool full path name if a custom script exists, false if no custom script exists
9056 function custom_script_path() {
9057 global $CFG, $SCRIPT;
9059 if ($SCRIPT === null) {
9060 // Probably some weird external script.
9061 return false;
9064 $scriptpath = $CFG->customscripts . $SCRIPT;
9066 // Check the custom script exists.
9067 if (file_exists($scriptpath) and is_file($scriptpath)) {
9068 return $scriptpath;
9069 } else {
9070 return false;
9075 * Returns whether or not the user object is a remote MNET user. This function
9076 * is in moodlelib because it does not rely on loading any of the MNET code.
9078 * @param object $user A valid user object
9079 * @return bool True if the user is from a remote Moodle.
9081 function is_mnet_remote_user($user) {
9082 global $CFG;
9084 if (!isset($CFG->mnet_localhost_id)) {
9085 include_once($CFG->dirroot . '/mnet/lib.php');
9086 $env = new mnet_environment();
9087 $env->init();
9088 unset($env);
9091 return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id);
9095 * This function will search for browser prefereed languages, setting Moodle
9096 * to use the best one available if $SESSION->lang is undefined
9098 function setup_lang_from_browser() {
9099 global $CFG, $SESSION, $USER;
9101 if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) {
9102 // Lang is defined in session or user profile, nothing to do.
9103 return;
9106 if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do.
9107 return;
9110 // Extract and clean langs from headers.
9111 $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
9112 $rawlangs = str_replace('-', '_', $rawlangs); // We are using underscores.
9113 $rawlangs = explode(',', $rawlangs); // Convert to array.
9114 $langs = array();
9116 $order = 1.0;
9117 foreach ($rawlangs as $lang) {
9118 if (strpos($lang, ';') === false) {
9119 $langs[(string)$order] = $lang;
9120 $order = $order-0.01;
9121 } else {
9122 $parts = explode(';', $lang);
9123 $pos = strpos($parts[1], '=');
9124 $langs[substr($parts[1], $pos+1)] = $parts[0];
9127 krsort($langs, SORT_NUMERIC);
9129 // Look for such langs under standard locations.
9130 foreach ($langs as $lang) {
9131 // Clean it properly for include.
9132 $lang = strtolower(clean_param($lang, PARAM_SAFEDIR));
9133 if (get_string_manager()->translation_exists($lang, false)) {
9134 // Lang exists, set it in session.
9135 $SESSION->lang = $lang;
9136 // We have finished. Go out.
9137 break;
9140 return;
9144 * Check if $url matches anything in proxybypass list
9146 * Any errors just result in the proxy being used (least bad)
9148 * @param string $url url to check
9149 * @return boolean true if we should bypass the proxy
9151 function is_proxybypass( $url ) {
9152 global $CFG;
9154 // Sanity check.
9155 if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) {
9156 return false;
9159 // Get the host part out of the url.
9160 if (!$host = parse_url( $url, PHP_URL_HOST )) {
9161 return false;
9164 // Get the possible bypass hosts into an array.
9165 $matches = explode( ',', $CFG->proxybypass );
9167 // Check for a match.
9168 // (IPs need to match the left hand side and hosts the right of the url,
9169 // but we can recklessly check both as there can't be a false +ve).
9170 foreach ($matches as $match) {
9171 $match = trim($match);
9173 // Try for IP match (Left side).
9174 $lhs = substr($host, 0, strlen($match));
9175 if (strcasecmp($match, $lhs)==0) {
9176 return true;
9179 // Try for host match (Right side).
9180 $rhs = substr($host, -strlen($match));
9181 if (strcasecmp($match, $rhs)==0) {
9182 return true;
9186 // Nothing matched.
9187 return false;
9191 * Check if the passed navigation is of the new style
9193 * @param mixed $navigation
9194 * @return bool true for yes false for no
9196 function is_newnav($navigation) {
9197 if (is_array($navigation) && !empty($navigation['newnav'])) {
9198 return true;
9199 } else {
9200 return false;
9205 * Checks whether the given variable name is defined as a variable within the given object.
9207 * This will NOT work with stdClass objects, which have no class variables.
9209 * @param string $var The variable name
9210 * @param object $object The object to check
9211 * @return boolean
9213 function in_object_vars($var, $object) {
9214 $classvars = get_class_vars(get_class($object));
9215 $classvars = array_keys($classvars);
9216 return in_array($var, $classvars);
9220 * Returns an array without repeated objects.
9221 * This function is similar to array_unique, but for arrays that have objects as values
9223 * @param array $array
9224 * @param bool $keepkeyassoc
9225 * @return array
9227 function object_array_unique($array, $keepkeyassoc = true) {
9228 $duplicatekeys = array();
9229 $tmp = array();
9231 foreach ($array as $key => $val) {
9232 // Convert objects to arrays, in_array() does not support objects.
9233 if (is_object($val)) {
9234 $val = (array)$val;
9237 if (!in_array($val, $tmp)) {
9238 $tmp[] = $val;
9239 } else {
9240 $duplicatekeys[] = $key;
9244 foreach ($duplicatekeys as $key) {
9245 unset($array[$key]);
9248 return $keepkeyassoc ? $array : array_values($array);
9252 * Is a userid the primary administrator?
9254 * @param int $userid int id of user to check
9255 * @return boolean
9257 function is_primary_admin($userid) {
9258 $primaryadmin = get_admin();
9260 if ($userid == $primaryadmin->id) {
9261 return true;
9262 } else {
9263 return false;
9268 * Returns the site identifier
9270 * @return string $CFG->siteidentifier, first making sure it is properly initialised.
9272 function get_site_identifier() {
9273 global $CFG;
9274 // Check to see if it is missing. If so, initialise it.
9275 if (empty($CFG->siteidentifier)) {
9276 set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']);
9278 // Return it.
9279 return $CFG->siteidentifier;
9283 * Check whether the given password has no more than the specified
9284 * number of consecutive identical characters.
9286 * @param string $password password to be checked against the password policy
9287 * @param integer $maxchars maximum number of consecutive identical characters
9288 * @return bool
9290 function check_consecutive_identical_characters($password, $maxchars) {
9292 if ($maxchars < 1) {
9293 return true; // Zero 0 is to disable this check.
9295 if (strlen($password) <= $maxchars) {
9296 return true; // Too short to fail this test.
9299 $previouschar = '';
9300 $consecutivecount = 1;
9301 foreach (str_split($password) as $char) {
9302 if ($char != $previouschar) {
9303 $consecutivecount = 1;
9304 } else {
9305 $consecutivecount++;
9306 if ($consecutivecount > $maxchars) {
9307 return false; // Check failed already.
9311 $previouschar = $char;
9314 return true;
9318 * Helper function to do partial function binding.
9319 * so we can use it for preg_replace_callback, for example
9320 * this works with php functions, user functions, static methods and class methods
9321 * it returns you a callback that you can pass on like so:
9323 * $callback = partial('somefunction', $arg1, $arg2);
9324 * or
9325 * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
9326 * or even
9327 * $obj = new someclass();
9328 * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2);
9330 * and then the arguments that are passed through at calltime are appended to the argument list.
9332 * @param mixed $function a php callback
9333 * @param mixed $arg1,... $argv arguments to partially bind with
9334 * @return array Array callback
9336 function partial() {
9337 if (!class_exists('partial')) {
9339 * Used to manage function binding.
9340 * @copyright 2009 Penny Leach
9341 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
9343 class partial{
9344 /** @var array */
9345 public $values = array();
9346 /** @var string The function to call as a callback. */
9347 public $func;
9349 * Constructor
9350 * @param string $func
9351 * @param array $args
9353 public function __construct($func, $args) {
9354 $this->values = $args;
9355 $this->func = $func;
9358 * Calls the callback function.
9359 * @return mixed
9361 public function method() {
9362 $args = func_get_args();
9363 return call_user_func_array($this->func, array_merge($this->values, $args));
9367 $args = func_get_args();
9368 $func = array_shift($args);
9369 $p = new partial($func, $args);
9370 return array($p, 'method');
9374 * helper function to load up and initialise the mnet environment
9375 * this must be called before you use mnet functions.
9377 * @return mnet_environment the equivalent of old $MNET global
9379 function get_mnet_environment() {
9380 global $CFG;
9381 require_once($CFG->dirroot . '/mnet/lib.php');
9382 static $instance = null;
9383 if (empty($instance)) {
9384 $instance = new mnet_environment();
9385 $instance->init();
9387 return $instance;
9391 * during xmlrpc server code execution, any code wishing to access
9392 * information about the remote peer must use this to get it.
9394 * @return mnet_remote_client the equivalent of old $MNETREMOTE_CLIENT global
9396 function get_mnet_remote_client() {
9397 if (!defined('MNET_SERVER')) {
9398 debugging(get_string('notinxmlrpcserver', 'mnet'));
9399 return false;
9401 global $MNET_REMOTE_CLIENT;
9402 if (isset($MNET_REMOTE_CLIENT)) {
9403 return $MNET_REMOTE_CLIENT;
9405 return false;
9409 * during the xmlrpc server code execution, this will be called
9410 * to setup the object returned by {@link get_mnet_remote_client}
9412 * @param mnet_remote_client $client the client to set up
9413 * @throws moodle_exception
9415 function set_mnet_remote_client($client) {
9416 if (!defined('MNET_SERVER')) {
9417 throw new moodle_exception('notinxmlrpcserver', 'mnet');
9419 global $MNET_REMOTE_CLIENT;
9420 $MNET_REMOTE_CLIENT = $client;
9424 * return the jump url for a given remote user
9425 * this is used for rewriting forum post links in emails, etc
9427 * @param stdclass $user the user to get the idp url for
9429 function mnet_get_idp_jump_url($user) {
9430 global $CFG;
9432 static $mnetjumps = array();
9433 if (!array_key_exists($user->mnethostid, $mnetjumps)) {
9434 $idp = mnet_get_peer_host($user->mnethostid);
9435 $idpjumppath = mnet_get_app_jumppath($idp->applicationid);
9436 $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl=';
9438 return $mnetjumps[$user->mnethostid];
9442 * Gets the homepage to use for the current user
9444 * @return int One of HOMEPAGE_*
9446 function get_home_page() {
9447 global $CFG;
9449 if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) {
9450 if ($CFG->defaulthomepage == HOMEPAGE_MY) {
9451 return HOMEPAGE_MY;
9452 } else {
9453 return (int)get_user_preferences('user_home_page_preference', HOMEPAGE_MY);
9456 return HOMEPAGE_SITE;
9460 * Gets the name of a course to be displayed when showing a list of courses.
9461 * By default this is just $course->fullname but user can configure it. The
9462 * result of this function should be passed through print_string.
9463 * @param stdClass|course_in_list $course Moodle course object
9464 * @return string Display name of course (either fullname or short + fullname)
9466 function get_course_display_name_for_list($course) {
9467 global $CFG;
9468 if (!empty($CFG->courselistshortnames)) {
9469 if (!($course instanceof stdClass)) {
9470 $course = (object)convert_to_array($course);
9472 return get_string('courseextendednamedisplay', '', $course);
9473 } else {
9474 return $course->fullname;
9479 * The lang_string class
9481 * This special class is used to create an object representation of a string request.
9482 * It is special because processing doesn't occur until the object is first used.
9483 * The class was created especially to aid performance in areas where strings were
9484 * required to be generated but were not necessarily used.
9485 * As an example the admin tree when generated uses over 1500 strings, of which
9486 * normally only 1/3 are ever actually printed at any time.
9487 * The performance advantage is achieved by not actually processing strings that
9488 * arn't being used, as such reducing the processing required for the page.
9490 * How to use the lang_string class?
9491 * There are two methods of using the lang_string class, first through the
9492 * forth argument of the get_string function, and secondly directly.
9493 * The following are examples of both.
9494 * 1. Through get_string calls e.g.
9495 * $string = get_string($identifier, $component, $a, true);
9496 * $string = get_string('yes', 'moodle', null, true);
9497 * 2. Direct instantiation
9498 * $string = new lang_string($identifier, $component, $a, $lang);
9499 * $string = new lang_string('yes');
9501 * How do I use a lang_string object?
9502 * The lang_string object makes use of a magic __toString method so that you
9503 * are able to use the object exactly as you would use a string in most cases.
9504 * This means you are able to collect it into a variable and then directly
9505 * echo it, or concatenate it into another string, or similar.
9506 * The other thing you can do is manually get the string by calling the
9507 * lang_strings out method e.g.
9508 * $string = new lang_string('yes');
9509 * $string->out();
9510 * Also worth noting is that the out method can take one argument, $lang which
9511 * allows the developer to change the language on the fly.
9513 * When should I use a lang_string object?
9514 * The lang_string object is designed to be used in any situation where a
9515 * string may not be needed, but needs to be generated.
9516 * The admin tree is a good example of where lang_string objects should be
9517 * used.
9518 * A more practical example would be any class that requries strings that may
9519 * not be printed (after all classes get renderer by renderers and who knows
9520 * what they will do ;))
9522 * When should I not use a lang_string object?
9523 * Don't use lang_strings when you are going to use a string immediately.
9524 * There is no need as it will be processed immediately and there will be no
9525 * advantage, and in fact perhaps a negative hit as a class has to be
9526 * instantiated for a lang_string object, however get_string won't require
9527 * that.
9529 * Limitations:
9530 * 1. You cannot use a lang_string object as an array offset. Doing so will
9531 * result in PHP throwing an error. (You can use it as an object property!)
9533 * @package core
9534 * @category string
9535 * @copyright 2011 Sam Hemelryk
9536 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
9538 class lang_string {
9540 /** @var string The strings identifier */
9541 protected $identifier;
9542 /** @var string The strings component. Default '' */
9543 protected $component = '';
9544 /** @var array|stdClass Any arguments required for the string. Default null */
9545 protected $a = null;
9546 /** @var string The language to use when processing the string. Default null */
9547 protected $lang = null;
9549 /** @var string The processed string (once processed) */
9550 protected $string = null;
9553 * A special boolean. If set to true then the object has been woken up and
9554 * cannot be regenerated. If this is set then $this->string MUST be used.
9555 * @var bool
9557 protected $forcedstring = false;
9560 * Constructs a lang_string object
9562 * This function should do as little processing as possible to ensure the best
9563 * performance for strings that won't be used.
9565 * @param string $identifier The strings identifier
9566 * @param string $component The strings component
9567 * @param stdClass|array $a Any arguments the string requires
9568 * @param string $lang The language to use when processing the string.
9569 * @throws coding_exception
9571 public function __construct($identifier, $component = '', $a = null, $lang = null) {
9572 if (empty($component)) {
9573 $component = 'moodle';
9576 $this->identifier = $identifier;
9577 $this->component = $component;
9578 $this->lang = $lang;
9580 // We MUST duplicate $a to ensure that it if it changes by reference those
9581 // changes are not carried across.
9582 // To do this we always ensure $a or its properties/values are strings
9583 // and that any properties/values that arn't convertable are forgotten.
9584 if (!empty($a)) {
9585 if (is_scalar($a)) {
9586 $this->a = $a;
9587 } else if ($a instanceof lang_string) {
9588 $this->a = $a->out();
9589 } else if (is_object($a) or is_array($a)) {
9590 $a = (array)$a;
9591 $this->a = array();
9592 foreach ($a as $key => $value) {
9593 // Make sure conversion errors don't get displayed (results in '').
9594 if (is_array($value)) {
9595 $this->a[$key] = '';
9596 } else if (is_object($value)) {
9597 if (method_exists($value, '__toString')) {
9598 $this->a[$key] = $value->__toString();
9599 } else {
9600 $this->a[$key] = '';
9602 } else {
9603 $this->a[$key] = (string)$value;
9609 if (debugging(false, DEBUG_DEVELOPER)) {
9610 if (clean_param($this->identifier, PARAM_STRINGID) == '') {
9611 throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition');
9613 if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') {
9614 throw new coding_exception('Invalid string compontent. Please check your string definition');
9616 if (!get_string_manager()->string_exists($this->identifier, $this->component)) {
9617 debugging('String does not exist. Please check your string definition for '.$this->identifier.'/'.$this->component, DEBUG_DEVELOPER);
9623 * Processes the string.
9625 * This function actually processes the string, stores it in the string property
9626 * and then returns it.
9627 * You will notice that this function is VERY similar to the get_string method.
9628 * That is because it is pretty much doing the same thing.
9629 * However as this function is an upgrade it isn't as tolerant to backwards
9630 * compatibility.
9632 * @return string
9633 * @throws coding_exception
9635 protected function get_string() {
9636 global $CFG;
9638 // Check if we need to process the string.
9639 if ($this->string === null) {
9640 // Check the quality of the identifier.
9641 if ($CFG->debugdeveloper && clean_param($this->identifier, PARAM_STRINGID) === '') {
9642 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);
9645 // Process the string.
9646 $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang);
9647 // Debugging feature lets you display string identifier and component.
9648 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
9649 $this->string .= ' {' . $this->identifier . '/' . $this->component . '}';
9652 // Return the string.
9653 return $this->string;
9657 * Returns the string
9659 * @param string $lang The langauge to use when processing the string
9660 * @return string
9662 public function out($lang = null) {
9663 if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) {
9664 if ($this->forcedstring) {
9665 debugging('lang_string objects that have been used cannot be printed in another language. ('.$this->lang.' used)', DEBUG_DEVELOPER);
9666 return $this->get_string();
9668 $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang);
9669 return $translatedstring->out();
9671 return $this->get_string();
9675 * Magic __toString method for printing a string
9677 * @return string
9679 public function __toString() {
9680 return $this->get_string();
9684 * Magic __set_state method used for var_export
9686 * @return string
9688 public function __set_state() {
9689 return $this->get_string();
9693 * Prepares the lang_string for sleep and stores only the forcedstring and
9694 * string properties... the string cannot be regenerated so we need to ensure
9695 * it is generated for this.
9697 * @return string
9699 public function __sleep() {
9700 $this->get_string();
9701 $this->forcedstring = true;
9702 return array('forcedstring', 'string', 'lang');