MDL-38898 theme_bootstrap: Made changes to layout/general.php to enable drag-n-drop...
[moodle.git] / lib / moodlelib.php
blob1d5b39ae82664d19259854e2dda7657a2aac86c1
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. //////////////
76 /**
77 * PARAM_ALPHA - contains only english ascii letters a-zA-Z.
79 define('PARAM_ALPHA', 'alpha');
81 /**
82 * PARAM_ALPHAEXT the same contents as PARAM_ALPHA plus the chars in quotes: "_-" allowed
83 * NOTE: originally this allowed "/" too, please use PARAM_SAFEPATH if "/" needed
85 define('PARAM_ALPHAEXT', 'alphaext');
87 /**
88 * PARAM_ALPHANUM - expected numbers and letters only.
90 define('PARAM_ALPHANUM', 'alphanum');
92 /**
93 * PARAM_ALPHANUMEXT - expected numbers, letters only and _-.
95 define('PARAM_ALPHANUMEXT', 'alphanumext');
97 /**
98 * PARAM_AUTH - actually checks to make sure the string is a valid auth plugin
100 define('PARAM_AUTH', 'auth');
103 * PARAM_BASE64 - Base 64 encoded format
105 define('PARAM_BASE64', 'base64');
108 * PARAM_BOOL - converts input into 0 or 1, use for switches in forms and urls.
110 define('PARAM_BOOL', 'bool');
113 * PARAM_CAPABILITY - A capability name, like 'moodle/role:manage'. Actually
114 * checked against the list of capabilities in the database.
116 define('PARAM_CAPABILITY', 'capability');
119 * PARAM_CLEANHTML - cleans submitted HTML code. Note that you almost never want
120 * to use this. The normal mode of operation is to use PARAM_RAW when recieving
121 * the input (required/optional_param or formslib) and then sanitse the HTML
122 * using format_text on output. This is for the rare cases when you want to
123 * sanitise the HTML on input. This cleaning may also fix xhtml strictness.
125 define('PARAM_CLEANHTML', 'cleanhtml');
128 * PARAM_EMAIL - an email address following the RFC
130 define('PARAM_EMAIL', 'email');
133 * PARAM_FILE - safe file name, all dangerous chars are stripped, protects against XSS, SQL injections and directory traversals
135 define('PARAM_FILE', 'file');
138 * PARAM_FLOAT - a real/floating point number.
140 * Note that you should not use PARAM_FLOAT for numbers typed in by the user.
141 * It does not work for languages that use , as a decimal separator.
142 * Instead, do something like
143 * $rawvalue = required_param('name', PARAM_RAW);
144 * // ... other code including require_login, which sets current lang ...
145 * $realvalue = unformat_float($rawvalue);
146 * // ... then use $realvalue
148 define('PARAM_FLOAT', 'float');
151 * PARAM_HOST - expected fully qualified domain name (FQDN) or an IPv4 dotted quad (IP address)
153 define('PARAM_HOST', 'host');
156 * PARAM_INT - integers only, use when expecting only numbers.
158 define('PARAM_INT', 'int');
161 * PARAM_LANG - checks to see if the string is a valid installed language in the current site.
163 define('PARAM_LANG', 'lang');
166 * PARAM_LOCALURL - expected properly formatted URL as well as one that refers to the local server itself. (NOT orthogonal to the others! Implies PARAM_URL!)
168 define('PARAM_LOCALURL', 'localurl');
171 * PARAM_NOTAGS - all html tags are stripped from the text. Do not abuse this type.
173 define('PARAM_NOTAGS', 'notags');
176 * PARAM_PATH - safe relative path name, all dangerous chars are stripped, protects against XSS, SQL injections and directory traversals
177 * note: the leading slash is not removed, window drive letter is not allowed
179 define('PARAM_PATH', 'path');
182 * PARAM_PEM - Privacy Enhanced Mail format
184 define('PARAM_PEM', 'pem');
187 * PARAM_PERMISSION - A permission, one of CAP_INHERIT, CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT.
189 define('PARAM_PERMISSION', 'permission');
192 * PARAM_RAW specifies a parameter that is not cleaned/processed in any way except the discarding of the invalid utf-8 characters
194 define('PARAM_RAW', 'raw');
197 * PARAM_RAW_TRIMMED like PARAM_RAW but leading and trailing whitespace is stripped.
199 define('PARAM_RAW_TRIMMED', 'raw_trimmed');
202 * PARAM_SAFEDIR - safe directory name, suitable for include() and require()
204 define('PARAM_SAFEDIR', 'safedir');
207 * PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths, etc.
209 define('PARAM_SAFEPATH', 'safepath');
212 * PARAM_SEQUENCE - expects a sequence of numbers like 8 to 1,5,6,4,6,8,9. Numbers and comma only.
214 define('PARAM_SEQUENCE', 'sequence');
217 * PARAM_TAG - one tag (interests, blogs, etc.) - mostly international characters and space, <> not supported
219 define('PARAM_TAG', 'tag');
222 * PARAM_TAGLIST - list of tags separated by commas (interests, blogs, etc.)
224 define('PARAM_TAGLIST', 'taglist');
227 * PARAM_TEXT - general plain text compatible with multilang filter, no other html tags. Please note '<', or '>' are allowed here.
229 define('PARAM_TEXT', 'text');
232 * PARAM_THEME - Checks to see if the string is a valid theme name in the current site
234 define('PARAM_THEME', 'theme');
237 * PARAM_URL - expected properly formatted URL. Please note that domain part is required, http://localhost/ is not accepted but 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 accounts, do NOT use when syncing with external systems!!
244 define('PARAM_USERNAME', 'username');
247 * PARAM_STRINGID - used to check if the given string is valid string identifier for get_string()
249 define('PARAM_STRINGID', 'stringid');
251 ///// DEPRECATED PARAM TYPES OR ALIASES - DO NOT USE FOR NEW CODE /////
253 * PARAM_CLEAN - obsoleted, please use a more specific type of parameter.
254 * It was one of the first types, that is why it is abused so much ;-)
255 * @deprecated since 2.0
257 define('PARAM_CLEAN', 'clean');
260 * PARAM_INTEGER - deprecated alias for PARAM_INT
261 * @deprecated since 2.0
263 define('PARAM_INTEGER', 'int');
266 * PARAM_NUMBER - deprecated alias of PARAM_FLOAT
267 * @deprecated since 2.0
269 define('PARAM_NUMBER', 'float');
272 * PARAM_ACTION - deprecated alias for PARAM_ALPHANUMEXT, use for various actions in forms and urls
273 * NOTE: originally alias for PARAM_APLHA
274 * @deprecated since 2.0
276 define('PARAM_ACTION', 'alphanumext');
279 * PARAM_FORMAT - deprecated alias for PARAM_ALPHANUMEXT, use for names of plugins, formats, etc.
280 * NOTE: originally alias for PARAM_APLHA
281 * @deprecated since 2.0
283 define('PARAM_FORMAT', 'alphanumext');
286 * PARAM_MULTILANG - deprecated alias of PARAM_TEXT.
287 * @deprecated since 2.0
289 define('PARAM_MULTILANG', 'text');
292 * PARAM_TIMEZONE - expected timezone. Timezone can be int +-(0-13) or float +-(0.5-12.5) or
293 * string seperated by '/' and can have '-' &/ '_' (eg. America/North_Dakota/New_Salem
294 * America/Port-au-Prince)
296 define('PARAM_TIMEZONE', 'timezone');
299 * PARAM_CLEANFILE - deprecated alias of PARAM_FILE; originally was removing regional chars too
301 define('PARAM_CLEANFILE', 'file');
304 * PARAM_COMPONENT is used for full component names (aka frankenstyle) such as 'mod_forum', 'core_rating', 'auth_ldap'.
305 * Short legacy subsystem names and module names are accepted too ex: 'forum', 'rating', 'user'.
306 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
307 * NOTE: numbers and underscores are strongly discouraged in plugin names!
309 define('PARAM_COMPONENT', 'component');
312 * PARAM_AREA is a name of area used when addressing files, comments, ratings, etc.
313 * It is usually used together with context id and component.
314 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
316 define('PARAM_AREA', 'area');
319 * PARAM_PLUGIN is used for plugin names such as 'forum', 'glossary', 'ldap', 'radius', 'paypal', 'completionstatus'.
320 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
321 * NOTE: numbers and underscores are strongly discouraged in plugin names! Underscores are forbidden in module names.
323 define('PARAM_PLUGIN', 'plugin');
326 /// Web Services ///
329 * VALUE_REQUIRED - if the parameter is not supplied, there is an error
331 define('VALUE_REQUIRED', 1);
334 * VALUE_OPTIONAL - if the parameter is not supplied, then the param has no value
336 define('VALUE_OPTIONAL', 2);
339 * VALUE_DEFAULT - if the parameter is not supplied, then the default value is used
341 define('VALUE_DEFAULT', 0);
344 * NULL_NOT_ALLOWED - the parameter can not be set to null in the database
346 define('NULL_NOT_ALLOWED', false);
349 * NULL_ALLOWED - the parameter can be set to null in the database
351 define('NULL_ALLOWED', true);
353 /// Page types ///
355 * PAGE_COURSE_VIEW is a definition of a page type. For more information on the page class see moodle/lib/pagelib.php.
357 define('PAGE_COURSE_VIEW', 'course-view');
359 /** Get remote addr constant */
360 define('GETREMOTEADDR_SKIP_HTTP_CLIENT_IP', '1');
361 /** Get remote addr constant */
362 define('GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR', '2');
364 /// Blog access level constant declaration ///
365 define ('BLOG_USER_LEVEL', 1);
366 define ('BLOG_GROUP_LEVEL', 2);
367 define ('BLOG_COURSE_LEVEL', 3);
368 define ('BLOG_SITE_LEVEL', 4);
369 define ('BLOG_GLOBAL_LEVEL', 5);
372 ///Tag constants///
374 * To prevent problems with multibytes strings,Flag updating in nav not working on the review page. this should not exceed the
375 * length of "varchar(255) / 3 (bytes / utf-8 character) = 85".
376 * TODO: this is not correct, varchar(255) are 255 unicode chars ;-)
378 * @todo define(TAG_MAX_LENGTH) this is not correct, varchar(255) are 255 unicode chars ;-)
380 define('TAG_MAX_LENGTH', 50);
382 /// Password policy constants ///
383 define ('PASSWORD_LOWER', 'abcdefghijklmnopqrstuvwxyz');
384 define ('PASSWORD_UPPER', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
385 define ('PASSWORD_DIGITS', '0123456789');
386 define ('PASSWORD_NONALPHANUM', '.,;:!?_-+/*@#&$');
388 /// Feature constants ///
389 // Used for plugin_supports() to report features that are, or are not, supported by a module.
391 /** True if module can provide a grade */
392 define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade');
393 /** True if module supports outcomes */
394 define('FEATURE_GRADE_OUTCOMES', 'outcomes');
395 /** True if module supports advanced grading methods */
396 define('FEATURE_ADVANCED_GRADING', 'grade_advanced_grading');
397 /** True if module controls the grade visibility over the gradebook */
398 define('FEATURE_CONTROLS_GRADE_VISIBILITY', 'controlsgradevisbility');
399 /** True if module supports plagiarism plugins */
400 define('FEATURE_PLAGIARISM', 'plagiarism');
402 /** True if module has code to track whether somebody viewed it */
403 define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views');
404 /** True if module has custom completion rules */
405 define('FEATURE_COMPLETION_HAS_RULES', 'completion_has_rules');
407 /** True if module has no 'view' page (like label) */
408 define('FEATURE_NO_VIEW_LINK', 'viewlink');
409 /** True if module supports outcomes */
410 define('FEATURE_IDNUMBER', 'idnumber');
411 /** True if module supports groups */
412 define('FEATURE_GROUPS', 'groups');
413 /** True if module supports groupings */
414 define('FEATURE_GROUPINGS', 'groupings');
415 /** True if module supports groupmembersonly */
416 define('FEATURE_GROUPMEMBERSONLY', 'groupmembersonly');
418 /** Type of module */
419 define('FEATURE_MOD_ARCHETYPE', 'mod_archetype');
420 /** True if module supports intro editor */
421 define('FEATURE_MOD_INTRO', 'mod_intro');
422 /** True if module has default completion */
423 define('FEATURE_MODEDIT_DEFAULT_COMPLETION', 'modedit_default_completion');
425 define('FEATURE_COMMENT', 'comment');
427 define('FEATURE_RATE', 'rate');
428 /** True if module supports backup/restore of moodle2 format */
429 define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2');
431 /** True if module can show description on course main page */
432 define('FEATURE_SHOW_DESCRIPTION', 'showdescription');
434 /** Unspecified module archetype */
435 define('MOD_ARCHETYPE_OTHER', 0);
436 /** Resource-like type module */
437 define('MOD_ARCHETYPE_RESOURCE', 1);
438 /** Assignment module archetype */
439 define('MOD_ARCHETYPE_ASSIGNMENT', 2);
440 /** System (not user-addable) module archetype */
441 define('MOD_ARCHETYPE_SYSTEM', 3);
444 * Security token used for allowing access
445 * from external application such as web services.
446 * Scripts do not use any session, performance is relatively
447 * low because we need to load access info in each request.
448 * Scripts are executed in parallel.
450 define('EXTERNAL_TOKEN_PERMANENT', 0);
453 * Security token used for allowing access
454 * of embedded applications, the code is executed in the
455 * active user session. Token is invalidated after user logs out.
456 * Scripts are executed serially - normal session locking is used.
458 define('EXTERNAL_TOKEN_EMBEDDED', 1);
461 * The home page should be the site home
463 define('HOMEPAGE_SITE', 0);
465 * The home page should be the users my page
467 define('HOMEPAGE_MY', 1);
469 * The home page can be chosen by the user
471 define('HOMEPAGE_USER', 2);
474 * Hub directory url (should be moodle.org)
476 define('HUB_HUBDIRECTORYURL', "http://hubdirectory.moodle.org");
480 * Moodle.org url (should be moodle.org)
482 define('HUB_MOODLEORGHUBURL', "http://hub.moodle.org");
485 * Moodle mobile app service name
487 define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app');
490 * Indicates the user has the capabilities required to ignore activity and course file size restrictions
492 define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1);
495 * Course display settings
497 define('COURSE_DISPLAY_SINGLEPAGE', 0); // display all sections on one page
498 define('COURSE_DISPLAY_MULTIPAGE', 1); // split pages into a page per section
501 * Authentication constants.
503 define('AUTH_PASSWORD_NOT_CACHED', 'not cached'); // String used in password field when password is not stored.
505 /// PARAMETER HANDLING ////////////////////////////////////////////////////
508 * Returns a particular value for the named variable, taken from
509 * POST or GET. If the parameter doesn't exist then an error is
510 * thrown because we require this variable.
512 * This function should be used to initialise all required values
513 * in a script that are based on parameters. Usually it will be
514 * used like this:
515 * $id = required_param('id', PARAM_INT);
517 * Please note the $type parameter is now required and the value can not be array.
519 * @param string $parname the name of the page parameter we want
520 * @param string $type expected type of parameter
521 * @return mixed
523 function required_param($parname, $type) {
524 if (func_num_args() != 2 or empty($parname) or empty($type)) {
525 throw new coding_exception('required_param() requires $parname and $type to be specified (parameter: '.$parname.')');
527 if (isset($_POST[$parname])) { // POST has precedence
528 $param = $_POST[$parname];
529 } else if (isset($_GET[$parname])) {
530 $param = $_GET[$parname];
531 } else {
532 print_error('missingparam', '', '', $parname);
535 if (is_array($param)) {
536 debugging('Invalid array parameter detected in required_param(): '.$parname);
537 // TODO: switch to fatal error in Moodle 2.3
538 //print_error('missingparam', '', '', $parname);
539 return required_param_array($parname, $type);
542 return clean_param($param, $type);
546 * Returns a particular array value for the named variable, taken from
547 * POST or GET. If the parameter doesn't exist then an error is
548 * thrown because we require this variable.
550 * This function should be used to initialise all required values
551 * in a script that are based on parameters. Usually it will be
552 * used like this:
553 * $ids = required_param_array('ids', PARAM_INT);
555 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
557 * @param string $parname the name of the page parameter we want
558 * @param string $type expected type of parameter
559 * @return array
561 function required_param_array($parname, $type) {
562 if (func_num_args() != 2 or empty($parname) or empty($type)) {
563 throw new coding_exception('required_param_array() requires $parname and $type to be specified (parameter: '.$parname.')');
565 if (isset($_POST[$parname])) { // POST has precedence
566 $param = $_POST[$parname];
567 } else if (isset($_GET[$parname])) {
568 $param = $_GET[$parname];
569 } else {
570 print_error('missingparam', '', '', $parname);
572 if (!is_array($param)) {
573 print_error('missingparam', '', '', $parname);
576 $result = array();
577 foreach($param as $key=>$value) {
578 if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
579 debugging('Invalid key name in required_param_array() detected: '.$key.', parameter: '.$parname);
580 continue;
582 $result[$key] = clean_param($value, $type);
585 return $result;
589 * Returns a particular value for the named variable, taken from
590 * POST or GET, otherwise returning a given default.
592 * This function should be used to initialise all optional values
593 * in a script that are based on parameters. Usually it will be
594 * used like this:
595 * $name = optional_param('name', 'Fred', PARAM_TEXT);
597 * Please note the $type parameter is now required and the value can not be array.
599 * @param string $parname the name of the page parameter we want
600 * @param mixed $default the default value to return if nothing is found
601 * @param string $type expected type of parameter
602 * @return mixed
604 function optional_param($parname, $default, $type) {
605 if (func_num_args() != 3 or empty($parname) or empty($type)) {
606 throw new coding_exception('optional_param() requires $parname, $default and $type to be specified (parameter: '.$parname.')');
608 if (!isset($default)) {
609 $default = null;
612 if (isset($_POST[$parname])) { // POST has precedence
613 $param = $_POST[$parname];
614 } else if (isset($_GET[$parname])) {
615 $param = $_GET[$parname];
616 } else {
617 return $default;
620 if (is_array($param)) {
621 debugging('Invalid array parameter detected in required_param(): '.$parname);
622 // TODO: switch to $default in Moodle 2.3
623 //return $default;
624 return optional_param_array($parname, $default, $type);
627 return clean_param($param, $type);
631 * Returns a particular array value for the named variable, taken from
632 * POST or GET, otherwise returning a given default.
634 * This function should be used to initialise all optional values
635 * in a script that are based on parameters. Usually it will be
636 * used like this:
637 * $ids = optional_param('id', array(), PARAM_INT);
639 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
641 * @param string $parname the name of the page parameter we want
642 * @param mixed $default the default value to return if nothing is found
643 * @param string $type expected type of parameter
644 * @return array
646 function optional_param_array($parname, $default, $type) {
647 if (func_num_args() != 3 or empty($parname) or empty($type)) {
648 throw new coding_exception('optional_param_array() requires $parname, $default and $type to be specified (parameter: '.$parname.')');
651 if (isset($_POST[$parname])) { // POST has precedence
652 $param = $_POST[$parname];
653 } else if (isset($_GET[$parname])) {
654 $param = $_GET[$parname];
655 } else {
656 return $default;
658 if (!is_array($param)) {
659 debugging('optional_param_array() expects array parameters only: '.$parname);
660 return $default;
663 $result = array();
664 foreach($param as $key=>$value) {
665 if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
666 debugging('Invalid key name in optional_param_array() detected: '.$key.', parameter: '.$parname);
667 continue;
669 $result[$key] = clean_param($value, $type);
672 return $result;
676 * Strict validation of parameter values, the values are only converted
677 * to requested PHP type. Internally it is using clean_param, the values
678 * before and after cleaning must be equal - otherwise
679 * an invalid_parameter_exception is thrown.
680 * Objects and classes are not accepted.
682 * @param mixed $param
683 * @param string $type PARAM_ constant
684 * @param bool $allownull are nulls valid value?
685 * @param string $debuginfo optional debug information
686 * @return mixed the $param value converted to PHP type
687 * @throws invalid_parameter_exception if $param is not of given type
689 function validate_param($param, $type, $allownull=NULL_NOT_ALLOWED, $debuginfo='') {
690 if (is_null($param)) {
691 if ($allownull == NULL_ALLOWED) {
692 return null;
693 } else {
694 throw new invalid_parameter_exception($debuginfo);
697 if (is_array($param) or is_object($param)) {
698 throw new invalid_parameter_exception($debuginfo);
701 $cleaned = clean_param($param, $type);
703 if ($type == PARAM_FLOAT) {
704 // Do not detect precision loss here.
705 if (is_float($param) or is_int($param)) {
706 // These always fit.
707 } else if (!is_numeric($param) or !preg_match('/^[\+-]?[0-9]*\.?[0-9]*(e[-+]?[0-9]+)?$/i', (string)$param)) {
708 throw new invalid_parameter_exception($debuginfo);
710 } else if ((string)$param !== (string)$cleaned) {
711 // conversion to string is usually lossless
712 throw new invalid_parameter_exception($debuginfo);
715 return $cleaned;
719 * Makes sure array contains only the allowed types,
720 * this function does not validate array key names!
721 * <code>
722 * $options = clean_param($options, PARAM_INT);
723 * </code>
725 * @param array $param the variable array we are cleaning
726 * @param string $type expected format of param after cleaning.
727 * @param bool $recursive clean recursive arrays
728 * @return array
730 function clean_param_array(array $param = null, $type, $recursive = false) {
731 $param = (array)$param; // convert null to empty array
732 foreach ($param as $key => $value) {
733 if (is_array($value)) {
734 if ($recursive) {
735 $param[$key] = clean_param_array($value, $type, true);
736 } else {
737 throw new coding_exception('clean_param_array() can not process multidimensional arrays when $recursive is false.');
739 } else {
740 $param[$key] = clean_param($value, $type);
743 return $param;
747 * Used by {@link optional_param()} and {@link required_param()} to
748 * clean the variables and/or cast to specific types, based on
749 * an options field.
750 * <code>
751 * $course->format = clean_param($course->format, PARAM_ALPHA);
752 * $selectedgrade_item = clean_param($selectedgrade_item, PARAM_INT);
753 * </code>
755 * @param mixed $param the variable we are cleaning
756 * @param string $type expected format of param after cleaning.
757 * @return mixed
759 function clean_param($param, $type) {
761 global $CFG;
763 if (is_array($param)) {
764 throw new coding_exception('clean_param() can not process arrays, please use clean_param_array() instead.');
765 } else if (is_object($param)) {
766 if (method_exists($param, '__toString')) {
767 $param = $param->__toString();
768 } else {
769 throw new coding_exception('clean_param() can not process objects, please use clean_param_array() instead.');
773 switch ($type) {
774 case PARAM_RAW: // no cleaning at all
775 $param = fix_utf8($param);
776 return $param;
778 case PARAM_RAW_TRIMMED: // no cleaning, but strip leading and trailing whitespace.
779 $param = fix_utf8($param);
780 return trim($param);
782 case PARAM_CLEAN: // General HTML cleaning, try to use more specific type if possible
783 // this is deprecated!, please use more specific type instead
784 if (is_numeric($param)) {
785 return $param;
787 $param = fix_utf8($param);
788 return clean_text($param); // Sweep for scripts, etc
790 case PARAM_CLEANHTML: // clean html fragment
791 $param = fix_utf8($param);
792 $param = clean_text($param, FORMAT_HTML); // Sweep for scripts, etc
793 return trim($param);
795 case PARAM_INT:
796 return (int)$param; // Convert to integer
798 case PARAM_FLOAT:
799 return (float)$param; // Convert to float
801 case PARAM_ALPHA: // Remove everything not a-z
802 return preg_replace('/[^a-zA-Z]/i', '', $param);
804 case PARAM_ALPHAEXT: // Remove everything not a-zA-Z_- (originally allowed "/" too)
805 return preg_replace('/[^a-zA-Z_-]/i', '', $param);
807 case PARAM_ALPHANUM: // Remove everything not a-zA-Z0-9
808 return preg_replace('/[^A-Za-z0-9]/i', '', $param);
810 case PARAM_ALPHANUMEXT: // Remove everything not a-zA-Z0-9_-
811 return preg_replace('/[^A-Za-z0-9_-]/i', '', $param);
813 case PARAM_SEQUENCE: // Remove everything not 0-9,
814 return preg_replace('/[^0-9,]/i', '', $param);
816 case PARAM_BOOL: // Convert to 1 or 0
817 $tempstr = strtolower($param);
818 if ($tempstr === 'on' or $tempstr === 'yes' or $tempstr === 'true') {
819 $param = 1;
820 } else if ($tempstr === 'off' or $tempstr === 'no' or $tempstr === 'false') {
821 $param = 0;
822 } else {
823 $param = empty($param) ? 0 : 1;
825 return $param;
827 case PARAM_NOTAGS: // Strip all tags
828 $param = fix_utf8($param);
829 return strip_tags($param);
831 case PARAM_TEXT: // leave only tags needed for multilang
832 $param = fix_utf8($param);
833 // if the multilang syntax is not correct we strip all tags
834 // because it would break xhtml strict which is required for accessibility standards
835 // please note this cleaning does not strip unbalanced '>' for BC compatibility reasons
836 do {
837 if (strpos($param, '</lang>') !== false) {
838 // old and future mutilang syntax
839 $param = strip_tags($param, '<lang>');
840 if (!preg_match_all('/<.*>/suU', $param, $matches)) {
841 break;
843 $open = false;
844 foreach ($matches[0] as $match) {
845 if ($match === '</lang>') {
846 if ($open) {
847 $open = false;
848 continue;
849 } else {
850 break 2;
853 if (!preg_match('/^<lang lang="[a-zA-Z0-9_-]+"\s*>$/u', $match)) {
854 break 2;
855 } else {
856 $open = true;
859 if ($open) {
860 break;
862 return $param;
864 } else if (strpos($param, '</span>') !== false) {
865 // current problematic multilang syntax
866 $param = strip_tags($param, '<span>');
867 if (!preg_match_all('/<.*>/suU', $param, $matches)) {
868 break;
870 $open = false;
871 foreach ($matches[0] as $match) {
872 if ($match === '</span>') {
873 if ($open) {
874 $open = false;
875 continue;
876 } else {
877 break 2;
880 if (!preg_match('/^<span(\s+lang="[a-zA-Z0-9_-]+"|\s+class="multilang"){2}\s*>$/u', $match)) {
881 break 2;
882 } else {
883 $open = true;
886 if ($open) {
887 break;
889 return $param;
891 } while (false);
892 // easy, just strip all tags, if we ever want to fix orphaned '&' we have to do that in format_string()
893 return strip_tags($param);
895 case PARAM_COMPONENT:
896 // we do not want any guessing here, either the name is correct or not
897 // please note only normalised component names are accepted
898 if (!preg_match('/^[a-z]+(_[a-z][a-z0-9_]*)?[a-z0-9]$/', $param)) {
899 return '';
901 if (strpos($param, '__') !== false) {
902 return '';
904 if (strpos($param, 'mod_') === 0) {
905 // module names must not contain underscores because we need to differentiate them from invalid plugin types
906 if (substr_count($param, '_') != 1) {
907 return '';
910 return $param;
912 case PARAM_PLUGIN:
913 case PARAM_AREA:
914 // we do not want any guessing here, either the name is correct or not
915 if (!preg_match('/^[a-z][a-z0-9_]*[a-z0-9]$/', $param)) {
916 return '';
918 if (strpos($param, '__') !== false) {
919 return '';
921 return $param;
923 case PARAM_SAFEDIR: // Remove everything not a-zA-Z0-9_-
924 return preg_replace('/[^a-zA-Z0-9_-]/i', '', $param);
926 case PARAM_SAFEPATH: // Remove everything not a-zA-Z0-9/_-
927 return preg_replace('/[^a-zA-Z0-9\/_-]/i', '', $param);
929 case PARAM_FILE: // Strip all suspicious characters from filename
930 $param = fix_utf8($param);
931 $param = preg_replace('~[[:cntrl:]]|[&<>"`\|\':\\\\/]~u', '', $param);
932 if ($param === '.' || $param === '..') {
933 $param = '';
935 return $param;
937 case PARAM_PATH: // Strip all suspicious characters from file path
938 $param = fix_utf8($param);
939 $param = str_replace('\\', '/', $param);
941 // Explode the path and clean each element using the PARAM_FILE rules.
942 $breadcrumb = explode('/', $param);
943 foreach ($breadcrumb as $key => $crumb) {
944 if ($crumb === '.' && $key === 0) {
945 // Special condition to allow for relative current path such as ./currentdirfile.txt.
946 } else {
947 $crumb = clean_param($crumb, PARAM_FILE);
949 $breadcrumb[$key] = $crumb;
951 $param = implode('/', $breadcrumb);
953 // Remove multiple current path (./././) and multiple slashes (///).
954 $param = preg_replace('~//+~', '/', $param);
955 $param = preg_replace('~/(\./)+~', '/', $param);
956 return $param;
958 case PARAM_HOST: // allow FQDN or IPv4 dotted quad
959 $param = preg_replace('/[^\.\d\w-]/','', $param ); // only allowed chars
960 // match ipv4 dotted quad
961 if (preg_match('/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/',$param, $match)){
962 // confirm values are ok
963 if ( $match[0] > 255
964 || $match[1] > 255
965 || $match[3] > 255
966 || $match[4] > 255 ) {
967 // hmmm, what kind of dotted quad is this?
968 $param = '';
970 } elseif ( preg_match('/^[\w\d\.-]+$/', $param) // dots, hyphens, numbers
971 && !preg_match('/^[\.-]/', $param) // no leading dots/hyphens
972 && !preg_match('/[\.-]$/', $param) // no trailing dots/hyphens
974 // all is ok - $param is respected
975 } else {
976 // all is not ok...
977 $param='';
979 return $param;
981 case PARAM_URL: // allow safe ftp, http, mailto urls
982 $param = fix_utf8($param);
983 include_once($CFG->dirroot . '/lib/validateurlsyntax.php');
984 if (!empty($param) && validateUrlSyntax($param, 's?H?S?F?E?u-P-a?I?p?f?q?r?')) {
985 // all is ok, param is respected
986 } else {
987 $param =''; // not really ok
989 return $param;
991 case PARAM_LOCALURL: // allow http absolute, root relative and relative URLs within wwwroot
992 $param = clean_param($param, PARAM_URL);
993 if (!empty($param)) {
994 if (preg_match(':^/:', $param)) {
995 // root-relative, ok!
996 } elseif (preg_match('/^'.preg_quote($CFG->wwwroot, '/').'/i',$param)) {
997 // absolute, and matches our wwwroot
998 } else {
999 // relative - let's make sure there are no tricks
1000 if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?')) {
1001 // looks ok.
1002 } else {
1003 $param = '';
1007 return $param;
1009 case PARAM_PEM:
1010 $param = trim($param);
1011 // PEM formatted strings may contain letters/numbers and the symbols
1012 // forward slash: /
1013 // plus sign: +
1014 // equal sign: =
1015 // , surrounded by BEGIN and END CERTIFICATE prefix and suffixes
1016 if (preg_match('/^-----BEGIN CERTIFICATE-----([\s\w\/\+=]+)-----END CERTIFICATE-----$/', trim($param), $matches)) {
1017 list($wholething, $body) = $matches;
1018 unset($wholething, $matches);
1019 $b64 = clean_param($body, PARAM_BASE64);
1020 if (!empty($b64)) {
1021 return "-----BEGIN CERTIFICATE-----\n$b64\n-----END CERTIFICATE-----\n";
1022 } else {
1023 return '';
1026 return '';
1028 case PARAM_BASE64:
1029 if (!empty($param)) {
1030 // PEM formatted strings may contain letters/numbers and the symbols
1031 // forward slash: /
1032 // plus sign: +
1033 // equal sign: =
1034 if (0 >= preg_match('/^([\s\w\/\+=]+)$/', trim($param))) {
1035 return '';
1037 $lines = preg_split('/[\s]+/', $param, -1, PREG_SPLIT_NO_EMPTY);
1038 // Each line of base64 encoded data must be 64 characters in
1039 // length, except for the last line which may be less than (or
1040 // equal to) 64 characters long.
1041 for ($i=0, $j=count($lines); $i < $j; $i++) {
1042 if ($i + 1 == $j) {
1043 if (64 < strlen($lines[$i])) {
1044 return '';
1046 continue;
1049 if (64 != strlen($lines[$i])) {
1050 return '';
1053 return implode("\n",$lines);
1054 } else {
1055 return '';
1058 case PARAM_TAG:
1059 $param = fix_utf8($param);
1060 // Please note it is not safe to use the tag name directly anywhere,
1061 // it must be processed with s(), urlencode() before embedding anywhere.
1062 // remove some nasties
1063 $param = preg_replace('~[[:cntrl:]]|[<>`]~u', '', $param);
1064 //convert many whitespace chars into one
1065 $param = preg_replace('/\s+/', ' ', $param);
1066 $param = textlib::substr(trim($param), 0, TAG_MAX_LENGTH);
1067 return $param;
1069 case PARAM_TAGLIST:
1070 $param = fix_utf8($param);
1071 $tags = explode(',', $param);
1072 $result = array();
1073 foreach ($tags as $tag) {
1074 $res = clean_param($tag, PARAM_TAG);
1075 if ($res !== '') {
1076 $result[] = $res;
1079 if ($result) {
1080 return implode(',', $result);
1081 } else {
1082 return '';
1085 case PARAM_CAPABILITY:
1086 if (get_capability_info($param)) {
1087 return $param;
1088 } else {
1089 return '';
1092 case PARAM_PERMISSION:
1093 $param = (int)$param;
1094 if (in_array($param, array(CAP_INHERIT, CAP_ALLOW, CAP_PREVENT, CAP_PROHIBIT))) {
1095 return $param;
1096 } else {
1097 return CAP_INHERIT;
1100 case PARAM_AUTH:
1101 $param = clean_param($param, PARAM_PLUGIN);
1102 if (empty($param)) {
1103 return '';
1104 } else if (exists_auth_plugin($param)) {
1105 return $param;
1106 } else {
1107 return '';
1110 case PARAM_LANG:
1111 $param = clean_param($param, PARAM_SAFEDIR);
1112 if (get_string_manager()->translation_exists($param)) {
1113 return $param;
1114 } else {
1115 return ''; // Specified language is not installed or param malformed
1118 case PARAM_THEME:
1119 $param = clean_param($param, PARAM_PLUGIN);
1120 if (empty($param)) {
1121 return '';
1122 } else if (file_exists("$CFG->dirroot/theme/$param/config.php")) {
1123 return $param;
1124 } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$param/config.php")) {
1125 return $param;
1126 } else {
1127 return ''; // Specified theme is not installed
1130 case PARAM_USERNAME:
1131 $param = fix_utf8($param);
1132 $param = str_replace(" " , "", $param);
1133 $param = textlib::strtolower($param); // Convert uppercase to lowercase MDL-16919
1134 if (empty($CFG->extendedusernamechars)) {
1135 // regular expression, eliminate all chars EXCEPT:
1136 // alphanum, dash (-), underscore (_), at sign (@) and period (.) characters.
1137 $param = preg_replace('/[^-\.@_a-z0-9]/', '', $param);
1139 return $param;
1141 case PARAM_EMAIL:
1142 $param = fix_utf8($param);
1143 if (validate_email($param)) {
1144 return $param;
1145 } else {
1146 return '';
1149 case PARAM_STRINGID:
1150 if (preg_match('|^[a-zA-Z][a-zA-Z0-9\.:/_-]*$|', $param)) {
1151 return $param;
1152 } else {
1153 return '';
1156 case PARAM_TIMEZONE: //can be int, float(with .5 or .0) or string seperated by '/' and can have '-_'
1157 $param = fix_utf8($param);
1158 $timezonepattern = '/^(([+-]?(0?[0-9](\.[5|0])?|1[0-3](\.0)?|1[0-2]\.5))|(99)|[[:alnum:]]+(\/?[[:alpha:]_-])+)$/';
1159 if (preg_match($timezonepattern, $param)) {
1160 return $param;
1161 } else {
1162 return '';
1165 default: // throw error, switched parameters in optional_param or another serious problem
1166 print_error("unknownparamtype", '', '', $type);
1171 * Makes sure the data is using valid utf8, invalid characters are discarded.
1173 * Note: this function is not intended for full objects with methods and private properties.
1175 * @param mixed $value
1176 * @return mixed with proper utf-8 encoding
1178 function fix_utf8($value) {
1179 if (is_null($value) or $value === '') {
1180 return $value;
1182 } else if (is_string($value)) {
1183 if ((string)(int)$value === $value) {
1184 // shortcut
1185 return $value;
1188 // Lower error reporting because glibc throws bogus notices.
1189 $olderror = error_reporting();
1190 if ($olderror & E_NOTICE) {
1191 error_reporting($olderror ^ E_NOTICE);
1194 // Note: this duplicates min_fix_utf8() intentionally.
1195 static $buggyiconv = null;
1196 if ($buggyiconv === null) {
1197 $buggyiconv = (!function_exists('iconv') or iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€');
1200 if ($buggyiconv) {
1201 if (function_exists('mb_convert_encoding')) {
1202 $subst = mb_substitute_character();
1203 mb_substitute_character('');
1204 $result = mb_convert_encoding($value, 'utf-8', 'utf-8');
1205 mb_substitute_character($subst);
1207 } else {
1208 // Warn admins on admin/index.php page.
1209 $result = $value;
1212 } else {
1213 $result = iconv('UTF-8', 'UTF-8//IGNORE', $value);
1216 if ($olderror & E_NOTICE) {
1217 error_reporting($olderror);
1220 return $result;
1222 } else if (is_array($value)) {
1223 foreach ($value as $k=>$v) {
1224 $value[$k] = fix_utf8($v);
1226 return $value;
1228 } else if (is_object($value)) {
1229 $value = clone($value); // do not modify original
1230 foreach ($value as $k=>$v) {
1231 $value->$k = fix_utf8($v);
1233 return $value;
1235 } else {
1236 // this is some other type, no utf-8 here
1237 return $value;
1242 * Return true if given value is integer or string with integer value
1244 * @param mixed $value String or Int
1245 * @return bool true if number, false if not
1247 function is_number($value) {
1248 if (is_int($value)) {
1249 return true;
1250 } else if (is_string($value)) {
1251 return ((string)(int)$value) === $value;
1252 } else {
1253 return false;
1258 * Returns host part from url
1259 * @param string $url full url
1260 * @return string host, null if not found
1262 function get_host_from_url($url) {
1263 preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches);
1264 if ($matches) {
1265 return $matches[1];
1267 return null;
1271 * Tests whether anything was returned by text editor
1273 * This function is useful for testing whether something you got back from
1274 * the HTML editor actually contains anything. Sometimes the HTML editor
1275 * appear to be empty, but actually you get back a <br> tag or something.
1277 * @param string $string a string containing HTML.
1278 * @return boolean does the string contain any actual content - that is text,
1279 * images, objects, etc.
1281 function html_is_blank($string) {
1282 return trim(strip_tags($string, '<img><object><applet><input><select><textarea><hr>')) == '';
1286 * Set a key in global configuration
1288 * Set a key/value pair in both this session's {@link $CFG} global variable
1289 * and in the 'config' database table for future sessions.
1291 * Can also be used to update keys for plugin-scoped configs in config_plugin table.
1292 * In that case it doesn't affect $CFG.
1294 * A NULL value will delete the entry.
1296 * @global object
1297 * @global object
1298 * @param string $name the key to set
1299 * @param string $value the value to set (without magic quotes)
1300 * @param string $plugin (optional) the plugin scope, default NULL
1301 * @return bool true or exception
1303 function set_config($name, $value, $plugin=NULL) {
1304 global $CFG, $DB;
1306 if (empty($plugin)) {
1307 if (!array_key_exists($name, $CFG->config_php_settings)) {
1308 // So it's defined for this invocation at least
1309 if (is_null($value)) {
1310 unset($CFG->$name);
1311 } else {
1312 $CFG->$name = (string)$value; // settings from db are always strings
1316 if ($DB->get_field('config', 'name', array('name'=>$name))) {
1317 if ($value === null) {
1318 $DB->delete_records('config', array('name'=>$name));
1319 } else {
1320 $DB->set_field('config', 'value', $value, array('name'=>$name));
1322 } else {
1323 if ($value !== null) {
1324 $config = new stdClass();
1325 $config->name = $name;
1326 $config->value = $value;
1327 $DB->insert_record('config', $config, false);
1330 if ($name === 'siteidentifier') {
1331 cache_helper::update_site_identifier($value);
1333 cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1334 } else { // plugin scope
1335 if ($id = $DB->get_field('config_plugins', 'id', array('name'=>$name, 'plugin'=>$plugin))) {
1336 if ($value===null) {
1337 $DB->delete_records('config_plugins', array('name'=>$name, 'plugin'=>$plugin));
1338 } else {
1339 $DB->set_field('config_plugins', 'value', $value, array('id'=>$id));
1341 } else {
1342 if ($value !== null) {
1343 $config = new stdClass();
1344 $config->plugin = $plugin;
1345 $config->name = $name;
1346 $config->value = $value;
1347 $DB->insert_record('config_plugins', $config, false);
1350 cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1353 return true;
1357 * Get configuration values from the global config table
1358 * or the config_plugins table.
1360 * If called with one parameter, it will load all the config
1361 * variables for one plugin, and return them as an object.
1363 * If called with 2 parameters it will return a string single
1364 * value or false if the value is not found.
1366 * @static $siteidentifier The site identifier is not cached. We use this static cache so
1367 * that we need only fetch it once per request.
1368 * @param string $plugin full component name
1369 * @param string $name default NULL
1370 * @return mixed hash-like object or single value, return false no config found
1372 function get_config($plugin, $name = NULL) {
1373 global $CFG, $DB;
1375 static $siteidentifier = null;
1377 if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) {
1378 $forced =& $CFG->config_php_settings;
1379 $iscore = true;
1380 $plugin = 'core';
1381 } else {
1382 if (array_key_exists($plugin, $CFG->forced_plugin_settings)) {
1383 $forced =& $CFG->forced_plugin_settings[$plugin];
1384 } else {
1385 $forced = array();
1387 $iscore = false;
1390 if ($siteidentifier === null) {
1391 try {
1392 // This may fail during installation.
1393 // If you have a look at {@link initialise_cfg()} you will see that this is how we detect the need to
1394 // install the database.
1395 $siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
1396 } catch (dml_exception $ex) {
1397 // It's failed. We'll use this opportunity to disable cache stores so that we don't inadvertingly start using
1398 // old caches. People should delete their moodledata dirs when reinstalling the database... but they don't.
1399 cache_factory::disable_stores();
1400 // Set siteidentifier to false. We don't want to trip this continually.
1401 $siteidentifier = false;
1402 throw $ex;
1406 if (!empty($name)) {
1407 if (array_key_exists($name, $forced)) {
1408 return (string)$forced[$name];
1409 } else if ($name === 'siteidentifier' && $plugin == 'core') {
1410 return $siteidentifier;
1414 $cache = cache::make('core', 'config');
1415 $result = $cache->get($plugin);
1416 if ($result === false) {
1417 // the user is after a recordset
1418 $result = new stdClass;
1419 if (!$iscore) {
1420 $result = $DB->get_records_menu('config_plugins', array('plugin'=>$plugin), '', 'name,value');
1421 } else {
1422 // this part is not really used any more, but anyway...
1423 $result = $DB->get_records_menu('config', array(), '', 'name,value');;
1425 $cache->set($plugin, $result);
1428 if (!empty($name)) {
1429 if (array_key_exists($name, $result)) {
1430 return $result[$name];
1432 return false;
1435 if ($plugin === 'core') {
1436 $result['siteidentifier'] = $siteidentifier;
1439 foreach ($forced as $key => $value) {
1440 if (is_null($value) or is_array($value) or is_object($value)) {
1441 // we do not want any extra mess here, just real settings that could be saved in db
1442 unset($result[$key]);
1443 } else {
1444 //convert to string as if it went through the DB
1445 $result[$key] = (string)$value;
1449 return (object)$result;
1453 * Removes a key from global configuration
1455 * @param string $name the key to set
1456 * @param string $plugin (optional) the plugin scope
1457 * @global object
1458 * @return boolean whether the operation succeeded.
1460 function unset_config($name, $plugin=NULL) {
1461 global $CFG, $DB;
1463 if (empty($plugin)) {
1464 unset($CFG->$name);
1465 $DB->delete_records('config', array('name'=>$name));
1466 cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1467 } else {
1468 $DB->delete_records('config_plugins', array('name'=>$name, 'plugin'=>$plugin));
1469 cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1472 return true;
1476 * Remove all the config variables for a given plugin.
1478 * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice';
1479 * @return boolean whether the operation succeeded.
1481 function unset_all_config_for_plugin($plugin) {
1482 global $DB;
1483 // Delete from the obvious config_plugins first
1484 $DB->delete_records('config_plugins', array('plugin' => $plugin));
1485 // Next delete any suspect settings from config
1486 $like = $DB->sql_like('name', '?', true, true, false, '|');
1487 $params = array($DB->sql_like_escape($plugin.'_', '|') . '%');
1488 $DB->delete_records_select('config', $like, $params);
1489 // Finally clear both the plugin cache and the core cache (suspect settings now removed from core).
1490 cache_helper::invalidate_by_definition('core', 'config', array(), array('core', $plugin));
1492 return true;
1496 * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability.
1498 * All users are verified if they still have the necessary capability.
1500 * @param string $value the value of the config setting.
1501 * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor.
1502 * @param bool $include admins, include administrators
1503 * @return array of user objects.
1505 function get_users_from_config($value, $capability, $includeadmins = true) {
1506 global $CFG, $DB;
1508 if (empty($value) or $value === '$@NONE@$') {
1509 return array();
1512 // we have to make sure that users still have the necessary capability,
1513 // it should be faster to fetch them all first and then test if they are present
1514 // instead of validating them one-by-one
1515 $users = get_users_by_capability(context_system::instance(), $capability);
1516 if ($includeadmins) {
1517 $admins = get_admins();
1518 foreach ($admins as $admin) {
1519 $users[$admin->id] = $admin;
1523 if ($value === '$@ALL@$') {
1524 return $users;
1527 $result = array(); // result in correct order
1528 $allowed = explode(',', $value);
1529 foreach ($allowed as $uid) {
1530 if (isset($users[$uid])) {
1531 $user = $users[$uid];
1532 $result[$user->id] = $user;
1536 return $result;
1541 * Invalidates browser caches and cached data in temp
1543 * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
1544 * {@see phpunit_util::reset_dataroot()}
1546 * @return void
1548 function purge_all_caches() {
1549 global $CFG;
1551 reset_text_filters_cache();
1552 js_reset_all_caches();
1553 theme_reset_all_caches();
1554 get_string_manager()->reset_caches();
1555 textlib::reset_caches();
1557 cache_helper::purge_all();
1559 // purge all other caches: rss, simplepie, etc.
1560 remove_dir($CFG->cachedir.'', true);
1562 // make sure cache dir is writable, throws exception if not
1563 make_cache_directory('');
1565 // hack: this script may get called after the purifier was initialised,
1566 // but we do not want to verify repeatedly this exists in each call
1567 make_cache_directory('htmlpurifier');
1571 * Get volatile flags
1573 * @param string $type
1574 * @param int $changedsince default null
1575 * @return records array
1577 function get_cache_flags($type, $changedsince=NULL) {
1578 global $DB;
1580 $params = array('type'=>$type, 'expiry'=>time());
1581 $sqlwhere = "flagtype = :type AND expiry >= :expiry";
1582 if ($changedsince !== NULL) {
1583 $params['changedsince'] = $changedsince;
1584 $sqlwhere .= " AND timemodified > :changedsince";
1586 $cf = array();
1588 if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) {
1589 foreach ($flags as $flag) {
1590 $cf[$flag->name] = $flag->value;
1593 return $cf;
1597 * Get volatile flags
1599 * @param string $type
1600 * @param string $name
1601 * @param int $changedsince default null
1602 * @return records array
1604 function get_cache_flag($type, $name, $changedsince=NULL) {
1605 global $DB;
1607 $params = array('type'=>$type, 'name'=>$name, 'expiry'=>time());
1609 $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry";
1610 if ($changedsince !== NULL) {
1611 $params['changedsince'] = $changedsince;
1612 $sqlwhere .= " AND timemodified > :changedsince";
1615 return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params);
1619 * Set a volatile flag
1621 * @param string $type the "type" namespace for the key
1622 * @param string $name the key to set
1623 * @param string $value the value to set (without magic quotes) - NULL will remove the flag
1624 * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs
1625 * @return bool Always returns true
1627 function set_cache_flag($type, $name, $value, $expiry=NULL) {
1628 global $DB;
1630 $timemodified = time();
1631 if ($expiry===NULL || $expiry < $timemodified) {
1632 $expiry = $timemodified + 24 * 60 * 60;
1633 } else {
1634 $expiry = (int)$expiry;
1637 if ($value === NULL) {
1638 unset_cache_flag($type,$name);
1639 return true;
1642 if ($f = $DB->get_record('cache_flags', array('name'=>$name, 'flagtype'=>$type), '*', IGNORE_MULTIPLE)) { // this is a potential problem in DEBUG_DEVELOPER
1643 if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) {
1644 return true; //no need to update
1646 $f->value = $value;
1647 $f->expiry = $expiry;
1648 $f->timemodified = $timemodified;
1649 $DB->update_record('cache_flags', $f);
1650 } else {
1651 $f = new stdClass();
1652 $f->flagtype = $type;
1653 $f->name = $name;
1654 $f->value = $value;
1655 $f->expiry = $expiry;
1656 $f->timemodified = $timemodified;
1657 $DB->insert_record('cache_flags', $f);
1659 return true;
1663 * Removes a single volatile flag
1665 * @global object
1666 * @param string $type the "type" namespace for the key
1667 * @param string $name the key to set
1668 * @return bool
1670 function unset_cache_flag($type, $name) {
1671 global $DB;
1672 $DB->delete_records('cache_flags', array('name'=>$name, 'flagtype'=>$type));
1673 return true;
1677 * Garbage-collect volatile flags
1679 * @return bool Always returns true
1681 function gc_cache_flags() {
1682 global $DB;
1683 $DB->delete_records_select('cache_flags', 'expiry < ?', array(time()));
1684 return true;
1687 // USER PREFERENCE API
1690 * Refresh user preference cache. This is used most often for $USER
1691 * object that is stored in session, but it also helps with performance in cron script.
1693 * Preferences for each user are loaded on first use on every page, then again after the timeout expires.
1695 * @package core
1696 * @category preference
1697 * @access public
1698 * @param stdClass $user User object. Preferences are preloaded into 'preference' property
1699 * @param int $cachelifetime Cache life time on the current page (in seconds)
1700 * @throws coding_exception
1701 * @return null
1703 function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120) {
1704 global $DB;
1705 static $loadedusers = array(); // Static cache, we need to check on each page load, not only every 2 minutes.
1707 if (!isset($user->id)) {
1708 throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field');
1711 if (empty($user->id) or isguestuser($user->id)) {
1712 // No permanent storage for not-logged-in users and guest
1713 if (!isset($user->preference)) {
1714 $user->preference = array();
1716 return;
1719 $timenow = time();
1721 if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) {
1722 // Already loaded at least once on this page. Are we up to date?
1723 if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) {
1724 // no need to reload - we are on the same page and we loaded prefs just a moment ago
1725 return;
1727 } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) {
1728 // no change since the lastcheck on this page
1729 $user->preference['_lastloaded'] = $timenow;
1730 return;
1734 // OK, so we have to reload all preferences
1735 $loadedusers[$user->id] = true;
1736 $user->preference = $DB->get_records_menu('user_preferences', array('userid'=>$user->id), '', 'name,value'); // All values
1737 $user->preference['_lastloaded'] = $timenow;
1741 * Called from set/unset_user_preferences, so that the prefs can
1742 * be correctly reloaded in different sessions.
1744 * NOTE: internal function, do not call from other code.
1746 * @package core
1747 * @access private
1748 * @param integer $userid the user whose prefs were changed.
1750 function mark_user_preferences_changed($userid) {
1751 global $CFG;
1753 if (empty($userid) or isguestuser($userid)) {
1754 // no cache flags for guest and not-logged-in users
1755 return;
1758 set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout);
1762 * Sets a preference for the specified user.
1764 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1766 * @package core
1767 * @category preference
1768 * @access public
1769 * @param string $name The key to set as preference for the specified user
1770 * @param string $value The value to set for the $name key in the specified user's
1771 * record, null means delete current value.
1772 * @param stdClass|int|null $user A moodle user object or id, null means current user
1773 * @throws coding_exception
1774 * @return bool Always true or exception
1776 function set_user_preference($name, $value, $user = null) {
1777 global $USER, $DB;
1779 if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1780 throw new coding_exception('Invalid preference name in set_user_preference() call');
1783 if (is_null($value)) {
1784 // null means delete current
1785 return unset_user_preference($name, $user);
1786 } else if (is_object($value)) {
1787 throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed');
1788 } else if (is_array($value)) {
1789 throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed');
1791 $value = (string)$value;
1792 if (textlib::strlen($value) > 1333) { //value column maximum length is 1333 characters
1793 throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column');
1796 if (is_null($user)) {
1797 $user = $USER;
1798 } else if (isset($user->id)) {
1799 // $user is valid object
1800 } else if (is_numeric($user)) {
1801 $user = (object)array('id'=>(int)$user);
1802 } else {
1803 throw new coding_exception('Invalid $user parameter in set_user_preference() call');
1806 check_user_preferences_loaded($user);
1808 if (empty($user->id) or isguestuser($user->id)) {
1809 // no permanent storage for not-logged-in users and guest
1810 $user->preference[$name] = $value;
1811 return true;
1814 if ($preference = $DB->get_record('user_preferences', array('userid'=>$user->id, 'name'=>$name))) {
1815 if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) {
1816 // preference already set to this value
1817 return true;
1819 $DB->set_field('user_preferences', 'value', $value, array('id'=>$preference->id));
1821 } else {
1822 $preference = new stdClass();
1823 $preference->userid = $user->id;
1824 $preference->name = $name;
1825 $preference->value = $value;
1826 $DB->insert_record('user_preferences', $preference);
1829 // update value in cache
1830 $user->preference[$name] = $value;
1832 // set reload flag for other sessions
1833 mark_user_preferences_changed($user->id);
1835 return true;
1839 * Sets a whole array of preferences for the current user
1841 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1843 * @package core
1844 * @category preference
1845 * @access public
1846 * @param array $prefarray An array of key/value pairs to be set
1847 * @param stdClass|int|null $user A moodle user object or id, null means current user
1848 * @return bool Always true or exception
1850 function set_user_preferences(array $prefarray, $user = null) {
1851 foreach ($prefarray as $name => $value) {
1852 set_user_preference($name, $value, $user);
1854 return true;
1858 * Unsets a preference completely by deleting it from the database
1860 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1862 * @package core
1863 * @category preference
1864 * @access public
1865 * @param string $name The key to unset as preference for the specified user
1866 * @param stdClass|int|null $user A moodle user object or id, null means current user
1867 * @throws coding_exception
1868 * @return bool Always true or exception
1870 function unset_user_preference($name, $user = null) {
1871 global $USER, $DB;
1873 if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1874 throw new coding_exception('Invalid preference name in unset_user_preference() call');
1877 if (is_null($user)) {
1878 $user = $USER;
1879 } else if (isset($user->id)) {
1880 // $user is valid object
1881 } else if (is_numeric($user)) {
1882 $user = (object)array('id'=>(int)$user);
1883 } else {
1884 throw new coding_exception('Invalid $user parameter in unset_user_preference() call');
1887 check_user_preferences_loaded($user);
1889 if (empty($user->id) or isguestuser($user->id)) {
1890 // no permanent storage for not-logged-in user and guest
1891 unset($user->preference[$name]);
1892 return true;
1895 // delete from DB
1896 $DB->delete_records('user_preferences', array('userid'=>$user->id, 'name'=>$name));
1898 // delete the preference from cache
1899 unset($user->preference[$name]);
1901 // set reload flag for other sessions
1902 mark_user_preferences_changed($user->id);
1904 return true;
1908 * Used to fetch user preference(s)
1910 * If no arguments are supplied this function will return
1911 * all of the current user preferences as an array.
1913 * If a name is specified then this function
1914 * attempts to return that particular preference value. If
1915 * none is found, then the optional value $default is returned,
1916 * otherwise NULL.
1918 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1920 * @package core
1921 * @category preference
1922 * @access public
1923 * @param string $name Name of the key to use in finding a preference value
1924 * @param mixed|null $default Value to be returned if the $name key is not set in the user preferences
1925 * @param stdClass|int|null $user A moodle user object or id, null means current user
1926 * @throws coding_exception
1927 * @return string|mixed|null A string containing the value of a single preference. An
1928 * array with all of the preferences or null
1930 function get_user_preferences($name = null, $default = null, $user = null) {
1931 global $USER;
1933 if (is_null($name)) {
1934 // all prefs
1935 } else if (is_numeric($name) or $name === '_lastloaded') {
1936 throw new coding_exception('Invalid preference name in get_user_preferences() call');
1939 if (is_null($user)) {
1940 $user = $USER;
1941 } else if (isset($user->id)) {
1942 // $user is valid object
1943 } else if (is_numeric($user)) {
1944 $user = (object)array('id'=>(int)$user);
1945 } else {
1946 throw new coding_exception('Invalid $user parameter in get_user_preferences() call');
1949 check_user_preferences_loaded($user);
1951 if (empty($name)) {
1952 return $user->preference; // All values
1953 } else if (isset($user->preference[$name])) {
1954 return $user->preference[$name]; // The single string value
1955 } else {
1956 return $default; // Default value (null if not specified)
1960 /// FUNCTIONS FOR HANDLING TIME ////////////////////////////////////////////
1963 * Given date parts in user time produce a GMT timestamp.
1965 * @package core
1966 * @category time
1967 * @param int $year The year part to create timestamp of
1968 * @param int $month The month part to create timestamp of
1969 * @param int $day The day part to create timestamp of
1970 * @param int $hour The hour part to create timestamp of
1971 * @param int $minute The minute part to create timestamp of
1972 * @param int $second The second part to create timestamp of
1973 * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset.
1974 * if 99 then default user's timezone is used {@link http://docs.moodle.org/dev/Time_API#Timezone}
1975 * @param bool $applydst Toggle Daylight Saving Time, default true, will be
1976 * applied only if timezone is 99 or string.
1977 * @return int GMT timestamp
1979 function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) {
1981 //save input timezone, required for dst offset check.
1982 $passedtimezone = $timezone;
1984 $timezone = get_user_timezone_offset($timezone);
1986 if (abs($timezone) > 13) { //server time
1987 $time = mktime((int)$hour, (int)$minute, (int)$second, (int)$month, (int)$day, (int)$year);
1988 } else {
1989 $time = gmmktime((int)$hour, (int)$minute, (int)$second, (int)$month, (int)$day, (int)$year);
1990 $time = usertime($time, $timezone);
1992 //Apply dst for string timezones or if 99 then try dst offset with user's default timezone
1993 if ($applydst && ((99 == $passedtimezone) || !is_numeric($passedtimezone))) {
1994 $time -= dst_offset_on($time, $passedtimezone);
1998 return $time;
2003 * Format a date/time (seconds) as weeks, days, hours etc as needed
2005 * Given an amount of time in seconds, returns string
2006 * formatted nicely as weeks, days, hours etc as needed
2008 * @package core
2009 * @category time
2010 * @uses MINSECS
2011 * @uses HOURSECS
2012 * @uses DAYSECS
2013 * @uses YEARSECS
2014 * @param int $totalsecs Time in seconds
2015 * @param object $str Should be a time object
2016 * @return string A nicely formatted date/time string
2018 function format_time($totalsecs, $str=NULL) {
2020 $totalsecs = abs($totalsecs);
2022 if (!$str) { // Create the str structure the slow way
2023 $str = new stdClass();
2024 $str->day = get_string('day');
2025 $str->days = get_string('days');
2026 $str->hour = get_string('hour');
2027 $str->hours = get_string('hours');
2028 $str->min = get_string('min');
2029 $str->mins = get_string('mins');
2030 $str->sec = get_string('sec');
2031 $str->secs = get_string('secs');
2032 $str->year = get_string('year');
2033 $str->years = get_string('years');
2037 $years = floor($totalsecs/YEARSECS);
2038 $remainder = $totalsecs - ($years*YEARSECS);
2039 $days = floor($remainder/DAYSECS);
2040 $remainder = $totalsecs - ($days*DAYSECS);
2041 $hours = floor($remainder/HOURSECS);
2042 $remainder = $remainder - ($hours*HOURSECS);
2043 $mins = floor($remainder/MINSECS);
2044 $secs = $remainder - ($mins*MINSECS);
2046 $ss = ($secs == 1) ? $str->sec : $str->secs;
2047 $sm = ($mins == 1) ? $str->min : $str->mins;
2048 $sh = ($hours == 1) ? $str->hour : $str->hours;
2049 $sd = ($days == 1) ? $str->day : $str->days;
2050 $sy = ($years == 1) ? $str->year : $str->years;
2052 $oyears = '';
2053 $odays = '';
2054 $ohours = '';
2055 $omins = '';
2056 $osecs = '';
2058 if ($years) $oyears = $years .' '. $sy;
2059 if ($days) $odays = $days .' '. $sd;
2060 if ($hours) $ohours = $hours .' '. $sh;
2061 if ($mins) $omins = $mins .' '. $sm;
2062 if ($secs) $osecs = $secs .' '. $ss;
2064 if ($years) return trim($oyears .' '. $odays);
2065 if ($days) return trim($odays .' '. $ohours);
2066 if ($hours) return trim($ohours .' '. $omins);
2067 if ($mins) return trim($omins .' '. $osecs);
2068 if ($secs) return $osecs;
2069 return get_string('now');
2073 * Returns a formatted string that represents a date in user time
2075 * Returns a formatted string that represents a date in user time
2076 * <b>WARNING: note that the format is for strftime(), not date().</b>
2077 * Because of a bug in most Windows time libraries, we can't use
2078 * the nicer %e, so we have to use %d which has leading zeroes.
2079 * A lot of the fuss in the function is just getting rid of these leading
2080 * zeroes as efficiently as possible.
2082 * If parameter fixday = true (default), then take off leading
2083 * zero from %d, else maintain it.
2085 * @package core
2086 * @category time
2087 * @param int $date the timestamp in UTC, as obtained from the database.
2088 * @param string $format strftime format. You should probably get this using
2089 * get_string('strftime...', 'langconfig');
2090 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
2091 * not 99 then daylight saving will not be added.
2092 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2093 * @param bool $fixday If true (default) then the leading zero from %d is removed.
2094 * If false then the leading zero is maintained.
2095 * @param bool $fixhour If true (default) then the leading zero from %I is removed.
2096 * @return string the formatted date/time.
2098 function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
2100 global $CFG;
2102 if (empty($format)) {
2103 $format = get_string('strftimedaydatetime', 'langconfig');
2106 if (!empty($CFG->nofixday)) { // Config.php can force %d not to be fixed.
2107 $fixday = false;
2108 } else if ($fixday) {
2109 $formatnoday = str_replace('%d', 'DD', $format);
2110 $fixday = ($formatnoday != $format);
2111 $format = $formatnoday;
2114 // Note: This logic about fixing 12-hour time to remove unnecessary leading
2115 // zero is required because on Windows, PHP strftime function does not
2116 // support the correct 'hour without leading zero' parameter (%l).
2117 if (!empty($CFG->nofixhour)) {
2118 // Config.php can force %I not to be fixed.
2119 $fixhour = false;
2120 } else if ($fixhour) {
2121 $formatnohour = str_replace('%I', 'HH', $format);
2122 $fixhour = ($formatnohour != $format);
2123 $format = $formatnohour;
2126 //add daylight saving offset for string timezones only, as we can't get dst for
2127 //float values. if timezone is 99 (user default timezone), then try update dst.
2128 if ((99 == $timezone) || !is_numeric($timezone)) {
2129 $date += dst_offset_on($date, $timezone);
2132 $timezone = get_user_timezone_offset($timezone);
2134 // If we are running under Windows convert to windows encoding and then back to UTF-8
2135 // (because it's impossible to specify UTF-8 to fetch locale info in Win32)
2137 if (abs($timezone) > 13) { /// Server time
2138 $datestring = date_format_string($date, $format, $timezone);
2139 if ($fixday) {
2140 $daystring = ltrim(str_replace(array(' 0', ' '), '', strftime(' %d', $date)));
2141 $datestring = str_replace('DD', $daystring, $datestring);
2143 if ($fixhour) {
2144 $hourstring = ltrim(str_replace(array(' 0', ' '), '', strftime(' %I', $date)));
2145 $datestring = str_replace('HH', $hourstring, $datestring);
2148 } else {
2149 $date += (int)($timezone * 3600);
2150 $datestring = date_format_string($date, $format, $timezone);
2151 if ($fixday) {
2152 $daystring = ltrim(str_replace(array(' 0', ' '), '', gmstrftime(' %d', $date)));
2153 $datestring = str_replace('DD', $daystring, $datestring);
2155 if ($fixhour) {
2156 $hourstring = ltrim(str_replace(array(' 0', ' '), '', gmstrftime(' %I', $date)));
2157 $datestring = str_replace('HH', $hourstring, $datestring);
2161 return $datestring;
2165 * Returns a formatted date ensuring it is UTF-8.
2167 * If we are running under Windows convert to Windows encoding and then back to UTF-8
2168 * (because it's impossible to specify UTF-8 to fetch locale info in Win32).
2170 * This function does not do any calculation regarding the user preferences and should
2171 * therefore receive the final date timestamp, format and timezone. Timezone being only used
2172 * to differenciate the use of server time or not (strftime() against gmstrftime()).
2174 * @param int $date the timestamp.
2175 * @param string $format strftime format.
2176 * @param int|float $timezone the numerical timezone, typically returned by {@link get_user_timezone_offset()}.
2177 * @return string the formatted date/time.
2178 * @since 2.3.3
2180 function date_format_string($date, $format, $tz = 99) {
2181 global $CFG;
2182 if (abs($tz) > 13) {
2183 if ($CFG->ostype == 'WINDOWS' and $localewincharset = get_string('localewincharset', 'langconfig')) {
2184 $format = textlib::convert($format, 'utf-8', $localewincharset);
2185 $datestring = strftime($format, $date);
2186 $datestring = textlib::convert($datestring, $localewincharset, 'utf-8');
2187 } else {
2188 $datestring = strftime($format, $date);
2190 } else {
2191 if ($CFG->ostype == 'WINDOWS' and $localewincharset = get_string('localewincharset', 'langconfig')) {
2192 $format = textlib::convert($format, 'utf-8', $localewincharset);
2193 $datestring = gmstrftime($format, $date);
2194 $datestring = textlib::convert($datestring, $localewincharset, 'utf-8');
2195 } else {
2196 $datestring = gmstrftime($format, $date);
2199 return $datestring;
2203 * Given a $time timestamp in GMT (seconds since epoch),
2204 * returns an array that represents the date in user time
2206 * @package core
2207 * @category time
2208 * @uses HOURSECS
2209 * @param int $time Timestamp in GMT
2210 * @param float|int|string $timezone offset's time with timezone, if float and not 99, then no
2211 * dst offset is applyed {@link http://docs.moodle.org/dev/Time_API#Timezone}
2212 * @return array An array that represents the date in user time
2214 function usergetdate($time, $timezone=99) {
2216 //save input timezone, required for dst offset check.
2217 $passedtimezone = $timezone;
2219 $timezone = get_user_timezone_offset($timezone);
2221 if (abs($timezone) > 13) { // Server time
2222 return getdate($time);
2225 //add daylight saving offset for string timezones only, as we can't get dst for
2226 //float values. if timezone is 99 (user default timezone), then try update dst.
2227 if ($passedtimezone == 99 || !is_numeric($passedtimezone)) {
2228 $time += dst_offset_on($time, $passedtimezone);
2231 $time += intval((float)$timezone * HOURSECS);
2233 $datestring = gmstrftime('%B_%A_%j_%Y_%m_%w_%d_%H_%M_%S', $time);
2235 //be careful to ensure the returned array matches that produced by getdate() above
2236 list(
2237 $getdate['month'],
2238 $getdate['weekday'],
2239 $getdate['yday'],
2240 $getdate['year'],
2241 $getdate['mon'],
2242 $getdate['wday'],
2243 $getdate['mday'],
2244 $getdate['hours'],
2245 $getdate['minutes'],
2246 $getdate['seconds']
2247 ) = explode('_', $datestring);
2249 // set correct datatype to match with getdate()
2250 $getdate['seconds'] = (int)$getdate['seconds'];
2251 $getdate['yday'] = (int)$getdate['yday'] - 1; // gettime returns 0 through 365
2252 $getdate['year'] = (int)$getdate['year'];
2253 $getdate['mon'] = (int)$getdate['mon'];
2254 $getdate['wday'] = (int)$getdate['wday'];
2255 $getdate['mday'] = (int)$getdate['mday'];
2256 $getdate['hours'] = (int)$getdate['hours'];
2257 $getdate['minutes'] = (int)$getdate['minutes'];
2258 return $getdate;
2262 * Given a GMT timestamp (seconds since epoch), offsets it by
2263 * the timezone. eg 3pm in India is 3pm GMT - 7 * 3600 seconds
2265 * @package core
2266 * @category time
2267 * @uses HOURSECS
2268 * @param int $date Timestamp in GMT
2269 * @param float|int|string $timezone timezone to calculate GMT time offset before
2270 * calculating user time, 99 is default user timezone
2271 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2272 * @return int
2274 function usertime($date, $timezone=99) {
2276 $timezone = get_user_timezone_offset($timezone);
2278 if (abs($timezone) > 13) {
2279 return $date;
2281 return $date - (int)($timezone * HOURSECS);
2285 * Given a time, return the GMT timestamp of the most recent midnight
2286 * for the current user.
2288 * @package core
2289 * @category time
2290 * @param int $date Timestamp in GMT
2291 * @param float|int|string $timezone timezone to calculate GMT time offset before
2292 * calculating user midnight time, 99 is default user timezone
2293 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2294 * @return int Returns a GMT timestamp
2296 function usergetmidnight($date, $timezone=99) {
2298 $userdate = usergetdate($date, $timezone);
2300 // Time of midnight of this user's day, in GMT
2301 return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone);
2306 * Returns a string that prints the user's timezone
2308 * @package core
2309 * @category time
2310 * @param float|int|string $timezone timezone to calculate GMT time offset before
2311 * calculating user timezone, 99 is default user timezone
2312 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2313 * @return string
2315 function usertimezone($timezone=99) {
2317 $tz = get_user_timezone($timezone);
2319 if (!is_float($tz)) {
2320 return $tz;
2323 if(abs($tz) > 13) { // Server time
2324 return get_string('serverlocaltime');
2327 if($tz == intval($tz)) {
2328 // Don't show .0 for whole hours
2329 $tz = intval($tz);
2332 if($tz == 0) {
2333 return 'UTC';
2335 else if($tz > 0) {
2336 return 'UTC+'.$tz;
2338 else {
2339 return 'UTC'.$tz;
2345 * Returns a float which represents the user's timezone difference from GMT in hours
2346 * Checks various settings and picks the most dominant of those which have a value
2348 * @package core
2349 * @category time
2350 * @param float|int|string $tz timezone to calculate GMT time offset for user,
2351 * 99 is default user timezone
2352 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2353 * @return float
2355 function get_user_timezone_offset($tz = 99) {
2357 global $USER, $CFG;
2359 $tz = get_user_timezone($tz);
2361 if (is_float($tz)) {
2362 return $tz;
2363 } else {
2364 $tzrecord = get_timezone_record($tz);
2365 if (empty($tzrecord)) {
2366 return 99.0;
2368 return (float)$tzrecord->gmtoff / HOURMINS;
2373 * Returns an int which represents the systems's timezone difference from GMT in seconds
2375 * @package core
2376 * @category time
2377 * @param float|int|string $tz timezone for which offset is required.
2378 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2379 * @return int|bool if found, false is timezone 99 or error
2381 function get_timezone_offset($tz) {
2382 global $CFG;
2384 if ($tz == 99) {
2385 return false;
2388 if (is_numeric($tz)) {
2389 return intval($tz * 60*60);
2392 if (!$tzrecord = get_timezone_record($tz)) {
2393 return false;
2395 return intval($tzrecord->gmtoff * 60);
2399 * Returns a float or a string which denotes the user's timezone
2400 * 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)
2401 * means that for this timezone there are also DST rules to be taken into account
2402 * Checks various settings and picks the most dominant of those which have a value
2404 * @package core
2405 * @category time
2406 * @param float|int|string $tz timezone to calculate GMT time offset before
2407 * calculating user timezone, 99 is default user timezone
2408 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2409 * @return float|string
2411 function get_user_timezone($tz = 99) {
2412 global $USER, $CFG;
2414 $timezones = array(
2415 $tz,
2416 isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99,
2417 isset($USER->timezone) ? $USER->timezone : 99,
2418 isset($CFG->timezone) ? $CFG->timezone : 99,
2421 $tz = 99;
2423 // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array
2424 while(((empty($tz) && !is_numeric($tz)) || $tz == 99) && $next = each($timezones)) {
2425 $tz = $next['value'];
2427 return is_numeric($tz) ? (float) $tz : $tz;
2431 * Returns cached timezone record for given $timezonename
2433 * @package core
2434 * @param string $timezonename name of the timezone
2435 * @return stdClass|bool timezonerecord or false
2437 function get_timezone_record($timezonename) {
2438 global $CFG, $DB;
2439 static $cache = NULL;
2441 if ($cache === NULL) {
2442 $cache = array();
2445 if (isset($cache[$timezonename])) {
2446 return $cache[$timezonename];
2449 return $cache[$timezonename] = $DB->get_record_sql('SELECT * FROM {timezone}
2450 WHERE name = ? ORDER BY year DESC', array($timezonename), IGNORE_MULTIPLE);
2454 * Build and store the users Daylight Saving Time (DST) table
2456 * @package core
2457 * @param int $from_year Start year for the table, defaults to 1971
2458 * @param int $to_year End year for the table, defaults to 2035
2459 * @param int|float|string $strtimezone, timezone to check if dst should be applyed.
2460 * @return bool
2462 function calculate_user_dst_table($from_year = NULL, $to_year = NULL, $strtimezone = NULL) {
2463 global $CFG, $SESSION, $DB;
2465 $usertz = get_user_timezone($strtimezone);
2467 if (is_float($usertz)) {
2468 // Trivial timezone, no DST
2469 return false;
2472 if (!empty($SESSION->dst_offsettz) && $SESSION->dst_offsettz != $usertz) {
2473 // We have precalculated values, but the user's effective TZ has changed in the meantime, so reset
2474 unset($SESSION->dst_offsets);
2475 unset($SESSION->dst_range);
2478 if (!empty($SESSION->dst_offsets) && empty($from_year) && empty($to_year)) {
2479 // Repeat calls which do not request specific year ranges stop here, we have already calculated the table
2480 // This will be the return path most of the time, pretty light computationally
2481 return true;
2484 // Reaching here means we either need to extend our table or create it from scratch
2486 // Remember which TZ we calculated these changes for
2487 $SESSION->dst_offsettz = $usertz;
2489 if(empty($SESSION->dst_offsets)) {
2490 // If we 're creating from scratch, put the two guard elements in there
2491 $SESSION->dst_offsets = array(1 => NULL, 0 => NULL);
2493 if(empty($SESSION->dst_range)) {
2494 // If creating from scratch
2495 $from = max((empty($from_year) ? intval(date('Y')) - 3 : $from_year), 1971);
2496 $to = min((empty($to_year) ? intval(date('Y')) + 3 : $to_year), 2035);
2498 // Fill in the array with the extra years we need to process
2499 $yearstoprocess = array();
2500 for($i = $from; $i <= $to; ++$i) {
2501 $yearstoprocess[] = $i;
2504 // Take note of which years we have processed for future calls
2505 $SESSION->dst_range = array($from, $to);
2507 else {
2508 // If needing to extend the table, do the same
2509 $yearstoprocess = array();
2511 $from = max((empty($from_year) ? $SESSION->dst_range[0] : $from_year), 1971);
2512 $to = min((empty($to_year) ? $SESSION->dst_range[1] : $to_year), 2035);
2514 if($from < $SESSION->dst_range[0]) {
2515 // Take note of which years we need to process and then note that we have processed them for future calls
2516 for($i = $from; $i < $SESSION->dst_range[0]; ++$i) {
2517 $yearstoprocess[] = $i;
2519 $SESSION->dst_range[0] = $from;
2521 if($to > $SESSION->dst_range[1]) {
2522 // Take note of which years we need to process and then note that we have processed them for future calls
2523 for($i = $SESSION->dst_range[1] + 1; $i <= $to; ++$i) {
2524 $yearstoprocess[] = $i;
2526 $SESSION->dst_range[1] = $to;
2530 if(empty($yearstoprocess)) {
2531 // This means that there was a call requesting a SMALLER range than we have already calculated
2532 return true;
2535 // From now on, we know that the array has at least the two guard elements, and $yearstoprocess has the years we need
2536 // Also, the array is sorted in descending timestamp order!
2538 // Get DB data
2540 static $presets_cache = array();
2541 if (!isset($presets_cache[$usertz])) {
2542 $presets_cache[$usertz] = $DB->get_records('timezone', array('name'=>$usertz), 'year DESC', 'year, gmtoff, dstoff, dst_month, dst_startday, dst_weekday, dst_skipweeks, dst_time, std_month, std_startday, std_weekday, std_skipweeks, std_time');
2544 if(empty($presets_cache[$usertz])) {
2545 return false;
2548 // Remove ending guard (first element of the array)
2549 reset($SESSION->dst_offsets);
2550 unset($SESSION->dst_offsets[key($SESSION->dst_offsets)]);
2552 // Add all required change timestamps
2553 foreach($yearstoprocess as $y) {
2554 // Find the record which is in effect for the year $y
2555 foreach($presets_cache[$usertz] as $year => $preset) {
2556 if($year <= $y) {
2557 break;
2561 $changes = dst_changes_for_year($y, $preset);
2563 if($changes === NULL) {
2564 continue;
2566 if($changes['dst'] != 0) {
2567 $SESSION->dst_offsets[$changes['dst']] = $preset->dstoff * MINSECS;
2569 if($changes['std'] != 0) {
2570 $SESSION->dst_offsets[$changes['std']] = 0;
2574 // Put in a guard element at the top
2575 $maxtimestamp = max(array_keys($SESSION->dst_offsets));
2576 $SESSION->dst_offsets[($maxtimestamp + DAYSECS)] = NULL; // DAYSECS is arbitrary, any "small" number will do
2578 // Sort again
2579 krsort($SESSION->dst_offsets);
2581 return true;
2585 * Calculates the required DST change and returns a Timestamp Array
2587 * @package core
2588 * @category time
2589 * @uses HOURSECS
2590 * @uses MINSECS
2591 * @param int|string $year Int or String Year to focus on
2592 * @param object $timezone Instatiated Timezone object
2593 * @return array|null Array dst=>xx, 0=>xx, std=>yy, 1=>yy or NULL
2595 function dst_changes_for_year($year, $timezone) {
2597 if($timezone->dst_startday == 0 && $timezone->dst_weekday == 0 && $timezone->std_startday == 0 && $timezone->std_weekday == 0) {
2598 return NULL;
2601 $monthdaydst = find_day_in_month($timezone->dst_startday, $timezone->dst_weekday, $timezone->dst_month, $year);
2602 $monthdaystd = find_day_in_month($timezone->std_startday, $timezone->std_weekday, $timezone->std_month, $year);
2604 list($dst_hour, $dst_min) = explode(':', $timezone->dst_time);
2605 list($std_hour, $std_min) = explode(':', $timezone->std_time);
2607 $timedst = make_timestamp($year, $timezone->dst_month, $monthdaydst, 0, 0, 0, 99, false);
2608 $timestd = make_timestamp($year, $timezone->std_month, $monthdaystd, 0, 0, 0, 99, false);
2610 // Instead of putting hour and minute in make_timestamp(), we add them afterwards.
2611 // This has the advantage of being able to have negative values for hour, i.e. for timezones
2612 // where GMT time would be in the PREVIOUS day than the local one on which DST changes.
2614 $timedst += $dst_hour * HOURSECS + $dst_min * MINSECS;
2615 $timestd += $std_hour * HOURSECS + $std_min * MINSECS;
2617 return array('dst' => $timedst, 0 => $timedst, 'std' => $timestd, 1 => $timestd);
2621 * Calculates the Daylight Saving Offset for a given date/time (timestamp)
2622 * - Note: Daylight saving only works for string timezones and not for float.
2624 * @package core
2625 * @category time
2626 * @param int $time must NOT be compensated at all, it has to be a pure timestamp
2627 * @param int|float|string $strtimezone timezone for which offset is expected, if 99 or null
2628 * then user's default timezone is used. {@link http://docs.moodle.org/dev/Time_API#Timezone}
2629 * @return int
2631 function dst_offset_on($time, $strtimezone = NULL) {
2632 global $SESSION;
2634 if(!calculate_user_dst_table(NULL, NULL, $strtimezone) || empty($SESSION->dst_offsets)) {
2635 return 0;
2638 reset($SESSION->dst_offsets);
2639 while(list($from, $offset) = each($SESSION->dst_offsets)) {
2640 if($from <= $time) {
2641 break;
2645 // This is the normal return path
2646 if($offset !== NULL) {
2647 return $offset;
2650 // Reaching this point means we haven't calculated far enough, do it now:
2651 // Calculate extra DST changes if needed and recurse. The recursion always
2652 // moves toward the stopping condition, so will always end.
2654 if($from == 0) {
2655 // We need a year smaller than $SESSION->dst_range[0]
2656 if($SESSION->dst_range[0] == 1971) {
2657 return 0;
2659 calculate_user_dst_table($SESSION->dst_range[0] - 5, NULL, $strtimezone);
2660 return dst_offset_on($time, $strtimezone);
2662 else {
2663 // We need a year larger than $SESSION->dst_range[1]
2664 if($SESSION->dst_range[1] == 2035) {
2665 return 0;
2667 calculate_user_dst_table(NULL, $SESSION->dst_range[1] + 5, $strtimezone);
2668 return dst_offset_on($time, $strtimezone);
2673 * Calculates when the day appears in specific month
2675 * @package core
2676 * @category time
2677 * @param int $startday starting day of the month
2678 * @param int $weekday The day when week starts (normally taken from user preferences)
2679 * @param int $month The month whose day is sought
2680 * @param int $year The year of the month whose day is sought
2681 * @return int
2683 function find_day_in_month($startday, $weekday, $month, $year) {
2685 $daysinmonth = days_in_month($month, $year);
2687 if($weekday == -1) {
2688 // Don't care about weekday, so return:
2689 // abs($startday) if $startday != -1
2690 // $daysinmonth otherwise
2691 return ($startday == -1) ? $daysinmonth : abs($startday);
2694 // From now on we 're looking for a specific weekday
2696 // Give "end of month" its actual value, since we know it
2697 if($startday == -1) {
2698 $startday = -1 * $daysinmonth;
2701 // Starting from day $startday, the sign is the direction
2703 if($startday < 1) {
2705 $startday = abs($startday);
2706 $lastmonthweekday = strftime('%w', mktime(12, 0, 0, $month, $daysinmonth, $year));
2708 // This is the last such weekday of the month
2709 $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday;
2710 if($lastinmonth > $daysinmonth) {
2711 $lastinmonth -= 7;
2714 // Find the first such weekday <= $startday
2715 while($lastinmonth > $startday) {
2716 $lastinmonth -= 7;
2719 return $lastinmonth;
2722 else {
2724 $indexweekday = strftime('%w', mktime(12, 0, 0, $month, $startday, $year));
2726 $diff = $weekday - $indexweekday;
2727 if($diff < 0) {
2728 $diff += 7;
2731 // This is the first such weekday of the month equal to or after $startday
2732 $firstfromindex = $startday + $diff;
2734 return $firstfromindex;
2740 * Calculate the number of days in a given month
2742 * @package core
2743 * @category time
2744 * @param int $month The month whose day count is sought
2745 * @param int $year The year of the month whose day count is sought
2746 * @return int
2748 function days_in_month($month, $year) {
2749 return intval(date('t', mktime(12, 0, 0, $month, 1, $year)));
2753 * Calculate the position in the week of a specific calendar day
2755 * @package core
2756 * @category time
2757 * @param int $day The day of the date whose position in the week is sought
2758 * @param int $month The month of the date whose position in the week is sought
2759 * @param int $year The year of the date whose position in the week is sought
2760 * @return int
2762 function dayofweek($day, $month, $year) {
2763 // I wonder if this is any different from
2764 // strftime('%w', mktime(12, 0, 0, $month, $daysinmonth, $year, 0));
2765 return intval(date('w', mktime(12, 0, 0, $month, $day, $year)));
2768 /// USER AUTHENTICATION AND LOGIN ////////////////////////////////////////
2771 * Returns full login url.
2773 * @return string login url
2775 function get_login_url() {
2776 global $CFG;
2778 $url = "$CFG->wwwroot/login/index.php";
2780 if (!empty($CFG->loginhttps)) {
2781 $url = str_replace('http:', 'https:', $url);
2784 return $url;
2788 * This function checks that the current user is logged in and has the
2789 * required privileges
2791 * This function checks that the current user is logged in, and optionally
2792 * whether they are allowed to be in a particular course and view a particular
2793 * course module.
2794 * If they are not logged in, then it redirects them to the site login unless
2795 * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which
2796 * case they are automatically logged in as guests.
2797 * If $courseid is given and the user is not enrolled in that course then the
2798 * user is redirected to the course enrolment page.
2799 * If $cm is given and the course module is hidden and the user is not a teacher
2800 * in the course then the user is redirected to the course home page.
2802 * When $cm parameter specified, this function sets page layout to 'module'.
2803 * You need to change it manually later if some other layout needed.
2805 * @package core_access
2806 * @category access
2808 * @param mixed $courseorid id of the course or course object
2809 * @param bool $autologinguest default true
2810 * @param object $cm course module object
2811 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2812 * true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2813 * in order to keep redirects working properly. MDL-14495
2814 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2815 * @return mixed Void, exit, and die depending on path
2817 function require_login($courseorid = NULL, $autologinguest = true, $cm = NULL, $setwantsurltome = true, $preventredirect = false) {
2818 global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT;
2820 // Must not redirect when byteserving already started.
2821 if (!empty($_SERVER['HTTP_RANGE'])) {
2822 $preventredirect = true;
2825 // setup global $COURSE, themes, language and locale
2826 if (!empty($courseorid)) {
2827 if (is_object($courseorid)) {
2828 $course = $courseorid;
2829 } else if ($courseorid == SITEID) {
2830 $course = clone($SITE);
2831 } else {
2832 $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST);
2834 if ($cm) {
2835 if ($cm->course != $course->id) {
2836 throw new coding_exception('course and cm parameters in require_login() call do not match!!');
2838 // make sure we have a $cm from get_fast_modinfo as this contains activity access details
2839 if (!($cm instanceof cm_info)) {
2840 // note: nearly all pages call get_fast_modinfo anyway and it does not make any
2841 // db queries so this is not really a performance concern, however it is obviously
2842 // better if you use get_fast_modinfo to get the cm before calling this.
2843 $modinfo = get_fast_modinfo($course);
2844 $cm = $modinfo->get_cm($cm->id);
2846 $PAGE->set_cm($cm, $course); // set's up global $COURSE
2847 $PAGE->set_pagelayout('incourse');
2848 } else {
2849 $PAGE->set_course($course); // set's up global $COURSE
2851 } else {
2852 // do not touch global $COURSE via $PAGE->set_course(),
2853 // the reasons is we need to be able to call require_login() at any time!!
2854 $course = $SITE;
2855 if ($cm) {
2856 throw new coding_exception('cm parameter in require_login() requires valid course parameter!');
2860 // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false.
2861 // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future
2862 // risk leading the user back to the AJAX request URL.
2863 if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) {
2864 $setwantsurltome = false;
2867 // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour.
2868 if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !$preventredirect && !empty($CFG->dbsessions)) {
2869 if ($setwantsurltome) {
2870 $SESSION->wantsurl = qualified_me();
2872 redirect(get_login_url());
2875 // If the user is not even logged in yet then make sure they are
2876 if (!isloggedin()) {
2877 if ($autologinguest and !empty($CFG->guestloginbutton) and !empty($CFG->autologinguests)) {
2878 if (!$guest = get_complete_user_data('id', $CFG->siteguest)) {
2879 // misconfigured site guest, just redirect to login page
2880 redirect(get_login_url());
2881 exit; // never reached
2883 $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang;
2884 complete_user_login($guest);
2885 $USER->autologinguest = true;
2886 $SESSION->lang = $lang;
2887 } else {
2888 //NOTE: $USER->site check was obsoleted by session test cookie,
2889 // $USER->confirmed test is in login/index.php
2890 if ($preventredirect) {
2891 throw new require_login_exception('You are not logged in');
2894 if ($setwantsurltome) {
2895 $SESSION->wantsurl = qualified_me();
2897 if (!empty($_SERVER['HTTP_REFERER'])) {
2898 $SESSION->fromurl = $_SERVER['HTTP_REFERER'];
2900 redirect(get_login_url());
2901 exit; // never reached
2905 // loginas as redirection if needed
2906 if ($course->id != SITEID and session_is_loggedinas()) {
2907 if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) {
2908 if ($USER->loginascontext->instanceid != $course->id) {
2909 print_error('loginasonecourse', '', $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid);
2914 // check whether the user should be changing password (but only if it is REALLY them)
2915 if (get_user_preferences('auth_forcepasswordchange') && !session_is_loggedinas()) {
2916 $userauth = get_auth_plugin($USER->auth);
2917 if ($userauth->can_change_password() and !$preventredirect) {
2918 if ($setwantsurltome) {
2919 $SESSION->wantsurl = qualified_me();
2921 if ($changeurl = $userauth->change_password_url()) {
2922 //use plugin custom url
2923 redirect($changeurl);
2924 } else {
2925 //use moodle internal method
2926 if (empty($CFG->loginhttps)) {
2927 redirect($CFG->wwwroot .'/login/change_password.php');
2928 } else {
2929 $wwwroot = str_replace('http:','https:', $CFG->wwwroot);
2930 redirect($wwwroot .'/login/change_password.php');
2933 } else {
2934 print_error('nopasswordchangeforced', 'auth');
2938 // Check that the user account is properly set up
2939 if (user_not_fully_set_up($USER)) {
2940 if ($preventredirect) {
2941 throw new require_login_exception('User not fully set-up');
2943 if ($setwantsurltome) {
2944 $SESSION->wantsurl = qualified_me();
2946 redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&amp;course='. SITEID);
2949 // Make sure the USER has a sesskey set up. Used for CSRF protection.
2950 sesskey();
2952 // Do not bother admins with any formalities
2953 if (is_siteadmin()) {
2954 //set accesstime or the user will appear offline which messes up messaging
2955 user_accesstime_log($course->id);
2956 return;
2959 // Check that the user has agreed to a site policy if there is one - do not test in case of admins
2960 if (!$USER->policyagreed and !is_siteadmin()) {
2961 if (!empty($CFG->sitepolicy) and !isguestuser()) {
2962 if ($preventredirect) {
2963 throw new require_login_exception('Policy not agreed');
2965 if ($setwantsurltome) {
2966 $SESSION->wantsurl = qualified_me();
2968 redirect($CFG->wwwroot .'/user/policy.php');
2969 } else if (!empty($CFG->sitepolicyguest) and isguestuser()) {
2970 if ($preventredirect) {
2971 throw new require_login_exception('Policy not agreed');
2973 if ($setwantsurltome) {
2974 $SESSION->wantsurl = qualified_me();
2976 redirect($CFG->wwwroot .'/user/policy.php');
2980 // Fetch the system context, the course context, and prefetch its child contexts
2981 $sysctx = context_system::instance();
2982 $coursecontext = context_course::instance($course->id, MUST_EXIST);
2983 if ($cm) {
2984 $cmcontext = context_module::instance($cm->id, MUST_EXIST);
2985 } else {
2986 $cmcontext = null;
2989 // If the site is currently under maintenance, then print a message
2990 if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:config', $sysctx)) {
2991 if ($preventredirect) {
2992 throw new require_login_exception('Maintenance in progress');
2995 print_maintenance_message();
2998 // make sure the course itself is not hidden
2999 if ($course->id == SITEID) {
3000 // frontpage can not be hidden
3001 } else {
3002 if (is_role_switched($course->id)) {
3003 // when switching roles ignore the hidden flag - user had to be in course to do the switch
3004 } else {
3005 if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
3006 // originally there was also test of parent category visibility,
3007 // BUT is was very slow in complex queries involving "my courses"
3008 // now it is also possible to simply hide all courses user is not enrolled in :-)
3009 if ($preventredirect) {
3010 throw new require_login_exception('Course is hidden');
3012 // We need to override the navigation URL as the course won't have
3013 // been added to the navigation and thus the navigation will mess up
3014 // when trying to find it.
3015 navigation_node::override_active_url(new moodle_url('/'));
3016 notice(get_string('coursehidden'), $CFG->wwwroot .'/');
3021 // is the user enrolled?
3022 if ($course->id == SITEID) {
3023 // everybody is enrolled on the frontpage
3025 } else {
3026 if (session_is_loggedinas()) {
3027 // Make sure the REAL person can access this course first
3028 $realuser = session_get_realuser();
3029 if (!is_enrolled($coursecontext, $realuser->id, '', true) and !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) {
3030 if ($preventredirect) {
3031 throw new require_login_exception('Invalid course login-as access');
3033 echo $OUTPUT->header();
3034 notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/');
3038 $access = false;
3040 if (is_role_switched($course->id)) {
3041 // ok, user had to be inside this course before the switch
3042 $access = true;
3044 } else if (is_viewing($coursecontext, $USER)) {
3045 // ok, no need to mess with enrol
3046 $access = true;
3048 } else {
3049 if (isset($USER->enrol['enrolled'][$course->id])) {
3050 if ($USER->enrol['enrolled'][$course->id] > time()) {
3051 $access = true;
3052 if (isset($USER->enrol['tempguest'][$course->id])) {
3053 unset($USER->enrol['tempguest'][$course->id]);
3054 remove_temp_course_roles($coursecontext);
3056 } else {
3057 //expired
3058 unset($USER->enrol['enrolled'][$course->id]);
3061 if (isset($USER->enrol['tempguest'][$course->id])) {
3062 if ($USER->enrol['tempguest'][$course->id] == 0) {
3063 $access = true;
3064 } else if ($USER->enrol['tempguest'][$course->id] > time()) {
3065 $access = true;
3066 } else {
3067 //expired
3068 unset($USER->enrol['tempguest'][$course->id]);
3069 remove_temp_course_roles($coursecontext);
3073 if ($access) {
3074 // cache ok
3075 } else {
3076 $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id);
3077 if ($until !== false) {
3078 // active participants may always access, a timestamp in the future, 0 (always) or false.
3079 if ($until == 0) {
3080 $until = ENROL_MAX_TIMESTAMP;
3082 $USER->enrol['enrolled'][$course->id] = $until;
3083 $access = true;
3085 } else {
3086 $instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder, id ASC');
3087 $enrols = enrol_get_plugins(true);
3088 // first ask all enabled enrol instances in course if they want to auto enrol user
3089 foreach($instances as $instance) {
3090 if (!isset($enrols[$instance->enrol])) {
3091 continue;
3093 // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false.
3094 $until = $enrols[$instance->enrol]->try_autoenrol($instance);
3095 if ($until !== false) {
3096 if ($until == 0) {
3097 $until = ENROL_MAX_TIMESTAMP;
3099 $USER->enrol['enrolled'][$course->id] = $until;
3100 $access = true;
3101 break;
3104 // if not enrolled yet try to gain temporary guest access
3105 if (!$access) {
3106 foreach($instances as $instance) {
3107 if (!isset($enrols[$instance->enrol])) {
3108 continue;
3110 // Get a duration for the guest access, a timestamp in the future or false.
3111 $until = $enrols[$instance->enrol]->try_guestaccess($instance);
3112 if ($until !== false and $until > time()) {
3113 $USER->enrol['tempguest'][$course->id] = $until;
3114 $access = true;
3115 break;
3123 if (!$access) {
3124 if ($preventredirect) {
3125 throw new require_login_exception('Not enrolled');
3127 if ($setwantsurltome) {
3128 $SESSION->wantsurl = qualified_me();
3130 redirect($CFG->wwwroot .'/enrol/index.php?id='. $course->id);
3134 // Check visibility of activity to current user; includes visible flag, groupmembersonly,
3135 // conditional availability, etc
3136 if ($cm && !$cm->uservisible) {
3137 if ($preventredirect) {
3138 throw new require_login_exception('Activity is hidden');
3140 if ($course->id != SITEID) {
3141 $url = new moodle_url('/course/view.php', array('id'=>$course->id));
3142 } else {
3143 $url = new moodle_url('/');
3145 redirect($url, get_string('activityiscurrentlyhidden'));
3148 // Finally access granted, update lastaccess times
3149 user_accesstime_log($course->id);
3154 * This function just makes sure a user is logged out.
3156 * @package core_access
3158 function require_logout() {
3159 global $USER;
3161 $params = $USER;
3163 if (isloggedin()) {
3164 add_to_log(SITEID, "user", "logout", "view.php?id=$USER->id&course=".SITEID, $USER->id, 0, $USER->id);
3166 $authsequence = get_enabled_auth_plugins(); // auths, in sequence
3167 foreach($authsequence as $authname) {
3168 $authplugin = get_auth_plugin($authname);
3169 $authplugin->prelogout_hook();
3173 events_trigger('user_logout', $params);
3174 session_get_instance()->terminate_current();
3175 unset($params);
3179 * Weaker version of require_login()
3181 * This is a weaker version of {@link require_login()} which only requires login
3182 * when called from within a course rather than the site page, unless
3183 * the forcelogin option is turned on.
3184 * @see require_login()
3186 * @package core_access
3187 * @category access
3189 * @param mixed $courseorid The course object or id in question
3190 * @param bool $autologinguest Allow autologin guests if that is wanted
3191 * @param object $cm Course activity module if known
3192 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
3193 * true. Used to avoid (=false) some scripts (file.php...) to set that variable,
3194 * in order to keep redirects working properly. MDL-14495
3195 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
3196 * @return void
3198 function require_course_login($courseorid, $autologinguest = true, $cm = NULL, $setwantsurltome = true, $preventredirect = false) {
3199 global $CFG, $PAGE, $SITE;
3200 $issite = (is_object($courseorid) and $courseorid->id == SITEID)
3201 or (!is_object($courseorid) and $courseorid == SITEID);
3202 if ($issite && !empty($cm) && !($cm instanceof cm_info)) {
3203 // note: nearly all pages call get_fast_modinfo anyway and it does not make any
3204 // db queries so this is not really a performance concern, however it is obviously
3205 // better if you use get_fast_modinfo to get the cm before calling this.
3206 if (is_object($courseorid)) {
3207 $course = $courseorid;
3208 } else {
3209 $course = clone($SITE);
3211 $modinfo = get_fast_modinfo($course);
3212 $cm = $modinfo->get_cm($cm->id);
3214 if (!empty($CFG->forcelogin)) {
3215 // login required for both SITE and courses
3216 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3218 } else if ($issite && !empty($cm) and !$cm->uservisible) {
3219 // always login for hidden activities
3220 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3222 } else if ($issite) {
3223 //login for SITE not required
3224 if ($cm and empty($cm->visible)) {
3225 // hidden activities are not accessible without login
3226 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3227 } else if ($cm and !empty($CFG->enablegroupmembersonly) and $cm->groupmembersonly) {
3228 // not-logged-in users do not have any group membership
3229 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3230 } else {
3231 // We still need to instatiate PAGE vars properly so that things
3232 // that rely on it like navigation function correctly.
3233 if (!empty($courseorid)) {
3234 if (is_object($courseorid)) {
3235 $course = $courseorid;
3236 } else {
3237 $course = clone($SITE);
3239 if ($cm) {
3240 if ($cm->course != $course->id) {
3241 throw new coding_exception('course and cm parameters in require_course_login() call do not match!!');
3243 $PAGE->set_cm($cm, $course);
3244 $PAGE->set_pagelayout('incourse');
3245 } else {
3246 $PAGE->set_course($course);
3248 } else {
3249 // If $PAGE->course, and hence $PAGE->context, have not already been set
3250 // up properly, set them up now.
3251 $PAGE->set_course($PAGE->course);
3253 //TODO: verify conditional activities here
3254 user_accesstime_log(SITEID);
3255 return;
3258 } else {
3259 // course login always required
3260 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3265 * Require key login. Function terminates with error if key not found or incorrect.
3267 * @global object
3268 * @global object
3269 * @global object
3270 * @global object
3271 * @uses NO_MOODLE_COOKIES
3272 * @uses PARAM_ALPHANUM
3273 * @param string $script unique script identifier
3274 * @param int $instance optional instance id
3275 * @return int Instance ID
3277 function require_user_key_login($script, $instance=null) {
3278 global $USER, $SESSION, $CFG, $DB;
3280 if (!NO_MOODLE_COOKIES) {
3281 print_error('sessioncookiesdisable');
3284 /// extra safety
3285 @session_write_close();
3287 $keyvalue = required_param('key', PARAM_ALPHANUM);
3289 if (!$key = $DB->get_record('user_private_key', array('script'=>$script, 'value'=>$keyvalue, 'instance'=>$instance))) {
3290 print_error('invalidkey');
3293 if (!empty($key->validuntil) and $key->validuntil < time()) {
3294 print_error('expiredkey');
3297 if ($key->iprestriction) {
3298 $remoteaddr = getremoteaddr(null);
3299 if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) {
3300 print_error('ipmismatch');
3304 if (!$user = $DB->get_record('user', array('id'=>$key->userid))) {
3305 print_error('invaliduserid');
3308 /// emulate normal session
3309 enrol_check_plugins($user);
3310 session_set_user($user);
3312 /// note we are not using normal login
3313 if (!defined('USER_KEY_LOGIN')) {
3314 define('USER_KEY_LOGIN', true);
3317 /// return instance id - it might be empty
3318 return $key->instance;
3322 * Creates a new private user access key.
3324 * @global object
3325 * @param string $script unique target identifier
3326 * @param int $userid
3327 * @param int $instance optional instance id
3328 * @param string $iprestriction optional ip restricted access
3329 * @param timestamp $validuntil key valid only until given data
3330 * @return string access key value
3332 function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3333 global $DB;
3335 $key = new stdClass();
3336 $key->script = $script;
3337 $key->userid = $userid;
3338 $key->instance = $instance;
3339 $key->iprestriction = $iprestriction;
3340 $key->validuntil = $validuntil;
3341 $key->timecreated = time();
3343 $key->value = md5($userid.'_'.time().random_string(40)); // something long and unique
3344 while ($DB->record_exists('user_private_key', array('value'=>$key->value))) {
3345 // must be unique
3346 $key->value = md5($userid.'_'.time().random_string(40));
3348 $DB->insert_record('user_private_key', $key);
3349 return $key->value;
3353 * Delete the user's new private user access keys for a particular script.
3355 * @global object
3356 * @param string $script unique target identifier
3357 * @param int $userid
3358 * @return void
3360 function delete_user_key($script,$userid) {
3361 global $DB;
3362 $DB->delete_records('user_private_key', array('script'=>$script, 'userid'=>$userid));
3366 * Gets a private user access key (and creates one if one doesn't exist).
3368 * @global object
3369 * @param string $script unique target identifier
3370 * @param int $userid
3371 * @param int $instance optional instance id
3372 * @param string $iprestriction optional ip restricted access
3373 * @param timestamp $validuntil key valid only until given data
3374 * @return string access key value
3376 function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3377 global $DB;
3379 if ($key = $DB->get_record('user_private_key', array('script'=>$script, 'userid'=>$userid,
3380 'instance'=>$instance, 'iprestriction'=>$iprestriction,
3381 'validuntil'=>$validuntil))) {
3382 return $key->value;
3383 } else {
3384 return create_user_key($script, $userid, $instance, $iprestriction, $validuntil);
3390 * Modify the user table by setting the currently logged in user's
3391 * last login to now.
3393 * @global object
3394 * @global object
3395 * @return bool Always returns true
3397 function update_user_login_times() {
3398 global $USER, $DB;
3400 if (isguestuser()) {
3401 // Do not update guest access times/ips for performance.
3402 return true;
3405 $now = time();
3407 $user = new stdClass();
3408 $user->id = $USER->id;
3410 // Make sure all users that logged in have some firstaccess.
3411 if ($USER->firstaccess == 0) {
3412 $USER->firstaccess = $user->firstaccess = $now;
3415 // Store the previous current as lastlogin.
3416 $USER->lastlogin = $user->lastlogin = $USER->currentlogin;
3418 $USER->currentlogin = $user->currentlogin = $now;
3420 // Function user_accesstime_log() may not update immediately, better do it here.
3421 $USER->lastaccess = $user->lastaccess = $now;
3422 $USER->lastip = $user->lastip = getremoteaddr();
3424 $DB->update_record('user', $user);
3425 return true;
3429 * Determines if a user has completed setting up their account.
3431 * @param user $user A {@link $USER} object to test for the existence of a valid name and email
3432 * @return bool
3434 function user_not_fully_set_up($user) {
3435 if (isguestuser($user)) {
3436 return false;
3438 return (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user));
3442 * Check whether the user has exceeded the bounce threshold
3444 * @global object
3445 * @global object
3446 * @param user $user A {@link $USER} object
3447 * @return bool true=>User has exceeded bounce threshold
3449 function over_bounce_threshold($user) {
3450 global $CFG, $DB;
3452 if (empty($CFG->handlebounces)) {
3453 return false;
3456 if (empty($user->id)) { /// No real (DB) user, nothing to do here.
3457 return false;
3460 // set sensible defaults
3461 if (empty($CFG->minbounces)) {
3462 $CFG->minbounces = 10;
3464 if (empty($CFG->bounceratio)) {
3465 $CFG->bounceratio = .20;
3467 $bouncecount = 0;
3468 $sendcount = 0;
3469 if ($bounce = $DB->get_record('user_preferences', array ('userid'=>$user->id, 'name'=>'email_bounce_count'))) {
3470 $bouncecount = $bounce->value;
3472 if ($send = $DB->get_record('user_preferences', array('userid'=>$user->id, 'name'=>'email_send_count'))) {
3473 $sendcount = $send->value;
3475 return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio);
3479 * Used to increment or reset email sent count
3481 * @global object
3482 * @param user $user object containing an id
3483 * @param bool $reset will reset the count to 0
3484 * @return void
3486 function set_send_count($user,$reset=false) {
3487 global $DB;
3489 if (empty($user->id)) { /// No real (DB) user, nothing to do here.
3490 return;
3493 if ($pref = $DB->get_record('user_preferences', array('userid'=>$user->id, 'name'=>'email_send_count'))) {
3494 $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3495 $DB->update_record('user_preferences', $pref);
3497 else if (!empty($reset)) { // if it's not there and we're resetting, don't bother.
3498 // make a new one
3499 $pref = new stdClass();
3500 $pref->name = 'email_send_count';
3501 $pref->value = 1;
3502 $pref->userid = $user->id;
3503 $DB->insert_record('user_preferences', $pref, false);
3508 * Increment or reset user's email bounce count
3510 * @global object
3511 * @param user $user object containing an id
3512 * @param bool $reset will reset the count to 0
3514 function set_bounce_count($user,$reset=false) {
3515 global $DB;
3517 if ($pref = $DB->get_record('user_preferences', array('userid'=>$user->id, 'name'=>'email_bounce_count'))) {
3518 $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3519 $DB->update_record('user_preferences', $pref);
3521 else if (!empty($reset)) { // if it's not there and we're resetting, don't bother.
3522 // make a new one
3523 $pref = new stdClass();
3524 $pref->name = 'email_bounce_count';
3525 $pref->value = 1;
3526 $pref->userid = $user->id;
3527 $DB->insert_record('user_preferences', $pref, false);
3532 * Determines if the currently logged in user is in editing mode.
3533 * Note: originally this function had $userid parameter - it was not usable anyway
3535 * @deprecated since Moodle 2.0 - use $PAGE->user_is_editing() instead.
3536 * @todo Deprecated function remove when ready
3538 * @global object
3539 * @uses DEBUG_DEVELOPER
3540 * @return bool
3542 function isediting() {
3543 global $PAGE;
3544 debugging('call to deprecated function isediting(). Please use $PAGE->user_is_editing() instead', DEBUG_DEVELOPER);
3545 return $PAGE->user_is_editing();
3549 * Determines if the logged in user is currently moving an activity
3551 * @global object
3552 * @param int $courseid The id of the course being tested
3553 * @return bool
3555 function ismoving($courseid) {
3556 global $USER;
3558 if (!empty($USER->activitycopy)) {
3559 return ($USER->activitycopycourse == $courseid);
3561 return false;
3565 * Returns a persons full name
3567 * Given an object containing firstname and lastname
3568 * values, this function returns a string with the
3569 * full name of the person.
3570 * The result may depend on system settings
3571 * or language. 'override' will force both names
3572 * to be used even if system settings specify one.
3574 * @global object
3575 * @global object
3576 * @param object $user A {@link $USER} object to get full name of
3577 * @param bool $override If true then the name will be first name followed by last name rather than adhering to fullnamedisplay setting.
3578 * @return string
3580 function fullname($user, $override=false) {
3581 global $CFG, $SESSION;
3583 if (!isset($user->firstname) and !isset($user->lastname)) {
3584 return '';
3587 if (!$override) {
3588 if (!empty($CFG->forcefirstname)) {
3589 $user->firstname = $CFG->forcefirstname;
3591 if (!empty($CFG->forcelastname)) {
3592 $user->lastname = $CFG->forcelastname;
3596 if (!empty($SESSION->fullnamedisplay)) {
3597 $CFG->fullnamedisplay = $SESSION->fullnamedisplay;
3600 if (!isset($CFG->fullnamedisplay) or $CFG->fullnamedisplay === 'firstname lastname') {
3601 return $user->firstname .' '. $user->lastname;
3603 } else if ($CFG->fullnamedisplay == 'lastname firstname') {
3604 return $user->lastname .' '. $user->firstname;
3606 } else if ($CFG->fullnamedisplay == 'firstname') {
3607 if ($override) {
3608 return get_string('fullnamedisplay', '', $user);
3609 } else {
3610 return $user->firstname;
3614 return get_string('fullnamedisplay', '', $user);
3618 * Checks if current user is shown any extra fields when listing users.
3619 * @param object $context Context
3620 * @param array $already Array of fields that we're going to show anyway
3621 * so don't bother listing them
3622 * @return array Array of field names from user table, not including anything
3623 * listed in $already
3625 function get_extra_user_fields($context, $already = array()) {
3626 global $CFG;
3628 // Only users with permission get the extra fields
3629 if (!has_capability('moodle/site:viewuseridentity', $context)) {
3630 return array();
3633 // Split showuseridentity on comma
3634 if (empty($CFG->showuseridentity)) {
3635 // Explode gives wrong result with empty string
3636 $extra = array();
3637 } else {
3638 $extra = explode(',', $CFG->showuseridentity);
3640 $renumber = false;
3641 foreach ($extra as $key => $field) {
3642 if (in_array($field, $already)) {
3643 unset($extra[$key]);
3644 $renumber = true;
3647 if ($renumber) {
3648 // For consistency, if entries are removed from array, renumber it
3649 // so they are numbered as you would expect
3650 $extra = array_merge($extra);
3652 return $extra;
3656 * If the current user is to be shown extra user fields when listing or
3657 * selecting users, returns a string suitable for including in an SQL select
3658 * clause to retrieve those fields.
3659 * @param object $context Context
3660 * @param string $alias Alias of user table, e.g. 'u' (default none)
3661 * @param string $prefix Prefix for field names using AS, e.g. 'u_' (default none)
3662 * @param array $already Array of fields that we're going to include anyway
3663 * so don't list them (default none)
3664 * @return string Partial SQL select clause, beginning with comma, for example
3665 * ',u.idnumber,u.department' unless it is blank
3667 function get_extra_user_fields_sql($context, $alias='', $prefix='',
3668 $already = array()) {
3669 $fields = get_extra_user_fields($context, $already);
3670 $result = '';
3671 // Add punctuation for alias
3672 if ($alias !== '') {
3673 $alias .= '.';
3675 foreach ($fields as $field) {
3676 $result .= ', ' . $alias . $field;
3677 if ($prefix) {
3678 $result .= ' AS ' . $prefix . $field;
3681 return $result;
3685 * Returns the display name of a field in the user table. Works for most fields
3686 * that are commonly displayed to users.
3687 * @param string $field Field name, e.g. 'phone1'
3688 * @return string Text description taken from language file, e.g. 'Phone number'
3690 function get_user_field_name($field) {
3691 // Some fields have language strings which are not the same as field name
3692 switch ($field) {
3693 case 'phone1' : return get_string('phone');
3694 case 'url' : return get_string('webpage');
3695 case 'icq' : return get_string('icqnumber');
3696 case 'skype' : return get_string('skypeid');
3697 case 'aim' : return get_string('aimid');
3698 case 'yahoo' : return get_string('yahooid');
3699 case 'msn' : return get_string('msnid');
3701 // Otherwise just use the same lang string
3702 return get_string($field);
3706 * Returns whether a given authentication plugin exists.
3708 * @global object
3709 * @param string $auth Form of authentication to check for. Defaults to the
3710 * global setting in {@link $CFG}.
3711 * @return boolean Whether the plugin is available.
3713 function exists_auth_plugin($auth) {
3714 global $CFG;
3716 if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) {
3717 return is_readable("{$CFG->dirroot}/auth/$auth/auth.php");
3719 return false;
3723 * Checks if a given plugin is in the list of enabled authentication plugins.
3725 * @param string $auth Authentication plugin.
3726 * @return boolean Whether the plugin is enabled.
3728 function is_enabled_auth($auth) {
3729 if (empty($auth)) {
3730 return false;
3733 $enabled = get_enabled_auth_plugins();
3735 return in_array($auth, $enabled);
3739 * Returns an authentication plugin instance.
3741 * @global object
3742 * @param string $auth name of authentication plugin
3743 * @return auth_plugin_base An instance of the required authentication plugin.
3745 function get_auth_plugin($auth) {
3746 global $CFG;
3748 // check the plugin exists first
3749 if (! exists_auth_plugin($auth)) {
3750 print_error('authpluginnotfound', 'debug', '', $auth);
3753 // return auth plugin instance
3754 require_once "{$CFG->dirroot}/auth/$auth/auth.php";
3755 $class = "auth_plugin_$auth";
3756 return new $class;
3760 * Returns array of active auth plugins.
3762 * @param bool $fix fix $CFG->auth if needed
3763 * @return array
3765 function get_enabled_auth_plugins($fix=false) {
3766 global $CFG;
3768 $default = array('manual', 'nologin');
3770 if (empty($CFG->auth)) {
3771 $auths = array();
3772 } else {
3773 $auths = explode(',', $CFG->auth);
3776 if ($fix) {
3777 $auths = array_unique($auths);
3778 foreach($auths as $k=>$authname) {
3779 if (!exists_auth_plugin($authname) or in_array($authname, $default)) {
3780 unset($auths[$k]);
3783 $newconfig = implode(',', $auths);
3784 if (!isset($CFG->auth) or $newconfig != $CFG->auth) {
3785 set_config('auth', $newconfig);
3789 return (array_merge($default, $auths));
3793 * Returns true if an internal authentication method is being used.
3794 * if method not specified then, global default is assumed
3796 * @param string $auth Form of authentication required
3797 * @return bool
3799 function is_internal_auth($auth) {
3800 $authplugin = get_auth_plugin($auth); // throws error if bad $auth
3801 return $authplugin->is_internal();
3805 * Returns true if the user is a 'restored' one
3807 * Used in the login process to inform the user
3808 * and allow him/her to reset the password
3810 * @uses $CFG
3811 * @uses $DB
3812 * @param string $username username to be checked
3813 * @return bool
3815 function is_restored_user($username) {
3816 global $CFG, $DB;
3818 return $DB->record_exists('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id, 'password'=>'restored'));
3822 * Returns an array of user fields
3824 * @return array User field/column names
3826 function get_user_fieldnames() {
3827 global $DB;
3829 $fieldarray = $DB->get_columns('user');
3830 unset($fieldarray['id']);
3831 $fieldarray = array_keys($fieldarray);
3833 return $fieldarray;
3837 * Creates a bare-bones user record
3839 * @todo Outline auth types and provide code example
3841 * @param string $username New user's username to add to record
3842 * @param string $password New user's password to add to record
3843 * @param string $auth Form of authentication required
3844 * @return stdClass A complete user object
3846 function create_user_record($username, $password, $auth = 'manual') {
3847 global $CFG, $DB;
3849 //just in case check text case
3850 $username = trim(textlib::strtolower($username));
3852 $authplugin = get_auth_plugin($auth);
3854 $newuser = new stdClass();
3856 if ($newinfo = $authplugin->get_userinfo($username)) {
3857 $newinfo = truncate_userinfo($newinfo);
3858 foreach ($newinfo as $key => $value){
3859 $newuser->$key = $value;
3863 if (!empty($newuser->email)) {
3864 if (email_is_not_allowed($newuser->email)) {
3865 unset($newuser->email);
3869 if (!isset($newuser->city)) {
3870 $newuser->city = '';
3873 $newuser->auth = $auth;
3874 $newuser->username = $username;
3876 // fix for MDL-8480
3877 // user CFG lang for user if $newuser->lang is empty
3878 // or $user->lang is not an installed language
3879 if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) {
3880 $newuser->lang = $CFG->lang;
3882 $newuser->confirmed = 1;
3883 $newuser->lastip = getremoteaddr();
3884 $newuser->timecreated = time();
3885 $newuser->timemodified = $newuser->timecreated;
3886 $newuser->mnethostid = $CFG->mnet_localhost_id;
3888 $newuser->id = $DB->insert_record('user', $newuser);
3889 $user = get_complete_user_data('id', $newuser->id);
3890 if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})){
3891 set_user_preference('auth_forcepasswordchange', 1, $user);
3893 // Set the password.
3894 update_internal_user_password($user, $password);
3896 // fetch full user record for the event, the complete user data contains too much info
3897 // and we want to be consistent with other places that trigger this event
3898 events_trigger('user_created', $DB->get_record('user', array('id'=>$user->id)));
3900 return $user;
3904 * Will update a local user record from an external source.
3905 * (MNET users can not be updated using this method!)
3907 * @param string $username user's username to update the record
3908 * @return stdClass A complete user object
3910 function update_user_record($username) {
3911 global $DB, $CFG;
3913 $username = trim(textlib::strtolower($username)); /// just in case check text case
3915 $oldinfo = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id), '*', MUST_EXIST);
3916 $newuser = array();
3917 $userauth = get_auth_plugin($oldinfo->auth);
3919 if ($newinfo = $userauth->get_userinfo($username)) {
3920 $newinfo = truncate_userinfo($newinfo);
3921 foreach ($newinfo as $key => $value){
3922 $key = strtolower($key);
3923 if (!property_exists($oldinfo, $key) or $key === 'username' or $key === 'id'
3924 or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') {
3925 // unknown or must not be changed
3926 continue;
3928 $confval = $userauth->config->{'field_updatelocal_' . $key};
3929 $lockval = $userauth->config->{'field_lock_' . $key};
3930 if (empty($confval) || empty($lockval)) {
3931 continue;
3933 if ($confval === 'onlogin') {
3934 // MDL-4207 Don't overwrite modified user profile values with
3935 // empty LDAP values when 'unlocked if empty' is set. The purpose
3936 // of the setting 'unlocked if empty' is to allow the user to fill
3937 // in a value for the selected field _if LDAP is giving
3938 // nothing_ for this field. Thus it makes sense to let this value
3939 // stand in until LDAP is giving a value for this field.
3940 if (!(empty($value) && $lockval === 'unlockedifempty')) {
3941 if ((string)$oldinfo->$key !== (string)$value) {
3942 $newuser[$key] = (string)$value;
3947 if ($newuser) {
3948 $newuser['id'] = $oldinfo->id;
3949 $newuser['timemodified'] = time();
3950 $DB->update_record('user', $newuser);
3951 // fetch full user record for the event, the complete user data contains too much info
3952 // and we want to be consistent with other places that trigger this event
3953 events_trigger('user_updated', $DB->get_record('user', array('id'=>$oldinfo->id)));
3957 return get_complete_user_data('id', $oldinfo->id);
3961 * Will truncate userinfo as it comes from auth_get_userinfo (from external auth)
3962 * which may have large fields
3964 * @todo Add vartype handling to ensure $info is an array
3966 * @param array $info Array of user properties to truncate if needed
3967 * @return array The now truncated information that was passed in
3969 function truncate_userinfo($info) {
3970 // define the limits
3971 $limit = array(
3972 'username' => 100,
3973 'idnumber' => 255,
3974 'firstname' => 100,
3975 'lastname' => 100,
3976 'email' => 100,
3977 'icq' => 15,
3978 'phone1' => 20,
3979 'phone2' => 20,
3980 'institution' => 40,
3981 'department' => 30,
3982 'address' => 70,
3983 'city' => 120,
3984 'country' => 2,
3985 'url' => 255,
3988 // apply where needed
3989 foreach (array_keys($info) as $key) {
3990 if (!empty($limit[$key])) {
3991 $info[$key] = trim(textlib::substr($info[$key],0, $limit[$key]));
3995 return $info;
3999 * Marks user deleted in internal user database and notifies the auth plugin.
4000 * Also unenrols user from all roles and does other cleanup.
4002 * Any plugin that needs to purge user data should register the 'user_deleted' event.
4004 * @param stdClass $user full user object before delete
4005 * @return boolean success
4006 * @throws coding_exception if invalid $user parameter detected
4008 function delete_user(stdClass $user) {
4009 global $CFG, $DB;
4010 require_once($CFG->libdir.'/grouplib.php');
4011 require_once($CFG->libdir.'/gradelib.php');
4012 require_once($CFG->dirroot.'/message/lib.php');
4013 require_once($CFG->dirroot.'/tag/lib.php');
4015 // Make sure nobody sends bogus record type as parameter.
4016 if (!property_exists($user, 'id') or !property_exists($user, 'username')) {
4017 throw new coding_exception('Invalid $user parameter in delete_user() detected');
4020 // Better not trust the parameter and fetch the latest info,
4021 // this will be very expensive anyway.
4022 if (!$user = $DB->get_record('user', array('id'=>$user->id))) {
4023 debugging('Attempt to delete unknown user account.');
4024 return false;
4027 // There must be always exactly one guest record,
4028 // originally the guest account was identified by username only,
4029 // now we use $CFG->siteguest for performance reasons.
4030 if ($user->username === 'guest' or isguestuser($user)) {
4031 debugging('Guest user account can not be deleted.');
4032 return false;
4035 // Admin can be theoretically from different auth plugin,
4036 // but we want to prevent deletion of internal accoutns only,
4037 // if anything goes wrong ppl may force somebody to be admin via
4038 // config.php setting $CFG->siteadmins.
4039 if ($user->auth === 'manual' and is_siteadmin($user)) {
4040 debugging('Local administrator accounts can not be deleted.');
4041 return false;
4044 // delete all grades - backup is kept in grade_grades_history table
4045 grade_user_delete($user->id);
4047 //move unread messages from this user to read
4048 message_move_userfrom_unread2read($user->id);
4050 // TODO: remove from cohorts using standard API here
4052 // remove user tags
4053 tag_set('user', $user->id, array());
4055 // unconditionally unenrol from all courses
4056 enrol_user_delete($user);
4058 // unenrol from all roles in all contexts
4059 role_unassign_all(array('userid'=>$user->id)); // this might be slow but it is really needed - modules might do some extra cleanup!
4061 //now do a brute force cleanup
4063 // remove from all cohorts
4064 $DB->delete_records('cohort_members', array('userid'=>$user->id));
4066 // remove from all groups
4067 $DB->delete_records('groups_members', array('userid'=>$user->id));
4069 // brute force unenrol from all courses
4070 $DB->delete_records('user_enrolments', array('userid'=>$user->id));
4072 // purge user preferences
4073 $DB->delete_records('user_preferences', array('userid'=>$user->id));
4075 // purge user extra profile info
4076 $DB->delete_records('user_info_data', array('userid'=>$user->id));
4078 // last course access not necessary either
4079 $DB->delete_records('user_lastaccess', array('userid'=>$user->id));
4081 // remove all user tokens
4082 $DB->delete_records('external_tokens', array('userid'=>$user->id));
4084 // unauthorise the user for all services
4085 $DB->delete_records('external_services_users', array('userid'=>$user->id));
4087 // force logout - may fail if file based sessions used, sorry
4088 session_kill_user($user->id);
4090 // now do a final accesslib cleanup - removes all role assignments in user context and context itself
4091 delete_context(CONTEXT_USER, $user->id);
4093 // workaround for bulk deletes of users with the same email address
4094 $delname = "$user->email.".time();
4095 while ($DB->record_exists('user', array('username'=>$delname))) { // no need to use mnethostid here
4096 $delname++;
4099 // mark internal user record as "deleted"
4100 $updateuser = new stdClass();
4101 $updateuser->id = $user->id;
4102 $updateuser->deleted = 1;
4103 $updateuser->username = $delname; // Remember it just in case
4104 $updateuser->email = md5($user->username);// Store hash of username, useful importing/restoring users
4105 $updateuser->idnumber = ''; // Clear this field to free it up
4106 $updateuser->picture = 0;
4107 $updateuser->timemodified = time();
4109 $DB->update_record('user', $updateuser);
4110 // Add this action to log
4111 add_to_log(SITEID, 'user', 'delete', "view.php?id=$user->id", $user->firstname.' '.$user->lastname);
4114 // We will update the user's timemodified, as it will be passed to the user_deleted event, which
4115 // should know about this updated property persisted to the user's table.
4116 $user->timemodified = $updateuser->timemodified;
4118 // notify auth plugin - do not block the delete even when plugin fails
4119 $authplugin = get_auth_plugin($user->auth);
4120 $authplugin->user_delete($user);
4122 // any plugin that needs to cleanup should register this event
4123 events_trigger('user_deleted', $user);
4125 return true;
4129 * Retrieve the guest user object
4131 * @global object
4132 * @global object
4133 * @return user A {@link $USER} object
4135 function guest_user() {
4136 global $CFG, $DB;
4138 if ($newuser = $DB->get_record('user', array('id'=>$CFG->siteguest))) {
4139 $newuser->confirmed = 1;
4140 $newuser->lang = $CFG->lang;
4141 $newuser->lastip = getremoteaddr();
4144 return $newuser;
4148 * Authenticates a user against the chosen authentication mechanism
4150 * Given a username and password, this function looks them
4151 * up using the currently selected authentication mechanism,
4152 * and if the authentication is successful, it returns a
4153 * valid $user object from the 'user' table.
4155 * Uses auth_ functions from the currently active auth module
4157 * After authenticate_user_login() returns success, you will need to
4158 * log that the user has logged in, and call complete_user_login() to set
4159 * the session up.
4161 * Note: this function works only with non-mnet accounts!
4163 * @param string $username User's username
4164 * @param string $password User's password
4165 * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO
4166 * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists)
4167 * @return stdClass|false A {@link $USER} object or false if error
4169 function authenticate_user_login($username, $password, $ignorelockout=false, &$failurereason=null) {
4170 global $CFG, $DB;
4171 require_once("$CFG->libdir/authlib.php");
4173 $authsenabled = get_enabled_auth_plugins();
4175 if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) {
4176 $auth = empty($user->auth) ? 'manual' : $user->auth; // use manual if auth not set
4177 if (!empty($user->suspended)) {
4178 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4179 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4180 $failurereason = AUTH_LOGIN_SUSPENDED;
4181 return false;
4183 if ($auth=='nologin' or !is_enabled_auth($auth)) {
4184 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4185 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Disabled Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4186 $failurereason = AUTH_LOGIN_SUSPENDED; // Legacy way to suspend user.
4187 return false;
4189 $auths = array($auth);
4191 } else {
4192 // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user().
4193 if ($DB->get_field('user', 'id', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id, 'deleted'=>1))) {
4194 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Deleted Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4195 $failurereason = AUTH_LOGIN_NOUSER;
4196 return false;
4199 // Do not try to authenticate non-existent accounts when user creation is not disabled.
4200 if (!empty($CFG->authpreventaccountcreation)) {
4201 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4202 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Unknown user, can not create new accounts: $username ".$_SERVER['HTTP_USER_AGENT']);
4203 $failurereason = AUTH_LOGIN_NOUSER;
4204 return false;
4207 // User does not exist
4208 $auths = $authsenabled;
4209 $user = new stdClass();
4210 $user->id = 0;
4213 if ($ignorelockout) {
4214 // Some other mechanism protects against brute force password guessing,
4215 // for example login form might include reCAPTCHA or this function
4216 // is called from a SSO script.
4218 } else if ($user->id) {
4219 // Verify login lockout after other ways that may prevent user login.
4220 if (login_is_lockedout($user)) {
4221 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4222 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Login lockout: $username ".$_SERVER['HTTP_USER_AGENT']);
4223 $failurereason = AUTH_LOGIN_LOCKOUT;
4224 return false;
4227 } else {
4228 // We can not lockout non-existing accounts.
4231 foreach ($auths as $auth) {
4232 $authplugin = get_auth_plugin($auth);
4234 // on auth fail fall through to the next plugin
4235 if (!$authplugin->user_login($username, $password)) {
4236 continue;
4239 // successful authentication
4240 if ($user->id) { // User already exists in database
4241 if (empty($user->auth)) { // For some reason auth isn't set yet
4242 $DB->set_field('user', 'auth', $auth, array('username'=>$username));
4243 $user->auth = $auth;
4246 // If the existing hash is using an out-of-date algorithm (or the
4247 // legacy md5 algorithm), then we should update to the current
4248 // hash algorithm while we have access to the user's password.
4249 update_internal_user_password($user, $password);
4251 if ($authplugin->is_synchronised_with_external()) { // update user record from external DB
4252 $user = update_user_record($username);
4254 } else {
4255 // Create account, we verified above that user creation is allowed.
4256 $user = create_user_record($username, $password, $auth);
4259 $authplugin->sync_roles($user);
4261 foreach ($authsenabled as $hau) {
4262 $hauth = get_auth_plugin($hau);
4263 $hauth->user_authenticated_hook($user, $username, $password);
4266 if (empty($user->id)) {
4267 $failurereason = AUTH_LOGIN_NOUSER;
4268 return false;
4271 if (!empty($user->suspended)) {
4272 // just in case some auth plugin suspended account
4273 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4274 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4275 $failurereason = AUTH_LOGIN_SUSPENDED;
4276 return false;
4279 login_attempt_valid($user);
4280 $failurereason = AUTH_LOGIN_OK;
4281 return $user;
4284 // failed if all the plugins have failed
4285 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4286 if (debugging('', DEBUG_ALL)) {
4287 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Failed Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4290 if ($user->id) {
4291 login_attempt_failed($user);
4292 $failurereason = AUTH_LOGIN_FAILED;
4293 } else {
4294 $failurereason = AUTH_LOGIN_NOUSER;
4297 return false;
4301 * Call to complete the user login process after authenticate_user_login()
4302 * has succeeded. It will setup the $USER variable and other required bits
4303 * and pieces.
4305 * NOTE:
4306 * - It will NOT log anything -- up to the caller to decide what to log.
4307 * - this function does not set any cookies any more!
4309 * @param object $user
4310 * @return object A {@link $USER} object - BC only, do not use
4312 function complete_user_login($user) {
4313 global $CFG, $USER;
4315 // regenerate session id and delete old session,
4316 // this helps prevent session fixation attacks from the same domain
4317 session_regenerate_id(true);
4319 // let enrol plugins deal with new enrolments if necessary
4320 enrol_check_plugins($user);
4322 // check enrolments, load caps and setup $USER object
4323 session_set_user($user);
4325 // reload preferences from DB
4326 unset($USER->preference);
4327 check_user_preferences_loaded($USER);
4329 // update login times
4330 update_user_login_times();
4332 // extra session prefs init
4333 set_login_session_preferences();
4335 if (isguestuser()) {
4336 // no need to continue when user is THE guest
4337 return $USER;
4340 /// Select password change url
4341 $userauth = get_auth_plugin($USER->auth);
4343 /// check whether the user should be changing password
4344 if (get_user_preferences('auth_forcepasswordchange', false)){
4345 if ($userauth->can_change_password()) {
4346 if ($changeurl = $userauth->change_password_url()) {
4347 redirect($changeurl);
4348 } else {
4349 redirect($CFG->httpswwwroot.'/login/change_password.php');
4351 } else {
4352 print_error('nopasswordchangeforced', 'auth');
4355 return $USER;
4359 * Check a password hash to see if it was hashed using the
4360 * legacy hash algorithm (md5).
4362 * @param string $password String to check.
4363 * @return boolean True if the $password matches the format of an md5 sum.
4365 function password_is_legacy_hash($password) {
4366 return (bool) preg_match('/^[0-9a-f]{32}$/', $password);
4370 * Checks whether the password compatibility library will work with the current
4371 * version of PHP. This cannot be done using PHP version numbers since the fix
4372 * has been backported to earlier versions in some distributions.
4374 * See https://github.com/ircmaxell/password_compat/issues/10 for
4375 * more details.
4377 * @return bool True if the library is NOT supported.
4379 function password_compat_not_supported() {
4381 $hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG';
4383 // Create a one off application cache to store bcrypt support status as
4384 // the support status doesn't change and crypt() is slow.
4385 $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'password_compat');
4387 if (!$bcryptsupport = $cache->get('bcryptsupport')) {
4388 $test = crypt('password', $hash);
4389 // Cache string instead of boolean to avoid MDL-37472.
4390 if ($test == $hash) {
4391 $bcryptsupport = 'supported';
4392 } else {
4393 $bcryptsupport = 'not supported';
4395 $cache->set('bcryptsupport', $bcryptsupport);
4398 // Return true if bcrypt *not* supported.
4399 return ($bcryptsupport !== 'supported');
4403 * Compare password against hash stored in user object to determine if it is valid.
4405 * If necessary it also updates the stored hash to the current format.
4407 * @param stdClass $user (Password property may be updated).
4408 * @param string $password Plain text password.
4409 * @return bool True if password is valid.
4411 function validate_internal_user_password($user, $password) {
4412 global $CFG;
4413 require_once($CFG->libdir.'/password_compat/lib/password.php');
4415 if ($user->password === AUTH_PASSWORD_NOT_CACHED) {
4416 // Internal password is not used at all, it can not validate.
4417 return false;
4420 // If hash isn't a legacy (md5) hash, validate using the library function.
4421 if (!password_is_legacy_hash($user->password)) {
4422 return password_verify($password, $user->password);
4425 // Otherwise we need to check for a legacy (md5) hash instead. If the hash
4426 // is valid we can then update it to the new algorithm.
4428 $sitesalt = isset($CFG->passwordsaltmain) ? $CFG->passwordsaltmain : '';
4429 $validated = false;
4431 if ($user->password === md5($password.$sitesalt)
4432 or $user->password === md5($password)
4433 or $user->password === md5(addslashes($password).$sitesalt)
4434 or $user->password === md5(addslashes($password))) {
4435 // note: we are intentionally using the addslashes() here because we
4436 // need to accept old password hashes of passwords with magic quotes
4437 $validated = true;
4439 } else {
4440 for ($i=1; $i<=20; $i++) { //20 alternative salts should be enough, right?
4441 $alt = 'passwordsaltalt'.$i;
4442 if (!empty($CFG->$alt)) {
4443 if ($user->password === md5($password.$CFG->$alt) or $user->password === md5(addslashes($password).$CFG->$alt)) {
4444 $validated = true;
4445 break;
4451 if ($validated) {
4452 // If the password matches the existing md5 hash, update to the
4453 // current hash algorithm while we have access to the user's password.
4454 update_internal_user_password($user, $password);
4457 return $validated;
4461 * Calculate hash for a plain text password.
4463 * @param string $password Plain text password to be hashed.
4464 * @param bool $fasthash If true, use a low cost factor when generating the hash
4465 * This is much faster to generate but makes the hash
4466 * less secure. It is used when lots of hashes need to
4467 * be generated quickly.
4468 * @return string The hashed password.
4470 * @throws moodle_exception If a problem occurs while generating the hash.
4472 function hash_internal_user_password($password, $fasthash = false) {
4473 global $CFG;
4474 require_once($CFG->libdir.'/password_compat/lib/password.php');
4476 // Use the legacy hashing algorithm (md5) if PHP is not new enough
4477 // to support bcrypt properly
4478 if (password_compat_not_supported()) {
4479 if (isset($CFG->passwordsaltmain)) {
4480 return md5($password.$CFG->passwordsaltmain);
4481 } else {
4482 return md5($password);
4486 // Set the cost factor to 4 for fast hashing, otherwise use default cost.
4487 $options = ($fasthash) ? array('cost' => 4) : array();
4489 $generatedhash = password_hash($password, PASSWORD_DEFAULT, $options);
4491 if ($generatedhash === false) {
4492 throw new moodle_exception('Failed to generate password hash.');
4495 return $generatedhash;
4499 * Update password hash in user object (if necessary).
4501 * The password is updated if:
4502 * 1. The password has changed (the hash of $user->password is different
4503 * to the hash of $password).
4504 * 2. The existing hash is using an out-of-date algorithm (or the legacy
4505 * md5 algorithm).
4507 * Updating the password will modify the $user object and the database
4508 * record to use the current hashing algorithm.
4510 * @param stdClass $user User object (password property may be updated).
4511 * @param string $password Plain text password.
4512 * @return bool Always returns true.
4514 function update_internal_user_password($user, $password) {
4515 global $CFG, $DB;
4516 require_once($CFG->libdir.'/password_compat/lib/password.php');
4518 // Use the legacy hashing algorithm (md5) if PHP doesn't support
4519 // bcrypt properly.
4520 $legacyhash = password_compat_not_supported();
4522 // Figure out what the hashed password should be.
4523 $authplugin = get_auth_plugin($user->auth);
4524 if ($authplugin->prevent_local_passwords()) {
4525 $hashedpassword = AUTH_PASSWORD_NOT_CACHED;
4526 } else {
4527 $hashedpassword = hash_internal_user_password($password);
4530 if ($legacyhash) {
4531 $passwordchanged = ($user->password !== $hashedpassword);
4532 $algorithmchanged = false;
4533 } else {
4534 // If verification fails then it means the password has changed.
4535 $passwordchanged = !password_verify($password, $user->password);
4536 $algorithmchanged = password_needs_rehash($user->password, PASSWORD_DEFAULT);
4539 if ($passwordchanged || $algorithmchanged) {
4540 $DB->set_field('user', 'password', $hashedpassword, array('id'=>$user->id));
4541 $user->password = $hashedpassword;
4544 return true;
4548 * Get a complete user record, which includes all the info
4549 * in the user record.
4551 * Intended for setting as $USER session variable
4553 * @param string $field The user field to be checked for a given value.
4554 * @param string $value The value to match for $field.
4555 * @param int $mnethostid
4556 * @return mixed False, or A {@link $USER} object.
4558 function get_complete_user_data($field, $value, $mnethostid = null) {
4559 global $CFG, $DB;
4561 if (!$field || !$value) {
4562 return false;
4565 /// Build the WHERE clause for an SQL query
4566 $params = array('fieldval'=>$value);
4567 $constraints = "$field = :fieldval AND deleted <> 1";
4569 // If we are loading user data based on anything other than id,
4570 // we must also restrict our search based on mnet host.
4571 if ($field != 'id') {
4572 if (empty($mnethostid)) {
4573 // if empty, we restrict to local users
4574 $mnethostid = $CFG->mnet_localhost_id;
4577 if (!empty($mnethostid)) {
4578 $params['mnethostid'] = $mnethostid;
4579 $constraints .= " AND mnethostid = :mnethostid";
4582 /// Get all the basic user data
4584 if (! $user = $DB->get_record_select('user', $constraints, $params)) {
4585 return false;
4588 /// Get various settings and preferences
4590 // preload preference cache
4591 check_user_preferences_loaded($user);
4593 // load course enrolment related stuff
4594 $user->lastcourseaccess = array(); // during last session
4595 $user->currentcourseaccess = array(); // during current session
4596 if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid'=>$user->id))) {
4597 foreach ($lastaccesses as $lastaccess) {
4598 $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess;
4602 $sql = "SELECT g.id, g.courseid
4603 FROM {groups} g, {groups_members} gm
4604 WHERE gm.groupid=g.id AND gm.userid=?";
4606 // this is a special hack to speedup calendar display
4607 $user->groupmember = array();
4608 if (!isguestuser($user)) {
4609 if ($groups = $DB->get_records_sql($sql, array($user->id))) {
4610 foreach ($groups as $group) {
4611 if (!array_key_exists($group->courseid, $user->groupmember)) {
4612 $user->groupmember[$group->courseid] = array();
4614 $user->groupmember[$group->courseid][$group->id] = $group->id;
4619 /// Add the custom profile fields to the user record
4620 $user->profile = array();
4621 if (!isguestuser($user)) {
4622 require_once($CFG->dirroot.'/user/profile/lib.php');
4623 profile_load_custom_fields($user);
4626 /// Rewrite some variables if necessary
4627 if (!empty($user->description)) {
4628 $user->description = true; // No need to cart all of it around
4630 if (isguestuser($user)) {
4631 $user->lang = $CFG->lang; // Guest language always same as site
4632 $user->firstname = get_string('guestuser'); // Name always in current language
4633 $user->lastname = ' ';
4636 return $user;
4640 * Validate a password against the configured password policy
4642 * @global object
4643 * @param string $password the password to be checked against the password policy
4644 * @param string $errmsg the error message to display when the password doesn't comply with the policy.
4645 * @return bool true if the password is valid according to the policy. false otherwise.
4647 function check_password_policy($password, &$errmsg) {
4648 global $CFG;
4650 if (empty($CFG->passwordpolicy)) {
4651 return true;
4654 $errmsg = '';
4655 if (textlib::strlen($password) < $CFG->minpasswordlength) {
4656 $errmsg .= '<div>'. get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength) .'</div>';
4659 if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) {
4660 $errmsg .= '<div>'. get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits) .'</div>';
4663 if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) {
4664 $errmsg .= '<div>'. get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower) .'</div>';
4667 if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) {
4668 $errmsg .= '<div>'. get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper) .'</div>';
4671 if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) {
4672 $errmsg .= '<div>'. get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum) .'</div>';
4674 if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) {
4675 $errmsg .= '<div>'. get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars) .'</div>';
4678 if ($errmsg == '') {
4679 return true;
4680 } else {
4681 return false;
4687 * When logging in, this function is run to set certain preferences
4688 * for the current SESSION
4690 * @global object
4691 * @global object
4693 function set_login_session_preferences() {
4694 global $SESSION, $CFG;
4696 $SESSION->justloggedin = true;
4698 unset($SESSION->lang);
4703 * Delete a course, including all related data from the database,
4704 * and any associated files.
4706 * @global object
4707 * @global object
4708 * @param mixed $courseorid The id of the course or course object to delete.
4709 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4710 * @return bool true if all the removals succeeded. false if there were any failures. If this
4711 * method returns false, some of the removals will probably have succeeded, and others
4712 * failed, but you have no way of knowing which.
4714 function delete_course($courseorid, $showfeedback = true) {
4715 global $DB;
4717 if (is_object($courseorid)) {
4718 $courseid = $courseorid->id;
4719 $course = $courseorid;
4720 } else {
4721 $courseid = $courseorid;
4722 if (!$course = $DB->get_record('course', array('id'=>$courseid))) {
4723 return false;
4726 $context = context_course::instance($courseid);
4728 // frontpage course can not be deleted!!
4729 if ($courseid == SITEID) {
4730 return false;
4733 // make the course completely empty
4734 remove_course_contents($courseid, $showfeedback);
4736 // delete the course and related context instance
4737 delete_context(CONTEXT_COURSE, $courseid);
4739 // We will update the course's timemodified, as it will be passed to the course_deleted event,
4740 // which should know about this updated property, as this event is meant to pass the full course record
4741 $course->timemodified = time();
4743 $DB->delete_records("course", array("id" => $courseid));
4744 $DB->delete_records("course_format_options", array("courseid" => $courseid));
4746 //trigger events
4747 $course->context = $context; // you can not fetch context in the event because it was already deleted
4748 events_trigger('course_deleted', $course);
4750 return true;
4754 * Clear a course out completely, deleting all content
4755 * but don't delete the course itself.
4756 * This function does not verify any permissions.
4758 * Please note this function also deletes all user enrolments,
4759 * enrolment instances and role assignments by default.
4761 * $options:
4762 * - 'keep_roles_and_enrolments' - false by default
4763 * - 'keep_groups_and_groupings' - false by default
4765 * @param int $courseid The id of the course that is being deleted
4766 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4767 * @param array $options extra options
4768 * @return bool true if all the removals succeeded. false if there were any failures. If this
4769 * method returns false, some of the removals will probably have succeeded, and others
4770 * failed, but you have no way of knowing which.
4772 function remove_course_contents($courseid, $showfeedback = true, array $options = null) {
4773 global $CFG, $DB, $OUTPUT;
4774 require_once($CFG->libdir.'/completionlib.php');
4775 require_once($CFG->libdir.'/questionlib.php');
4776 require_once($CFG->libdir.'/gradelib.php');
4777 require_once($CFG->dirroot.'/group/lib.php');
4778 require_once($CFG->dirroot.'/tag/coursetagslib.php');
4779 require_once($CFG->dirroot.'/comment/lib.php');
4780 require_once($CFG->dirroot.'/rating/lib.php');
4782 // NOTE: these concatenated strings are suboptimal, but it is just extra info...
4783 $strdeleted = get_string('deleted').' - ';
4785 // Some crazy wishlist of stuff we should skip during purging of course content
4786 $options = (array)$options;
4788 $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST);
4789 $coursecontext = context_course::instance($courseid);
4790 $fs = get_file_storage();
4792 // Delete course completion information, this has to be done before grades and enrols
4793 $cc = new completion_info($course);
4794 $cc->clear_criteria();
4795 if ($showfeedback) {
4796 echo $OUTPUT->notification($strdeleted.get_string('completion', 'completion'), 'notifysuccess');
4799 // Remove all data from gradebook - this needs to be done before course modules
4800 // because while deleting this information, the system may need to reference
4801 // the course modules that own the grades.
4802 remove_course_grades($courseid, $showfeedback);
4803 remove_grade_letters($coursecontext, $showfeedback);
4805 // Delete course blocks in any all child contexts,
4806 // they may depend on modules so delete them first
4807 $childcontexts = $coursecontext->get_child_contexts(); // returns all subcontexts since 2.2
4808 foreach ($childcontexts as $childcontext) {
4809 blocks_delete_all_for_context($childcontext->id);
4811 unset($childcontexts);
4812 blocks_delete_all_for_context($coursecontext->id);
4813 if ($showfeedback) {
4814 echo $OUTPUT->notification($strdeleted.get_string('type_block_plural', 'plugin'), 'notifysuccess');
4817 // Delete every instance of every module,
4818 // this has to be done before deleting of course level stuff
4819 $locations = get_plugin_list('mod');
4820 foreach ($locations as $modname=>$moddir) {
4821 if ($modname === 'NEWMODULE') {
4822 continue;
4824 if ($module = $DB->get_record('modules', array('name'=>$modname))) {
4825 include_once("$moddir/lib.php"); // Shows php warning only if plugin defective
4826 $moddelete = $modname .'_delete_instance'; // Delete everything connected to an instance
4827 $moddeletecourse = $modname .'_delete_course'; // Delete other stray stuff (uncommon)
4829 if ($instances = $DB->get_records($modname, array('course'=>$course->id))) {
4830 foreach ($instances as $instance) {
4831 if ($cm = get_coursemodule_from_instance($modname, $instance->id, $course->id)) {
4832 /// Delete activity context questions and question categories
4833 question_delete_activity($cm, $showfeedback);
4835 if (function_exists($moddelete)) {
4836 // This purges all module data in related tables, extra user prefs, settings, etc.
4837 $moddelete($instance->id);
4838 } else {
4839 // NOTE: we should not allow installation of modules with missing delete support!
4840 debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!");
4841 $DB->delete_records($modname, array('id'=>$instance->id));
4844 if ($cm) {
4845 // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition
4846 context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4847 $DB->delete_records('course_modules', array('id'=>$cm->id));
4851 if (function_exists($moddeletecourse)) {
4852 // Execute ptional course cleanup callback
4853 $moddeletecourse($course, $showfeedback);
4855 if ($instances and $showfeedback) {
4856 echo $OUTPUT->notification($strdeleted.get_string('pluginname', $modname), 'notifysuccess');
4858 } else {
4859 // Ooops, this module is not properly installed, force-delete it in the next block
4863 // We have tried to delete everything the nice way - now let's force-delete any remaining module data
4865 // Remove all data from availability and completion tables that is associated
4866 // with course-modules belonging to this course. Note this is done even if the
4867 // features are not enabled now, in case they were enabled previously.
4868 $DB->delete_records_select('course_modules_completion',
4869 'coursemoduleid IN (SELECT id from {course_modules} WHERE course=?)',
4870 array($courseid));
4871 $DB->delete_records_select('course_modules_availability',
4872 'coursemoduleid IN (SELECT id from {course_modules} WHERE course=?)',
4873 array($courseid));
4874 $DB->delete_records_select('course_modules_avail_fields',
4875 'coursemoduleid IN (SELECT id from {course_modules} WHERE course=?)',
4876 array($courseid));
4878 // Remove course-module data.
4879 $cms = $DB->get_records('course_modules', array('course'=>$course->id));
4880 foreach ($cms as $cm) {
4881 if ($module = $DB->get_record('modules', array('id'=>$cm->module))) {
4882 try {
4883 $DB->delete_records($module->name, array('id'=>$cm->instance));
4884 } catch (Exception $e) {
4885 // Ignore weird or missing table problems
4888 context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4889 $DB->delete_records('course_modules', array('id'=>$cm->id));
4892 if ($showfeedback) {
4893 echo $OUTPUT->notification($strdeleted.get_string('type_mod_plural', 'plugin'), 'notifysuccess');
4896 // Cleanup the rest of plugins
4897 $cleanuplugintypes = array('report', 'coursereport', 'format');
4898 foreach ($cleanuplugintypes as $type) {
4899 $plugins = get_plugin_list_with_function($type, 'delete_course', 'lib.php');
4900 foreach ($plugins as $plugin=>$pluginfunction) {
4901 $pluginfunction($course->id, $showfeedback);
4903 if ($showfeedback) {
4904 echo $OUTPUT->notification($strdeleted.get_string('type_'.$type.'_plural', 'plugin'), 'notifysuccess');
4908 // Delete questions and question categories
4909 question_delete_course($course, $showfeedback);
4910 if ($showfeedback) {
4911 echo $OUTPUT->notification($strdeleted.get_string('questions', 'question'), 'notifysuccess');
4914 // Make sure there are no subcontexts left - all valid blocks and modules should be already gone
4915 $childcontexts = $coursecontext->get_child_contexts(); // returns all subcontexts since 2.2
4916 foreach ($childcontexts as $childcontext) {
4917 $childcontext->delete();
4919 unset($childcontexts);
4921 // Remove all roles and enrolments by default
4922 if (empty($options['keep_roles_and_enrolments'])) {
4923 // this hack is used in restore when deleting contents of existing course
4924 role_unassign_all(array('contextid'=>$coursecontext->id, 'component'=>''), true);
4925 enrol_course_delete($course);
4926 if ($showfeedback) {
4927 echo $OUTPUT->notification($strdeleted.get_string('type_enrol_plural', 'plugin'), 'notifysuccess');
4931 // Delete any groups, removing members and grouping/course links first.
4932 if (empty($options['keep_groups_and_groupings'])) {
4933 groups_delete_groupings($course->id, $showfeedback);
4934 groups_delete_groups($course->id, $showfeedback);
4937 // filters be gone!
4938 filter_delete_all_for_context($coursecontext->id);
4940 // die comments!
4941 comment::delete_comments($coursecontext->id);
4943 // ratings are history too
4944 $delopt = new stdclass();
4945 $delopt->contextid = $coursecontext->id;
4946 $rm = new rating_manager();
4947 $rm->delete_ratings($delopt);
4949 // Delete course tags
4950 coursetag_delete_course_tags($course->id, $showfeedback);
4952 // Delete calendar events
4953 $DB->delete_records('event', array('courseid'=>$course->id));
4954 $fs->delete_area_files($coursecontext->id, 'calendar');
4956 // Delete all related records in other core tables that may have a courseid
4957 // This array stores the tables that need to be cleared, as
4958 // table_name => column_name that contains the course id.
4959 $tablestoclear = array(
4960 'log' => 'course', // Course logs (NOTE: this might be changed in the future)
4961 'backup_courses' => 'courseid', // Scheduled backup stuff
4962 'user_lastaccess' => 'courseid', // User access info
4964 foreach ($tablestoclear as $table => $col) {
4965 $DB->delete_records($table, array($col=>$course->id));
4968 // delete all course backup files
4969 $fs->delete_area_files($coursecontext->id, 'backup');
4971 // cleanup course record - remove links to deleted stuff
4972 $oldcourse = new stdClass();
4973 $oldcourse->id = $course->id;
4974 $oldcourse->summary = '';
4975 $oldcourse->modinfo = NULL;
4976 $oldcourse->legacyfiles = 0;
4977 $oldcourse->enablecompletion = 0;
4978 if (!empty($options['keep_groups_and_groupings'])) {
4979 $oldcourse->defaultgroupingid = 0;
4981 $DB->update_record('course', $oldcourse);
4983 // Delete course sections and availability options.
4984 $DB->delete_records_select('course_sections_availability',
4985 'coursesectionid IN (SELECT id from {course_sections} WHERE course=?)',
4986 array($course->id));
4987 $DB->delete_records_select('course_sections_avail_fields',
4988 'coursesectionid IN (SELECT id from {course_sections} WHERE course=?)',
4989 array($course->id));
4990 $DB->delete_records('course_sections', array('course'=>$course->id));
4992 // delete legacy, section and any other course files
4993 $fs->delete_area_files($coursecontext->id, 'course'); // files from summary and section
4995 // Delete all remaining stuff linked to context such as files, comments, ratings, etc.
4996 if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) {
4997 // Easy, do not delete the context itself...
4998 $coursecontext->delete_content();
5000 } else {
5001 // Hack alert!!!!
5002 // We can not drop all context stuff because it would bork enrolments and roles,
5003 // there might be also files used by enrol plugins...
5006 // Delete legacy files - just in case some files are still left there after conversion to new file api,
5007 // also some non-standard unsupported plugins may try to store something there
5008 fulldelete($CFG->dataroot.'/'.$course->id);
5010 // Finally trigger the event
5011 $course->context = $coursecontext; // you can not access context in cron event later after course is deleted
5012 $course->options = $options; // not empty if we used any crazy hack
5013 events_trigger('course_content_removed', $course);
5015 return true;
5019 * Change dates in module - used from course reset.
5021 * @global object
5022 * @global object
5023 * @param string $modname forum, assignment, etc
5024 * @param array $fields array of date fields from mod table
5025 * @param int $timeshift time difference
5026 * @param int $courseid
5027 * @return bool success
5029 function shift_course_mod_dates($modname, $fields, $timeshift, $courseid) {
5030 global $CFG, $DB;
5031 include_once($CFG->dirroot.'/mod/'.$modname.'/lib.php');
5033 $return = true;
5034 foreach ($fields as $field) {
5035 $updatesql = "UPDATE {".$modname."}
5036 SET $field = $field + ?
5037 WHERE course=? AND $field<>0";
5038 $return = $DB->execute($updatesql, array($timeshift, $courseid)) && $return;
5041 $refreshfunction = $modname.'_refresh_events';
5042 if (function_exists($refreshfunction)) {
5043 $refreshfunction($courseid);
5046 return $return;
5050 * This function will empty a course of user data.
5051 * It will retain the activities and the structure of the course.
5053 * @param object $data an object containing all the settings including courseid (without magic quotes)
5054 * @return array status array of array component, item, error
5056 function reset_course_userdata($data) {
5057 global $CFG, $USER, $DB;
5058 require_once($CFG->libdir.'/gradelib.php');
5059 require_once($CFG->libdir.'/completionlib.php');
5060 require_once($CFG->dirroot.'/group/lib.php');
5062 $data->courseid = $data->id;
5063 $context = context_course::instance($data->courseid);
5065 // calculate the time shift of dates
5066 if (!empty($data->reset_start_date)) {
5067 // time part of course startdate should be zero
5068 $data->timeshift = $data->reset_start_date - usergetmidnight($data->reset_start_date_old);
5069 } else {
5070 $data->timeshift = 0;
5073 // result array: component, item, error
5074 $status = array();
5076 // start the resetting
5077 $componentstr = get_string('general');
5079 // move the course start time
5080 if (!empty($data->reset_start_date) and $data->timeshift) {
5081 // change course start data
5082 $DB->set_field('course', 'startdate', $data->reset_start_date, array('id'=>$data->courseid));
5083 // update all course and group events - do not move activity events
5084 $updatesql = "UPDATE {event}
5085 SET timestart = timestart + ?
5086 WHERE courseid=? AND instance=0";
5087 $DB->execute($updatesql, array($data->timeshift, $data->courseid));
5089 $status[] = array('component'=>$componentstr, 'item'=>get_string('datechanged'), 'error'=>false);
5092 if (!empty($data->reset_logs)) {
5093 $DB->delete_records('log', array('course'=>$data->courseid));
5094 $status[] = array('component'=>$componentstr, 'item'=>get_string('deletelogs'), 'error'=>false);
5097 if (!empty($data->reset_events)) {
5098 $DB->delete_records('event', array('courseid'=>$data->courseid));
5099 $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteevents', 'calendar'), 'error'=>false);
5102 if (!empty($data->reset_notes)) {
5103 require_once($CFG->dirroot.'/notes/lib.php');
5104 note_delete_all($data->courseid);
5105 $status[] = array('component'=>$componentstr, 'item'=>get_string('deletenotes', 'notes'), 'error'=>false);
5108 if (!empty($data->delete_blog_associations)) {
5109 require_once($CFG->dirroot.'/blog/lib.php');
5110 blog_remove_associations_for_course($data->courseid);
5111 $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteblogassociations', 'blog'), 'error'=>false);
5114 if (!empty($data->reset_completion)) {
5115 // Delete course and activity completion information.
5116 $course = $DB->get_record('course', array('id'=>$data->courseid));
5117 $cc = new completion_info($course);
5118 $cc->delete_all_completion_data();
5119 $status[] = array('component' => $componentstr,
5120 'item' => get_string('deletecompletiondata', 'completion'), 'error' => false);
5123 $componentstr = get_string('roles');
5125 if (!empty($data->reset_roles_overrides)) {
5126 $children = get_child_contexts($context);
5127 foreach ($children as $child) {
5128 $DB->delete_records('role_capabilities', array('contextid'=>$child->id));
5130 $DB->delete_records('role_capabilities', array('contextid'=>$context->id));
5131 //force refresh for logged in users
5132 mark_context_dirty($context->path);
5133 $status[] = array('component'=>$componentstr, 'item'=>get_string('deletecourseoverrides', 'role'), 'error'=>false);
5136 if (!empty($data->reset_roles_local)) {
5137 $children = get_child_contexts($context);
5138 foreach ($children as $child) {
5139 role_unassign_all(array('contextid'=>$child->id));
5141 //force refresh for logged in users
5142 mark_context_dirty($context->path);
5143 $status[] = array('component'=>$componentstr, 'item'=>get_string('deletelocalroles', 'role'), 'error'=>false);
5146 // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc.
5147 $data->unenrolled = array();
5148 if (!empty($data->unenrol_users)) {
5149 $plugins = enrol_get_plugins(true);
5150 $instances = enrol_get_instances($data->courseid, true);
5151 foreach ($instances as $key=>$instance) {
5152 if (!isset($plugins[$instance->enrol])) {
5153 unset($instances[$key]);
5154 continue;
5158 foreach($data->unenrol_users as $withroleid) {
5159 if ($withroleid) {
5160 $sql = "SELECT ue.*
5161 FROM {user_enrolments} ue
5162 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5163 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5164 JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)";
5165 $params = array('courseid'=>$data->courseid, 'roleid'=>$withroleid, 'courselevel'=>CONTEXT_COURSE);
5167 } else {
5168 // without any role assigned at course context
5169 $sql = "SELECT ue.*
5170 FROM {user_enrolments} ue
5171 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5172 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5173 LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid)
5174 WHERE ra.id IS NULL";
5175 $params = array('courseid'=>$data->courseid, 'courselevel'=>CONTEXT_COURSE);
5178 $rs = $DB->get_recordset_sql($sql, $params);
5179 foreach ($rs as $ue) {
5180 if (!isset($instances[$ue->enrolid])) {
5181 continue;
5183 $instance = $instances[$ue->enrolid];
5184 $plugin = $plugins[$instance->enrol];
5185 if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) {
5186 continue;
5189 $plugin->unenrol_user($instance, $ue->userid);
5190 $data->unenrolled[$ue->userid] = $ue->userid;
5192 $rs->close();
5195 if (!empty($data->unenrolled)) {
5196 $status[] = array('component'=>$componentstr, 'item'=>get_string('unenrol', 'enrol').' ('.count($data->unenrolled).')', 'error'=>false);
5200 $componentstr = get_string('groups');
5202 // remove all group members
5203 if (!empty($data->reset_groups_members)) {
5204 groups_delete_group_members($data->courseid);
5205 $status[] = array('component'=>$componentstr, 'item'=>get_string('removegroupsmembers', 'group'), 'error'=>false);
5208 // remove all groups
5209 if (!empty($data->reset_groups_remove)) {
5210 groups_delete_groups($data->courseid, false);
5211 $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteallgroups', 'group'), 'error'=>false);
5214 // remove all grouping members
5215 if (!empty($data->reset_groupings_members)) {
5216 groups_delete_groupings_groups($data->courseid, false);
5217 $status[] = array('component'=>$componentstr, 'item'=>get_string('removegroupingsmembers', 'group'), 'error'=>false);
5220 // remove all groupings
5221 if (!empty($data->reset_groupings_remove)) {
5222 groups_delete_groupings($data->courseid, false);
5223 $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteallgroupings', 'group'), 'error'=>false);
5226 // Look in every instance of every module for data to delete
5227 $unsupported_mods = array();
5228 if ($allmods = $DB->get_records('modules') ) {
5229 foreach ($allmods as $mod) {
5230 $modname = $mod->name;
5231 $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php';
5232 $moddeleteuserdata = $modname.'_reset_userdata'; // Function to delete user data
5233 if (file_exists($modfile)) {
5234 if (!$DB->count_records($modname, array('course'=>$data->courseid))) {
5235 continue; // Skip mods with no instances
5237 include_once($modfile);
5238 if (function_exists($moddeleteuserdata)) {
5239 $modstatus = $moddeleteuserdata($data);
5240 if (is_array($modstatus)) {
5241 $status = array_merge($status, $modstatus);
5242 } else {
5243 debugging('Module '.$modname.' returned incorrect staus - must be an array!');
5245 } else {
5246 $unsupported_mods[] = $mod;
5248 } else {
5249 debugging('Missing lib.php in '.$modname.' module!');
5254 // mention unsupported mods
5255 if (!empty($unsupported_mods)) {
5256 foreach($unsupported_mods as $mod) {
5257 $status[] = array('component'=>get_string('modulenameplural', $mod->name), 'item'=>'', 'error'=>get_string('resetnotimplemented'));
5262 $componentstr = get_string('gradebook', 'grades');
5263 // reset gradebook
5264 if (!empty($data->reset_gradebook_items)) {
5265 remove_course_grades($data->courseid, false);
5266 grade_grab_course_grades($data->courseid);
5267 grade_regrade_final_grades($data->courseid);
5268 $status[] = array('component'=>$componentstr, 'item'=>get_string('removeallcourseitems', 'grades'), 'error'=>false);
5270 } else if (!empty($data->reset_gradebook_grades)) {
5271 grade_course_reset($data->courseid);
5272 $status[] = array('component'=>$componentstr, 'item'=>get_string('removeallcoursegrades', 'grades'), 'error'=>false);
5274 // reset comments
5275 if (!empty($data->reset_comments)) {
5276 require_once($CFG->dirroot.'/comment/lib.php');
5277 comment::reset_course_page_comments($context);
5280 return $status;
5284 * Generate an email processing address
5286 * @param int $modid
5287 * @param string $modargs
5288 * @return string Returns email processing address
5290 function generate_email_processing_address($modid,$modargs) {
5291 global $CFG;
5293 $header = $CFG->mailprefix . substr(base64_encode(pack('C',$modid)),0,2).$modargs;
5294 return $header . substr(md5($header.get_site_identifier()),0,16).'@'.$CFG->maildomain;
5300 * @todo Finish documenting this function
5302 * @global object
5303 * @param string $modargs
5304 * @param string $body Currently unused
5306 function moodle_process_email($modargs,$body) {
5307 global $DB;
5309 // the first char should be an unencoded letter. We'll take this as an action
5310 switch ($modargs{0}) {
5311 case 'B': { // bounce
5312 list(,$userid) = unpack('V',base64_decode(substr($modargs,1,8)));
5313 if ($user = $DB->get_record("user", array('id'=>$userid), "id,email")) {
5314 // check the half md5 of their email
5315 $md5check = substr(md5($user->email),0,16);
5316 if ($md5check == substr($modargs, -16)) {
5317 set_bounce_count($user);
5319 // else maybe they've already changed it?
5322 break;
5323 // maybe more later?
5327 /// CORRESPONDENCE ////////////////////////////////////////////////
5330 * Get mailer instance, enable buffering, flush buffer or disable buffering.
5332 * @param string $action 'get', 'buffer', 'close' or 'flush'
5333 * @return moodle_phpmailer|null mailer instance if 'get' used or nothing
5335 function get_mailer($action='get') {
5336 global $CFG;
5338 static $mailer = null;
5339 static $counter = 0;
5341 if (!isset($CFG->smtpmaxbulk)) {
5342 $CFG->smtpmaxbulk = 1;
5345 if ($action == 'get') {
5346 $prevkeepalive = false;
5348 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5349 if ($counter < $CFG->smtpmaxbulk and !$mailer->IsError()) {
5350 $counter++;
5351 // reset the mailer
5352 $mailer->Priority = 3;
5353 $mailer->CharSet = 'UTF-8'; // our default
5354 $mailer->ContentType = "text/plain";
5355 $mailer->Encoding = "8bit";
5356 $mailer->From = "root@localhost";
5357 $mailer->FromName = "Root User";
5358 $mailer->Sender = "";
5359 $mailer->Subject = "";
5360 $mailer->Body = "";
5361 $mailer->AltBody = "";
5362 $mailer->ConfirmReadingTo = "";
5364 $mailer->ClearAllRecipients();
5365 $mailer->ClearReplyTos();
5366 $mailer->ClearAttachments();
5367 $mailer->ClearCustomHeaders();
5368 return $mailer;
5371 $prevkeepalive = $mailer->SMTPKeepAlive;
5372 get_mailer('flush');
5375 include_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php');
5376 $mailer = new moodle_phpmailer();
5378 $counter = 1;
5380 $mailer->Version = 'Moodle '.$CFG->version; // mailer version
5381 $mailer->PluginDir = $CFG->libdir.'/phpmailer/'; // plugin directory (eg smtp plugin)
5382 $mailer->CharSet = 'UTF-8';
5384 // some MTAs may do double conversion of LF if CRLF used, CRLF is required line ending in RFC 822bis
5385 if (isset($CFG->mailnewline) and $CFG->mailnewline == 'CRLF') {
5386 $mailer->LE = "\r\n";
5387 } else {
5388 $mailer->LE = "\n";
5391 if ($CFG->smtphosts == 'qmail') {
5392 $mailer->IsQmail(); // use Qmail system
5394 } else if (empty($CFG->smtphosts)) {
5395 $mailer->IsMail(); // use PHP mail() = sendmail
5397 } else {
5398 $mailer->IsSMTP(); // use SMTP directly
5399 if (!empty($CFG->debugsmtp)) {
5400 $mailer->SMTPDebug = true;
5402 $mailer->Host = $CFG->smtphosts; // specify main and backup servers
5403 $mailer->SMTPSecure = $CFG->smtpsecure; // specify secure connection protocol
5404 $mailer->SMTPKeepAlive = $prevkeepalive; // use previous keepalive
5406 if ($CFG->smtpuser) { // Use SMTP authentication
5407 $mailer->SMTPAuth = true;
5408 $mailer->Username = $CFG->smtpuser;
5409 $mailer->Password = $CFG->smtppass;
5413 return $mailer;
5416 $nothing = null;
5418 // keep smtp session open after sending
5419 if ($action == 'buffer') {
5420 if (!empty($CFG->smtpmaxbulk)) {
5421 get_mailer('flush');
5422 $m = get_mailer();
5423 if ($m->Mailer == 'smtp') {
5424 $m->SMTPKeepAlive = true;
5427 return $nothing;
5430 // close smtp session, but continue buffering
5431 if ($action == 'flush') {
5432 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5433 if (!empty($mailer->SMTPDebug)) {
5434 echo '<pre>'."\n";
5436 $mailer->SmtpClose();
5437 if (!empty($mailer->SMTPDebug)) {
5438 echo '</pre>';
5441 return $nothing;
5444 // close smtp session, do not buffer anymore
5445 if ($action == 'close') {
5446 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5447 get_mailer('flush');
5448 $mailer->SMTPKeepAlive = false;
5450 $mailer = null; // better force new instance
5451 return $nothing;
5456 * Send an email to a specified user
5458 * @global object
5459 * @global string
5460 * @global string IdentityProvider(IDP) URL user hits to jump to mnet peer.
5461 * @uses SITEID
5462 * @param stdClass $user A {@link $USER} object
5463 * @param stdClass $from A {@link $USER} object
5464 * @param string $subject plain text subject line of the email
5465 * @param string $messagetext plain text version of the message
5466 * @param string $messagehtml complete html version of the message (optional)
5467 * @param string $attachment a file on the filesystem, relative to $CFG->dataroot
5468 * @param string $attachname the name of the file (extension indicates MIME)
5469 * @param bool $usetrueaddress determines whether $from email address should
5470 * be sent out. Will be overruled by user profile setting for maildisplay
5471 * @param string $replyto Email address to reply to
5472 * @param string $replytoname Name of reply to recipient
5473 * @param int $wordwrapwidth custom word wrap width, default 79
5474 * @return bool Returns true if mail was sent OK and false if there was an error.
5476 function email_to_user($user, $from, $subject, $messagetext, $messagehtml='', $attachment='', $attachname='', $usetrueaddress=true, $replyto='', $replytoname='', $wordwrapwidth=79) {
5478 global $CFG;
5480 if (empty($user) || empty($user->email)) {
5481 $nulluser = 'User is null or has no email';
5482 error_log($nulluser);
5483 if (CLI_SCRIPT) {
5484 mtrace('Error: lib/moodlelib.php email_to_user(): '.$nulluser);
5486 return false;
5489 if (!empty($user->deleted)) {
5490 // do not mail deleted users
5491 $userdeleted = 'User is deleted';
5492 error_log($userdeleted);
5493 if (CLI_SCRIPT) {
5494 mtrace('Error: lib/moodlelib.php email_to_user(): '.$userdeleted);
5496 return false;
5499 if (!empty($CFG->noemailever)) {
5500 // hidden setting for development sites, set in config.php if needed
5501 $noemail = 'Not sending email due to noemailever config setting';
5502 error_log($noemail);
5503 if (CLI_SCRIPT) {
5504 mtrace('Error: lib/moodlelib.php email_to_user(): '.$noemail);
5506 return true;
5509 if (!empty($CFG->divertallemailsto)) {
5510 $subject = "[DIVERTED {$user->email}] $subject";
5511 $user = clone($user);
5512 $user->email = $CFG->divertallemailsto;
5515 // skip mail to suspended users
5516 if ((isset($user->auth) && $user->auth=='nologin') or (isset($user->suspended) && $user->suspended)) {
5517 return true;
5520 if (!validate_email($user->email)) {
5521 // we can not send emails to invalid addresses - it might create security issue or confuse the mailer
5522 $invalidemail = "User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending.";
5523 error_log($invalidemail);
5524 if (CLI_SCRIPT) {
5525 mtrace('Error: lib/moodlelib.php email_to_user(): '.$invalidemail);
5527 return false;
5530 if (over_bounce_threshold($user)) {
5531 $bouncemsg = "User $user->id (".fullname($user).") is over bounce threshold! Not sending.";
5532 error_log($bouncemsg);
5533 if (CLI_SCRIPT) {
5534 mtrace('Error: lib/moodlelib.php email_to_user(): '.$bouncemsg);
5536 return false;
5539 // If the user is a remote mnet user, parse the email text for URL to the
5540 // wwwroot and modify the url to direct the user's browser to login at their
5541 // home site (identity provider - idp) before hitting the link itself
5542 if (is_mnet_remote_user($user)) {
5543 require_once($CFG->dirroot.'/mnet/lib.php');
5545 $jumpurl = mnet_get_idp_jump_url($user);
5546 $callback = partial('mnet_sso_apply_indirection', $jumpurl);
5548 $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%",
5549 $callback,
5550 $messagetext);
5551 $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%",
5552 $callback,
5553 $messagehtml);
5555 $mail = get_mailer();
5557 if (!empty($mail->SMTPDebug)) {
5558 echo '<pre>' . "\n";
5561 $temprecipients = array();
5562 $tempreplyto = array();
5564 $supportuser = generate_email_supportuser();
5566 // make up an email address for handling bounces
5567 if (!empty($CFG->handlebounces)) {
5568 $modargs = 'B'.base64_encode(pack('V',$user->id)).substr(md5($user->email),0,16);
5569 $mail->Sender = generate_email_processing_address(0,$modargs);
5570 } else {
5571 $mail->Sender = $supportuser->email;
5574 if (is_string($from)) { // So we can pass whatever we want if there is need
5575 $mail->From = $CFG->noreplyaddress;
5576 $mail->FromName = $from;
5577 } else if ($usetrueaddress and $from->maildisplay) {
5578 $mail->From = $from->email;
5579 $mail->FromName = fullname($from);
5580 } else {
5581 $mail->From = $CFG->noreplyaddress;
5582 $mail->FromName = fullname($from);
5583 if (empty($replyto)) {
5584 $tempreplyto[] = array($CFG->noreplyaddress, get_string('noreplyname'));
5588 if (!empty($replyto)) {
5589 $tempreplyto[] = array($replyto, $replytoname);
5592 $mail->Subject = substr($subject, 0, 900);
5594 $temprecipients[] = array($user->email, fullname($user));
5596 $mail->WordWrap = $wordwrapwidth; // set word wrap
5598 if (!empty($from->customheaders)) { // Add custom headers
5599 if (is_array($from->customheaders)) {
5600 foreach ($from->customheaders as $customheader) {
5601 $mail->AddCustomHeader($customheader);
5603 } else {
5604 $mail->AddCustomHeader($from->customheaders);
5608 if (!empty($from->priority)) {
5609 $mail->Priority = $from->priority;
5612 if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) { // Don't ever send HTML to users who don't want it
5613 $mail->IsHTML(true);
5614 $mail->Encoding = 'quoted-printable'; // Encoding to use
5615 $mail->Body = $messagehtml;
5616 $mail->AltBody = "\n$messagetext\n";
5617 } else {
5618 $mail->IsHTML(false);
5619 $mail->Body = "\n$messagetext\n";
5622 if ($attachment && $attachname) {
5623 if (preg_match( "~\\.\\.~" ,$attachment )) { // Security check for ".." in dir path
5624 $temprecipients[] = array($supportuser->email, fullname($supportuser, true));
5625 $mail->AddStringAttachment('Error in attachment. User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain');
5626 } else {
5627 require_once($CFG->libdir.'/filelib.php');
5628 $mimetype = mimeinfo('type', $attachname);
5629 $mail->AddAttachment($CFG->dataroot .'/'. $attachment, $attachname, 'base64', $mimetype);
5633 // Check if the email should be sent in an other charset then the default UTF-8
5634 if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) {
5636 // use the defined site mail charset or eventually the one preferred by the recipient
5637 $charset = $CFG->sitemailcharset;
5638 if (!empty($CFG->allowusermailcharset)) {
5639 if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) {
5640 $charset = $useremailcharset;
5644 // convert all the necessary strings if the charset is supported
5645 $charsets = get_list_of_charsets();
5646 unset($charsets['UTF-8']);
5647 if (in_array($charset, $charsets)) {
5648 $mail->CharSet = $charset;
5649 $mail->FromName = textlib::convert($mail->FromName, 'utf-8', strtolower($charset));
5650 $mail->Subject = textlib::convert($mail->Subject, 'utf-8', strtolower($charset));
5651 $mail->Body = textlib::convert($mail->Body, 'utf-8', strtolower($charset));
5652 $mail->AltBody = textlib::convert($mail->AltBody, 'utf-8', strtolower($charset));
5654 foreach ($temprecipients as $key => $values) {
5655 $temprecipients[$key][1] = textlib::convert($values[1], 'utf-8', strtolower($charset));
5657 foreach ($tempreplyto as $key => $values) {
5658 $tempreplyto[$key][1] = textlib::convert($values[1], 'utf-8', strtolower($charset));
5663 foreach ($temprecipients as $values) {
5664 $mail->AddAddress($values[0], $values[1]);
5666 foreach ($tempreplyto as $values) {
5667 $mail->AddReplyTo($values[0], $values[1]);
5670 if ($mail->Send()) {
5671 set_send_count($user);
5672 if (!empty($mail->SMTPDebug)) {
5673 echo '</pre>';
5675 return true;
5676 } else {
5677 add_to_log(SITEID, 'library', 'mailer', qualified_me(), 'ERROR: '. $mail->ErrorInfo);
5678 if (CLI_SCRIPT) {
5679 mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo);
5681 if (!empty($mail->SMTPDebug)) {
5682 echo '</pre>';
5684 return false;
5689 * Generate a signoff for emails based on support settings
5691 * @global object
5692 * @return string
5694 function generate_email_signoff() {
5695 global $CFG;
5697 $signoff = "\n";
5698 if (!empty($CFG->supportname)) {
5699 $signoff .= $CFG->supportname."\n";
5701 if (!empty($CFG->supportemail)) {
5702 $signoff .= $CFG->supportemail."\n";
5704 if (!empty($CFG->supportpage)) {
5705 $signoff .= $CFG->supportpage."\n";
5707 return $signoff;
5711 * Generate a fake user for emails based on support settings
5712 * @global object
5713 * @return object user info
5715 function generate_email_supportuser() {
5716 global $CFG;
5718 static $supportuser;
5720 if (!empty($supportuser)) {
5721 return $supportuser;
5724 $supportuser = new stdClass();
5725 $supportuser->email = $CFG->supportemail ? $CFG->supportemail : $CFG->noreplyaddress;
5726 $supportuser->firstname = $CFG->supportname ? $CFG->supportname : get_string('noreplyname');
5727 $supportuser->lastname = '';
5728 $supportuser->maildisplay = true;
5730 return $supportuser;
5735 * Sets specified user's password and send the new password to the user via email.
5737 * @global object
5738 * @global object
5739 * @param user $user A {@link $USER} object
5740 * @param boolean $fasthash If true, use a low cost factor when generating the hash for speed.
5741 * @return boolean|string Returns "true" if mail was sent OK and "false" if there was an error
5743 function setnew_password_and_mail($user, $fasthash = false) {
5744 global $CFG, $DB;
5746 // we try to send the mail in language the user understands,
5747 // unfortunately the filter_string() does not support alternative langs yet
5748 // so multilang will not work properly for site->fullname
5749 $lang = empty($user->lang) ? $CFG->lang : $user->lang;
5751 $site = get_site();
5753 $supportuser = generate_email_supportuser();
5755 $newpassword = generate_password();
5757 $hashedpassword = hash_internal_user_password($newpassword, $fasthash);
5758 $DB->set_field('user', 'password', $hashedpassword, array('id'=>$user->id));
5760 $a = new stdClass();
5761 $a->firstname = fullname($user, true);
5762 $a->sitename = format_string($site->fullname);
5763 $a->username = $user->username;
5764 $a->newpassword = $newpassword;
5765 $a->link = $CFG->wwwroot .'/login/';
5766 $a->signoff = generate_email_signoff();
5768 $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang);
5770 $subject = format_string($site->fullname) .': '. (string)new lang_string('newusernewpasswordsubj', '', $a, $lang);
5772 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5773 return email_to_user($user, $supportuser, $subject, $message);
5778 * Resets specified user's password and send the new password to the user via email.
5780 * @param stdClass $user A {@link $USER} object
5781 * @return bool Returns true if mail was sent OK and false if there was an error.
5783 function reset_password_and_mail($user) {
5784 global $CFG;
5786 $site = get_site();
5787 $supportuser = generate_email_supportuser();
5789 $userauth = get_auth_plugin($user->auth);
5790 if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) {
5791 trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth.");
5792 return false;
5795 $newpassword = generate_password();
5797 if (!$userauth->user_update_password($user, $newpassword)) {
5798 print_error("cannotsetpassword");
5801 $a = new stdClass();
5802 $a->firstname = $user->firstname;
5803 $a->lastname = $user->lastname;
5804 $a->sitename = format_string($site->fullname);
5805 $a->username = $user->username;
5806 $a->newpassword = $newpassword;
5807 $a->link = $CFG->httpswwwroot .'/login/change_password.php';
5808 $a->signoff = generate_email_signoff();
5810 $message = get_string('newpasswordtext', '', $a);
5812 $subject = format_string($site->fullname) .': '. get_string('changedpassword');
5814 unset_user_preference('create_password', $user); // prevent cron from generating the password
5816 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5817 return email_to_user($user, $supportuser, $subject, $message);
5822 * Send email to specified user with confirmation text and activation link.
5824 * @global object
5825 * @param user $user A {@link $USER} object
5826 * @return bool Returns true if mail was sent OK and false if there was an error.
5828 function send_confirmation_email($user) {
5829 global $CFG;
5831 $site = get_site();
5832 $supportuser = generate_email_supportuser();
5834 $data = new stdClass();
5835 $data->firstname = fullname($user);
5836 $data->sitename = format_string($site->fullname);
5837 $data->admin = generate_email_signoff();
5839 $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname));
5841 $username = urlencode($user->username);
5842 $username = str_replace('.', '%2E', $username); // prevent problems with trailing dots
5843 $data->link = $CFG->wwwroot .'/login/confirm.php?data='. $user->secret .'/'. $username;
5844 $message = get_string('emailconfirmation', '', $data);
5845 $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true);
5847 $user->mailformat = 1; // Always send HTML version as well
5849 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5850 return email_to_user($user, $supportuser, $subject, $message, $messagehtml);
5855 * send_password_change_confirmation_email.
5857 * @global object
5858 * @param user $user A {@link $USER} object
5859 * @return bool Returns true if mail was sent OK and false if there was an error.
5861 function send_password_change_confirmation_email($user) {
5862 global $CFG;
5864 $site = get_site();
5865 $supportuser = generate_email_supportuser();
5867 $data = new stdClass();
5868 $data->firstname = $user->firstname;
5869 $data->lastname = $user->lastname;
5870 $data->sitename = format_string($site->fullname);
5871 $data->link = $CFG->httpswwwroot .'/login/forgot_password.php?p='. $user->secret .'&s='. urlencode($user->username);
5872 $data->admin = generate_email_signoff();
5874 $message = get_string('emailpasswordconfirmation', '', $data);
5875 $subject = get_string('emailpasswordconfirmationsubject', '', format_string($site->fullname));
5877 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5878 return email_to_user($user, $supportuser, $subject, $message);
5883 * send_password_change_info.
5885 * @global object
5886 * @param user $user A {@link $USER} object
5887 * @return bool Returns true if mail was sent OK and false if there was an error.
5889 function send_password_change_info($user) {
5890 global $CFG;
5892 $site = get_site();
5893 $supportuser = generate_email_supportuser();
5894 $systemcontext = context_system::instance();
5896 $data = new stdClass();
5897 $data->firstname = $user->firstname;
5898 $data->lastname = $user->lastname;
5899 $data->sitename = format_string($site->fullname);
5900 $data->admin = generate_email_signoff();
5902 $userauth = get_auth_plugin($user->auth);
5904 if (!is_enabled_auth($user->auth) or $user->auth == 'nologin') {
5905 $message = get_string('emailpasswordchangeinfodisabled', '', $data);
5906 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
5907 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5908 return email_to_user($user, $supportuser, $subject, $message);
5911 if ($userauth->can_change_password() and $userauth->change_password_url()) {
5912 // we have some external url for password changing
5913 $data->link .= $userauth->change_password_url();
5915 } else {
5916 //no way to change password, sorry
5917 $data->link = '';
5920 if (!empty($data->link) and has_capability('moodle/user:changeownpassword', $systemcontext, $user->id)) {
5921 $message = get_string('emailpasswordchangeinfo', '', $data);
5922 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
5923 } else {
5924 $message = get_string('emailpasswordchangeinfofail', '', $data);
5925 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
5928 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5929 return email_to_user($user, $supportuser, $subject, $message);
5934 * Check that an email is allowed. It returns an error message if there
5935 * was a problem.
5937 * @global object
5938 * @param string $email Content of email
5939 * @return string|false
5941 function email_is_not_allowed($email) {
5942 global $CFG;
5944 if (!empty($CFG->allowemailaddresses)) {
5945 $allowed = explode(' ', $CFG->allowemailaddresses);
5946 foreach ($allowed as $allowedpattern) {
5947 $allowedpattern = trim($allowedpattern);
5948 if (!$allowedpattern) {
5949 continue;
5951 if (strpos($allowedpattern, '.') === 0) {
5952 if (strpos(strrev($email), strrev($allowedpattern)) === 0) {
5953 // subdomains are in a form ".example.com" - matches "xxx@anything.example.com"
5954 return false;
5957 } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) { // Match! (bug 5250)
5958 return false;
5961 return get_string('emailonlyallowed', '', $CFG->allowemailaddresses);
5963 } else if (!empty($CFG->denyemailaddresses)) {
5964 $denied = explode(' ', $CFG->denyemailaddresses);
5965 foreach ($denied as $deniedpattern) {
5966 $deniedpattern = trim($deniedpattern);
5967 if (!$deniedpattern) {
5968 continue;
5970 if (strpos($deniedpattern, '.') === 0) {
5971 if (strpos(strrev($email), strrev($deniedpattern)) === 0) {
5972 // subdomains are in a form ".example.com" - matches "xxx@anything.example.com"
5973 return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
5976 } else if (strpos(strrev($email), strrev('@'.$deniedpattern)) === 0) { // Match! (bug 5250)
5977 return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
5982 return false;
5985 /// FILE HANDLING /////////////////////////////////////////////
5988 * Returns local file storage instance
5990 * @return file_storage
5992 function get_file_storage() {
5993 global $CFG;
5995 static $fs = null;
5997 if ($fs) {
5998 return $fs;
6001 require_once("$CFG->libdir/filelib.php");
6003 if (isset($CFG->filedir)) {
6004 $filedir = $CFG->filedir;
6005 } else {
6006 $filedir = $CFG->dataroot.'/filedir';
6009 if (isset($CFG->trashdir)) {
6010 $trashdirdir = $CFG->trashdir;
6011 } else {
6012 $trashdirdir = $CFG->dataroot.'/trashdir';
6015 $fs = new file_storage($filedir, $trashdirdir, "$CFG->tempdir/filestorage", $CFG->directorypermissions, $CFG->filepermissions);
6017 return $fs;
6021 * Returns local file storage instance
6023 * @return file_browser
6025 function get_file_browser() {
6026 global $CFG;
6028 static $fb = null;
6030 if ($fb) {
6031 return $fb;
6034 require_once("$CFG->libdir/filelib.php");
6036 $fb = new file_browser();
6038 return $fb;
6042 * Returns file packer
6044 * @param string $mimetype default application/zip
6045 * @return file_packer
6047 function get_file_packer($mimetype='application/zip') {
6048 global $CFG;
6050 static $fp = array();
6052 if (isset($fp[$mimetype])) {
6053 return $fp[$mimetype];
6056 switch ($mimetype) {
6057 case 'application/zip':
6058 case 'application/vnd.moodle.backup':
6059 $classname = 'zip_packer';
6060 break;
6061 case 'application/x-tar':
6062 // $classname = 'tar_packer';
6063 // break;
6064 default:
6065 return false;
6068 require_once("$CFG->libdir/filestorage/$classname.php");
6069 $fp[$mimetype] = new $classname();
6071 return $fp[$mimetype];
6075 * Returns current name of file on disk if it exists.
6077 * @param string $newfile File to be verified
6078 * @return string Current name of file on disk if true
6080 function valid_uploaded_file($newfile) {
6081 if (empty($newfile)) {
6082 return '';
6084 if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) {
6085 return $newfile['tmp_name'];
6086 } else {
6087 return '';
6092 * Returns the maximum size for uploading files.
6094 * There are seven possible upload limits:
6095 * 1. in Apache using LimitRequestBody (no way of checking or changing this)
6096 * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
6097 * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
6098 * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
6099 * 5. by the Moodle admin in $CFG->maxbytes
6100 * 6. by the teacher in the current course $course->maxbytes
6101 * 7. by the teacher for the current module, eg $assignment->maxbytes
6103 * These last two are passed to this function as arguments (in bytes).
6104 * Anything defined as 0 is ignored.
6105 * The smallest of all the non-zero numbers is returned.
6107 * @todo Finish documenting this function
6109 * @param int $sizebytes Set maximum size
6110 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6111 * @param int $modulebytes Current module ->maxbytes (in bytes)
6112 * @return int The maximum size for uploading files.
6114 function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0) {
6116 if (! $filesize = ini_get('upload_max_filesize')) {
6117 $filesize = '5M';
6119 $minimumsize = get_real_size($filesize);
6121 if ($postsize = ini_get('post_max_size')) {
6122 $postsize = get_real_size($postsize);
6123 if ($postsize < $minimumsize) {
6124 $minimumsize = $postsize;
6128 if (($sitebytes > 0) and ($sitebytes < $minimumsize)) {
6129 $minimumsize = $sitebytes;
6132 if (($coursebytes > 0) and ($coursebytes < $minimumsize)) {
6133 $minimumsize = $coursebytes;
6136 if (($modulebytes > 0) and ($modulebytes < $minimumsize)) {
6137 $minimumsize = $modulebytes;
6140 return $minimumsize;
6144 * Returns the maximum size for uploading files for the current user
6146 * This function takes in account @see:get_max_upload_file_size() the user's capabilities
6148 * @param context $context The context in which to check user capabilities
6149 * @param int $sizebytes Set maximum size
6150 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6151 * @param int $modulebytes Current module ->maxbytes (in bytes)
6152 * @param stdClass The user
6153 * @return int The maximum size for uploading files.
6155 function get_user_max_upload_file_size($context, $sitebytes=0, $coursebytes=0, $modulebytes=0, $user=null) {
6156 global $USER;
6158 if (empty($user)) {
6159 $user = $USER;
6162 if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) {
6163 return USER_CAN_IGNORE_FILE_SIZE_LIMITS;
6166 return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes);
6170 * Returns an array of possible sizes in local language
6172 * Related to {@link get_max_upload_file_size()} - this function returns an
6173 * array of possible sizes in an array, translated to the
6174 * local language.
6176 * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes.
6178 * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)"
6179 * with the value set to 0. This option will be the first in the list.
6181 * @global object
6182 * @uses SORT_NUMERIC
6183 * @param int $sizebytes Set maximum size
6184 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6185 * @param int $modulebytes Current module ->maxbytes (in bytes)
6186 * @param int|array $custombytes custom upload size/s which will be added to list,
6187 * Only value/s smaller then maxsize will be added to list.
6188 * @return array
6190 function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null) {
6191 global $CFG;
6193 if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) {
6194 return array();
6197 $filesize = array();
6198 $filesize[(string)intval($maxsize)] = display_size($maxsize);
6200 $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152,
6201 5242880, 10485760, 20971520, 52428800, 104857600);
6203 // If custombytes is given and is valid then add it to the list.
6204 if (is_number($custombytes) and $custombytes > 0) {
6205 $custombytes = (int)$custombytes;
6206 if (!in_array($custombytes, $sizelist)) {
6207 $sizelist[] = $custombytes;
6209 } else if (is_array($custombytes)) {
6210 $sizelist = array_unique(array_merge($sizelist, $custombytes));
6213 // Allow maxbytes to be selected if it falls outside the above boundaries
6214 if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) {
6215 // note: get_real_size() is used in order to prevent problems with invalid values
6216 $sizelist[] = get_real_size($CFG->maxbytes);
6219 foreach ($sizelist as $sizebytes) {
6220 if ($sizebytes < $maxsize && $sizebytes > 0) {
6221 $filesize[(string)intval($sizebytes)] = display_size($sizebytes);
6225 krsort($filesize, SORT_NUMERIC);
6226 $limitlevel = '';
6227 $displaysize = '';
6228 if ($modulebytes &&
6229 (($modulebytes < $coursebytes || $coursebytes == 0) &&
6230 ($modulebytes < $sitebytes || $sitebytes == 0))) {
6231 $limitlevel = get_string('activity', 'core');
6232 $displaysize = display_size($modulebytes);
6233 } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) {
6234 $limitlevel = get_string('course', 'core');
6235 $displaysize = display_size($coursebytes);
6236 } else if ($sitebytes) {
6237 $limitlevel = get_string('site', 'core');
6238 $displaysize = display_size($sitebytes);
6241 if ($limitlevel) {
6242 $params = (object) array('contextname'=>$limitlevel, 'displaysize'=>$displaysize);
6243 $filesize = array('0'=>get_string('uploadlimitwithsize', 'core', $params)) + $filesize;
6246 return $filesize;
6250 * Returns an array with all the filenames in all subdirectories, relative to the given rootdir.
6252 * If excludefiles is defined, then that file/directory is ignored
6253 * If getdirs is true, then (sub)directories are included in the output
6254 * If getfiles is true, then files are included in the output
6255 * (at least one of these must be true!)
6257 * @todo Finish documenting this function. Add examples of $excludefile usage.
6259 * @param string $rootdir A given root directory to start from
6260 * @param string|array $excludefile If defined then the specified file/directory is ignored
6261 * @param bool $descend If true then subdirectories are recursed as well
6262 * @param bool $getdirs If true then (sub)directories are included in the output
6263 * @param bool $getfiles If true then files are included in the output
6264 * @return array An array with all the filenames in
6265 * all subdirectories, relative to the given rootdir
6267 function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) {
6269 $dirs = array();
6271 if (!$getdirs and !$getfiles) { // Nothing to show
6272 return $dirs;
6275 if (!is_dir($rootdir)) { // Must be a directory
6276 return $dirs;
6279 if (!$dir = opendir($rootdir)) { // Can't open it for some reason
6280 return $dirs;
6283 if (!is_array($excludefiles)) {
6284 $excludefiles = array($excludefiles);
6287 while (false !== ($file = readdir($dir))) {
6288 $firstchar = substr($file, 0, 1);
6289 if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) {
6290 continue;
6292 $fullfile = $rootdir .'/'. $file;
6293 if (filetype($fullfile) == 'dir') {
6294 if ($getdirs) {
6295 $dirs[] = $file;
6297 if ($descend) {
6298 $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles);
6299 foreach ($subdirs as $subdir) {
6300 $dirs[] = $file .'/'. $subdir;
6303 } else if ($getfiles) {
6304 $dirs[] = $file;
6307 closedir($dir);
6309 asort($dirs);
6311 return $dirs;
6316 * Adds up all the files in a directory and works out the size.
6318 * @todo Finish documenting this function
6320 * @param string $rootdir The directory to start from
6321 * @param string $excludefile A file to exclude when summing directory size
6322 * @return int The summed size of all files and subfiles within the root directory
6324 function get_directory_size($rootdir, $excludefile='') {
6325 global $CFG;
6327 // do it this way if we can, it's much faster
6328 if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) {
6329 $command = trim($CFG->pathtodu).' -sk '.escapeshellarg($rootdir);
6330 $output = null;
6331 $return = null;
6332 exec($command,$output,$return);
6333 if (is_array($output)) {
6334 return get_real_size(intval($output[0]).'k'); // we told it to return k.
6338 if (!is_dir($rootdir)) { // Must be a directory
6339 return 0;
6342 if (!$dir = @opendir($rootdir)) { // Can't open it for some reason
6343 return 0;
6346 $size = 0;
6348 while (false !== ($file = readdir($dir))) {
6349 $firstchar = substr($file, 0, 1);
6350 if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) {
6351 continue;
6353 $fullfile = $rootdir .'/'. $file;
6354 if (filetype($fullfile) == 'dir') {
6355 $size += get_directory_size($fullfile, $excludefile);
6356 } else {
6357 $size += filesize($fullfile);
6360 closedir($dir);
6362 return $size;
6366 * Converts bytes into display form
6368 * @todo Finish documenting this function. Verify return type.
6370 * @staticvar string $gb Localized string for size in gigabytes
6371 * @staticvar string $mb Localized string for size in megabytes
6372 * @staticvar string $kb Localized string for size in kilobytes
6373 * @staticvar string $b Localized string for size in bytes
6374 * @param int $size The size to convert to human readable form
6375 * @return string
6377 function display_size($size) {
6379 static $gb, $mb, $kb, $b;
6381 if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) {
6382 return get_string('unlimited');
6385 if (empty($gb)) {
6386 $gb = get_string('sizegb');
6387 $mb = get_string('sizemb');
6388 $kb = get_string('sizekb');
6389 $b = get_string('sizeb');
6392 if ($size >= 1073741824) {
6393 $size = round($size / 1073741824 * 10) / 10 . $gb;
6394 } else if ($size >= 1048576) {
6395 $size = round($size / 1048576 * 10) / 10 . $mb;
6396 } else if ($size >= 1024) {
6397 $size = round($size / 1024 * 10) / 10 . $kb;
6398 } else {
6399 $size = intval($size) .' '. $b; // file sizes over 2GB can not work in 32bit PHP anyway
6401 return $size;
6405 * Cleans a given filename by removing suspicious or troublesome characters
6406 * @see clean_param()
6408 * @uses PARAM_FILE
6409 * @param string $string file name
6410 * @return string cleaned file name
6412 function clean_filename($string) {
6413 return clean_param($string, PARAM_FILE);
6417 /// STRING TRANSLATION ////////////////////////////////////////
6420 * Returns the code for the current language
6422 * @category string
6423 * @return string
6425 function current_language() {
6426 global $CFG, $USER, $SESSION, $COURSE;
6428 if (!empty($COURSE->id) and $COURSE->id != SITEID and !empty($COURSE->lang)) { // Course language can override all other settings for this page
6429 $return = $COURSE->lang;
6431 } else if (!empty($SESSION->lang)) { // Session language can override other settings
6432 $return = $SESSION->lang;
6434 } else if (!empty($USER->lang)) {
6435 $return = $USER->lang;
6437 } else if (isset($CFG->lang)) {
6438 $return = $CFG->lang;
6440 } else {
6441 $return = 'en';
6444 $return = str_replace('_utf8', '', $return); // Just in case this slipped in from somewhere by accident
6446 return $return;
6450 * Returns parent language of current active language if defined
6452 * @category string
6453 * @uses COURSE
6454 * @uses SESSION
6455 * @param string $lang null means current language
6456 * @return string
6458 function get_parent_language($lang=null) {
6459 global $COURSE, $SESSION;
6461 //let's hack around the current language
6462 if (!empty($lang)) {
6463 $old_course_lang = empty($COURSE->lang) ? '' : $COURSE->lang;
6464 $old_session_lang = empty($SESSION->lang) ? '' : $SESSION->lang;
6465 $COURSE->lang = '';
6466 $SESSION->lang = $lang;
6469 $parentlang = get_string('parentlanguage', 'langconfig');
6470 if ($parentlang === 'en') {
6471 $parentlang = '';
6474 //let's hack around the current language
6475 if (!empty($lang)) {
6476 $COURSE->lang = $old_course_lang;
6477 $SESSION->lang = $old_session_lang;
6480 return $parentlang;
6484 * Returns current string_manager instance.
6486 * The param $forcereload is needed for CLI installer only where the string_manager instance
6487 * must be replaced during the install.php script life time.
6489 * @category string
6490 * @param bool $forcereload shall the singleton be released and new instance created instead?
6491 * @return string_manager
6493 function get_string_manager($forcereload=false) {
6494 global $CFG;
6496 static $singleton = null;
6498 if ($forcereload) {
6499 $singleton = null;
6501 if ($singleton === null) {
6502 if (empty($CFG->early_install_lang)) {
6504 if (empty($CFG->langlist)) {
6505 $translist = array();
6506 } else {
6507 $translist = explode(',', $CFG->langlist);
6510 if (empty($CFG->langmenucachefile)) {
6511 $langmenucache = $CFG->cachedir . '/languages';
6512 } else {
6513 $langmenucache = $CFG->langmenucachefile;
6516 $singleton = new core_string_manager($CFG->langotherroot, $CFG->langlocalroot,
6517 !empty($CFG->langstringcache), $translist, $langmenucache);
6519 } else {
6520 $singleton = new install_string_manager();
6524 return $singleton;
6529 * Interface for string manager
6531 * Interface describing class which is responsible for getting
6532 * of localised strings from language packs.
6534 * @package core
6535 * @copyright 2010 Petr Skoda (http://skodak.org)
6536 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6538 interface string_manager {
6540 * Get String returns a requested string
6542 * @param string $identifier The identifier of the string to search for
6543 * @param string $component The module the string is associated with
6544 * @param string|object|array $a An object, string or number that can be used
6545 * within translation strings
6546 * @param string $lang moodle translation language, NULL means use current
6547 * @return string The String !
6549 public function get_string($identifier, $component = '', $a = NULL, $lang = NULL);
6552 * Does the string actually exist?
6554 * get_string() is throwing debug warnings, sometimes we do not want them
6555 * or we want to display better explanation of the problem.
6557 * Use with care!
6559 * @param string $identifier The identifier of the string to search for
6560 * @param string $component The module the string is associated with
6561 * @return boot true if exists
6563 public function string_exists($identifier, $component);
6566 * Returns a localised list of all country names, sorted by country keys.
6567 * @param bool $returnall return all or just enabled
6568 * @param string $lang moodle translation language, NULL means use current
6569 * @return array two-letter country code => translated name.
6571 public function get_list_of_countries($returnall = false, $lang = NULL);
6574 * Returns a localised list of languages, sorted by code keys.
6576 * @param string $lang moodle translation language, NULL means use current
6577 * @param string $standard language list standard
6578 * iso6392: three-letter language code (ISO 639-2/T) => translated name.
6579 * @return array language code => translated name
6581 public function get_list_of_languages($lang = NULL, $standard = 'iso6392');
6584 * Checks if the translation exists for the language
6586 * @param string $lang moodle translation language code
6587 * @param bool $includeall include also disabled translations
6588 * @return bool true if exists
6590 public function translation_exists($lang, $includeall = true);
6593 * Returns localised list of installed translations
6594 * @param bool $returnall return all or just enabled
6595 * @return array moodle translation code => localised translation name
6597 public function get_list_of_translations($returnall = false);
6600 * Returns localised list of currencies.
6602 * @param string $lang moodle translation language, NULL means use current
6603 * @return array currency code => localised currency name
6605 public function get_list_of_currencies($lang = NULL);
6608 * Load all strings for one component
6609 * @param string $component The module the string is associated with
6610 * @param string $lang
6611 * @param bool $disablecache Do not use caches, force fetching the strings from sources
6612 * @param bool $disablelocal Do not use customized strings in xx_local language packs
6613 * @return array of all string for given component and lang
6615 public function load_component_strings($component, $lang, $disablecache=false, $disablelocal=false);
6618 * Invalidates all caches, should the implementation use any
6619 * @param bool $phpunitreset true means called from our PHPUnit integration test reset
6621 public function reset_caches($phpunitreset = false);
6624 * Returns string revision counter, this is incremented after any
6625 * string cache reset.
6626 * @return int lang string revision counter, -1 if unknown
6628 public function get_revision();
6633 * Standard string_manager implementation
6635 * Implements string_manager with getting and printing localised strings
6637 * @package core
6638 * @category string
6639 * @copyright 2010 Petr Skoda (http://skodak.org)
6640 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6642 class core_string_manager implements string_manager {
6643 /** @var string location of all packs except 'en' */
6644 protected $otherroot;
6645 /** @var string location of all lang pack local modifications */
6646 protected $localroot;
6647 /** @var cache lang string cache - it will be optimised more later */
6648 protected $cache;
6649 /** @var int get_string() counter */
6650 protected $countgetstring = 0;
6651 /** @var bool use disk cache */
6652 protected $usecache;
6653 /** @var array limit list of translations */
6654 protected $translist;
6655 /** @var string location of a file that caches the list of available translations */
6656 protected $menucache;
6659 * Create new instance of string manager
6661 * @param string $otherroot location of downlaoded lang packs - usually $CFG->dataroot/lang
6662 * @param string $localroot usually the same as $otherroot
6663 * @param bool $usecache use disk cache
6664 * @param array $translist limit list of visible translations
6665 * @param string $menucache the location of a file that caches the list of available translations
6667 public function __construct($otherroot, $localroot, $usecache, $translist, $menucache) {
6668 $this->otherroot = $otherroot;
6669 $this->localroot = $localroot;
6670 $this->usecache = $usecache;
6671 $this->translist = $translist;
6672 $this->menucache = $menucache;
6674 if ($this->usecache) {
6675 // We can use a proper cache, establish the cache using the 'String cache' definition.
6676 $this->cache = cache::make('core', 'string');
6677 } else {
6678 // We only want a cache for the length of the request, create a static cache.
6679 $options = array(
6680 'simplekeys' => true,
6681 'simpledata' => true
6683 $this->cache = cache::make_from_params(cache_store::MODE_REQUEST, 'core', 'string', array(), $options);
6688 * Returns list of all explicit parent languages for the given language.
6690 * English (en) is considered as the top implicit parent of all language packs
6691 * and is not included in the returned list. The language itself is appended to the
6692 * end of the list. The method is aware of circular dependency risk.
6694 * @see self::populate_parent_languages()
6695 * @param string $lang the code of the language
6696 * @return array all explicit parent languages with the lang itself appended
6698 public function get_language_dependencies($lang) {
6699 return $this->populate_parent_languages($lang);
6703 * Load all strings for one component
6705 * @param string $component The module the string is associated with
6706 * @param string $lang
6707 * @param bool $disablecache Do not use caches, force fetching the strings from sources
6708 * @param bool $disablelocal Do not use customized strings in xx_local language packs
6709 * @return array of all string for given component and lang
6711 public function load_component_strings($component, $lang, $disablecache=false, $disablelocal=false) {
6712 global $CFG;
6714 list($plugintype, $pluginname) = normalize_component($component);
6715 if ($plugintype == 'core' and is_null($pluginname)) {
6716 $component = 'core';
6717 } else {
6718 $component = $plugintype . '_' . $pluginname;
6721 $cachekey = $lang.'_'.$component;
6723 if (!$disablecache and !$disablelocal) {
6724 $string = $this->cache->get($cachekey);
6725 if ($string) {
6726 return $string;
6730 // no cache found - let us merge all possible sources of the strings
6731 if ($plugintype === 'core') {
6732 $file = $pluginname;
6733 if ($file === null) {
6734 $file = 'moodle';
6736 $string = array();
6737 // first load english pack
6738 if (!file_exists("$CFG->dirroot/lang/en/$file.php")) {
6739 return array();
6741 include("$CFG->dirroot/lang/en/$file.php");
6742 $originalkeys = array_keys($string);
6743 $originalkeys = array_flip($originalkeys);
6745 // and then corresponding local if present and allowed
6746 if (!$disablelocal and file_exists("$this->localroot/en_local/$file.php")) {
6747 include("$this->localroot/en_local/$file.php");
6749 // now loop through all langs in correct order
6750 $deps = $this->get_language_dependencies($lang);
6751 foreach ($deps as $dep) {
6752 // the main lang string location
6753 if (file_exists("$this->otherroot/$dep/$file.php")) {
6754 include("$this->otherroot/$dep/$file.php");
6756 if (!$disablelocal and file_exists("$this->localroot/{$dep}_local/$file.php")) {
6757 include("$this->localroot/{$dep}_local/$file.php");
6761 } else {
6762 if (!$location = get_plugin_directory($plugintype, $pluginname) or !is_dir($location)) {
6763 return array();
6765 if ($plugintype === 'mod') {
6766 // bloody mod hack
6767 $file = $pluginname;
6768 } else {
6769 $file = $plugintype . '_' . $pluginname;
6771 $string = array();
6772 // first load English pack
6773 if (!file_exists("$location/lang/en/$file.php")) {
6774 //English pack does not exist, so do not try to load anything else
6775 return array();
6777 include("$location/lang/en/$file.php");
6778 $originalkeys = array_keys($string);
6779 $originalkeys = array_flip($originalkeys);
6780 // and then corresponding local english if present
6781 if (!$disablelocal and file_exists("$this->localroot/en_local/$file.php")) {
6782 include("$this->localroot/en_local/$file.php");
6785 // now loop through all langs in correct order
6786 $deps = $this->get_language_dependencies($lang);
6787 foreach ($deps as $dep) {
6788 // legacy location - used by contrib only
6789 if (file_exists("$location/lang/$dep/$file.php")) {
6790 include("$location/lang/$dep/$file.php");
6792 // the main lang string location
6793 if (file_exists("$this->otherroot/$dep/$file.php")) {
6794 include("$this->otherroot/$dep/$file.php");
6796 // local customisations
6797 if (!$disablelocal and file_exists("$this->localroot/{$dep}_local/$file.php")) {
6798 include("$this->localroot/{$dep}_local/$file.php");
6803 // we do not want any extra strings from other languages - everything must be in en lang pack
6804 $string = array_intersect_key($string, $originalkeys);
6806 if (!$disablelocal) {
6807 // now we have a list of strings from all possible sources. put it into both in-memory and on-disk
6808 // caches so we do not need to do all this merging and dependencies resolving again
6809 $this->cache->set($cachekey, $string);
6811 return $string;
6815 * Does the string actually exist?
6817 * get_string() is throwing debug warnings, sometimes we do not want them
6818 * or we want to display better explanation of the problem.
6819 * Note: Use with care!
6821 * @param string $identifier The identifier of the string to search for
6822 * @param string $component The module the string is associated with
6823 * @return boot true if exists
6825 public function string_exists($identifier, $component) {
6826 $identifier = clean_param($identifier, PARAM_STRINGID);
6827 if (empty($identifier)) {
6828 return false;
6830 $lang = current_language();
6831 $string = $this->load_component_strings($component, $lang);
6832 return isset($string[$identifier]);
6836 * Get String returns a requested string
6838 * @param string $identifier The identifier of the string to search for
6839 * @param string $component The module the string is associated with
6840 * @param string|object|array $a An object, string or number that can be used
6841 * within translation strings
6842 * @param string $lang moodle translation language, NULL means use current
6843 * @return string The String !
6845 public function get_string($identifier, $component = '', $a = NULL, $lang = NULL) {
6846 $this->countgetstring++;
6847 // there are very many uses of these time formating strings without the 'langconfig' component,
6848 // it would not be reasonable to expect that all of them would be converted during 2.0 migration
6849 static $langconfigstrs = array(
6850 'strftimedate' => 1,
6851 'strftimedatefullshort' => 1,
6852 'strftimedateshort' => 1,
6853 'strftimedatetime' => 1,
6854 'strftimedatetimeshort' => 1,
6855 'strftimedaydate' => 1,
6856 'strftimedaydatetime' => 1,
6857 'strftimedayshort' => 1,
6858 'strftimedaytime' => 1,
6859 'strftimemonthyear' => 1,
6860 'strftimerecent' => 1,
6861 'strftimerecentfull' => 1,
6862 'strftimetime' => 1);
6864 if (empty($component)) {
6865 if (isset($langconfigstrs[$identifier])) {
6866 $component = 'langconfig';
6867 } else {
6868 $component = 'moodle';
6872 if ($lang === NULL) {
6873 $lang = current_language();
6876 $string = $this->load_component_strings($component, $lang);
6878 if (!isset($string[$identifier])) {
6879 if ($component === 'pix' or $component === 'core_pix') {
6880 // this component contains only alt tags for emoticons,
6881 // not all of them are supposed to be defined
6882 return '';
6884 if ($identifier === 'parentlanguage' and ($component === 'langconfig' or $component === 'core_langconfig')) {
6885 // parentlanguage is a special string, undefined means use English if not defined
6886 return 'en';
6888 if ($this->usecache) {
6889 // maybe the on-disk cache is dirty - let the last attempt be to find the string in original sources,
6890 // do NOT write the results to disk cache because it may end up in race conditions see MDL-31904
6891 $this->usecache = false;
6892 $string = $this->load_component_strings($component, $lang, true);
6893 $this->usecache = true;
6895 if (!isset($string[$identifier])) {
6896 // the string is still missing - should be fixed by developer
6897 list($plugintype, $pluginname) = normalize_component($component);
6898 if ($plugintype == 'core') {
6899 $file = "lang/en/{$component}.php";
6900 } else if ($plugintype == 'mod') {
6901 $file = "mod/{$pluginname}/lang/en/{$pluginname}.php";
6902 } else {
6903 $path = get_plugin_directory($plugintype, $pluginname);
6904 $file = "{$path}/lang/en/{$plugintype}_{$pluginname}.php";
6906 debugging("Invalid get_string() identifier: '{$identifier}' or component '{$component}'. " .
6907 "Perhaps you are missing \$string['{$identifier}'] = ''; in {$file}?", DEBUG_DEVELOPER);
6908 return "[[$identifier]]";
6912 $string = $string[$identifier];
6914 if ($a !== NULL) {
6915 // Process array's and objects (except lang_strings)
6916 if (is_array($a) or (is_object($a) && !($a instanceof lang_string))) {
6917 $a = (array)$a;
6918 $search = array();
6919 $replace = array();
6920 foreach ($a as $key=>$value) {
6921 if (is_int($key)) {
6922 // we do not support numeric keys - sorry!
6923 continue;
6925 if (is_array($value) or (is_object($value) && !($value instanceof lang_string))) {
6926 // we support just string or lang_string as value
6927 continue;
6929 $search[] = '{$a->'.$key.'}';
6930 $replace[] = (string)$value;
6932 if ($search) {
6933 $string = str_replace($search, $replace, $string);
6935 } else {
6936 $string = str_replace('{$a}', (string)$a, $string);
6940 return $string;
6944 * Returns information about the string_manager performance
6946 * @return array
6948 public function get_performance_summary() {
6949 return array(array(
6950 'langcountgetstring' => $this->countgetstring,
6951 ), array(
6952 'langcountgetstring' => 'get_string calls',
6957 * Returns a localised list of all country names, sorted by localised name.
6959 * @param bool $returnall return all or just enabled
6960 * @param string $lang moodle translation language, NULL means use current
6961 * @return array two-letter country code => translated name.
6963 public function get_list_of_countries($returnall = false, $lang = NULL) {
6964 global $CFG;
6966 if ($lang === NULL) {
6967 $lang = current_language();
6970 $countries = $this->load_component_strings('core_countries', $lang);
6971 collatorlib::asort($countries);
6972 if (!$returnall and !empty($CFG->allcountrycodes)) {
6973 $enabled = explode(',', $CFG->allcountrycodes);
6974 $return = array();
6975 foreach ($enabled as $c) {
6976 if (isset($countries[$c])) {
6977 $return[$c] = $countries[$c];
6980 return $return;
6983 return $countries;
6987 * Returns a localised list of languages, sorted by code keys.
6989 * @param string $lang moodle translation language, NULL means use current
6990 * @param string $standard language list standard
6991 * - iso6392: three-letter language code (ISO 639-2/T) => translated name
6992 * - iso6391: two-letter langauge code (ISO 639-1) => translated name
6993 * @return array language code => translated name
6995 public function get_list_of_languages($lang = NULL, $standard = 'iso6391') {
6996 if ($lang === NULL) {
6997 $lang = current_language();
7000 if ($standard === 'iso6392') {
7001 $langs = $this->load_component_strings('core_iso6392', $lang);
7002 ksort($langs);
7003 return $langs;
7005 } else if ($standard === 'iso6391') {
7006 $langs2 = $this->load_component_strings('core_iso6392', $lang);
7007 static $mapping = array('aar' => 'aa', 'abk' => 'ab', 'afr' => 'af', 'aka' => 'ak', 'sqi' => 'sq', 'amh' => 'am', 'ara' => 'ar', 'arg' => 'an', 'hye' => 'hy',
7008 'asm' => 'as', 'ava' => 'av', 'ave' => 'ae', 'aym' => 'ay', 'aze' => 'az', 'bak' => 'ba', 'bam' => 'bm', 'eus' => 'eu', 'bel' => 'be', 'ben' => 'bn', 'bih' => 'bh',
7009 'bis' => 'bi', 'bos' => 'bs', 'bre' => 'br', 'bul' => 'bg', 'mya' => 'my', 'cat' => 'ca', 'cha' => 'ch', 'che' => 'ce', 'zho' => 'zh', 'chu' => 'cu', 'chv' => 'cv',
7010 'cor' => 'kw', 'cos' => 'co', 'cre' => 'cr', 'ces' => 'cs', 'dan' => 'da', 'div' => 'dv', 'nld' => 'nl', 'dzo' => 'dz', 'eng' => 'en', 'epo' => 'eo', 'est' => 'et',
7011 'ewe' => 'ee', 'fao' => 'fo', 'fij' => 'fj', 'fin' => 'fi', 'fra' => 'fr', 'fry' => 'fy', 'ful' => 'ff', 'kat' => 'ka', 'deu' => 'de', 'gla' => 'gd', 'gle' => 'ga',
7012 'glg' => 'gl', 'glv' => 'gv', 'ell' => 'el', 'grn' => 'gn', 'guj' => 'gu', 'hat' => 'ht', 'hau' => 'ha', 'heb' => 'he', 'her' => 'hz', 'hin' => 'hi', 'hmo' => 'ho',
7013 'hrv' => 'hr', 'hun' => 'hu', 'ibo' => 'ig', 'isl' => 'is', 'ido' => 'io', 'iii' => 'ii', 'iku' => 'iu', 'ile' => 'ie', 'ina' => 'ia', 'ind' => 'id', 'ipk' => 'ik',
7014 'ita' => 'it', 'jav' => 'jv', 'jpn' => 'ja', 'kal' => 'kl', 'kan' => 'kn', 'kas' => 'ks', 'kau' => 'kr', 'kaz' => 'kk', 'khm' => 'km', 'kik' => 'ki', 'kin' => 'rw',
7015 'kir' => 'ky', 'kom' => 'kv', 'kon' => 'kg', 'kor' => 'ko', 'kua' => 'kj', 'kur' => 'ku', 'lao' => 'lo', 'lat' => 'la', 'lav' => 'lv', 'lim' => 'li', 'lin' => 'ln',
7016 'lit' => 'lt', 'ltz' => 'lb', 'lub' => 'lu', 'lug' => 'lg', 'mkd' => 'mk', 'mah' => 'mh', 'mal' => 'ml', 'mri' => 'mi', 'mar' => 'mr', 'msa' => 'ms', 'mlg' => 'mg',
7017 'mlt' => 'mt', 'mon' => 'mn', 'nau' => 'na', 'nav' => 'nv', 'nbl' => 'nr', 'nde' => 'nd', 'ndo' => 'ng', 'nep' => 'ne', 'nno' => 'nn', 'nob' => 'nb', 'nor' => 'no',
7018 'nya' => 'ny', 'oci' => 'oc', 'oji' => 'oj', 'ori' => 'or', 'orm' => 'om', 'oss' => 'os', 'pan' => 'pa', 'fas' => 'fa', 'pli' => 'pi', 'pol' => 'pl', 'por' => 'pt',
7019 'pus' => 'ps', 'que' => 'qu', 'roh' => 'rm', 'ron' => 'ro', 'run' => 'rn', 'rus' => 'ru', 'sag' => 'sg', 'san' => 'sa', 'sin' => 'si', 'slk' => 'sk', 'slv' => 'sl',
7020 'sme' => 'se', 'smo' => 'sm', 'sna' => 'sn', 'snd' => 'sd', 'som' => 'so', 'sot' => 'st', 'spa' => 'es', 'srd' => 'sc', 'srp' => 'sr', 'ssw' => 'ss', 'sun' => 'su',
7021 'swa' => 'sw', 'swe' => 'sv', 'tah' => 'ty', 'tam' => 'ta', 'tat' => 'tt', 'tel' => 'te', 'tgk' => 'tg', 'tgl' => 'tl', 'tha' => 'th', 'bod' => 'bo', 'tir' => 'ti',
7022 'ton' => 'to', 'tsn' => 'tn', 'tso' => 'ts', 'tuk' => 'tk', 'tur' => 'tr', 'twi' => 'tw', 'uig' => 'ug', 'ukr' => 'uk', 'urd' => 'ur', 'uzb' => 'uz', 'ven' => 've',
7023 'vie' => 'vi', 'vol' => 'vo', 'cym' => 'cy', 'wln' => 'wa', 'wol' => 'wo', 'xho' => 'xh', 'yid' => 'yi', 'yor' => 'yo', 'zha' => 'za', 'zul' => 'zu');
7024 $langs1 = array();
7025 foreach ($mapping as $c2=>$c1) {
7026 $langs1[$c1] = $langs2[$c2];
7028 ksort($langs1);
7029 return $langs1;
7031 } else {
7032 debugging('Unsupported $standard parameter in get_list_of_languages() method: '.$standard);
7035 return array();
7039 * Checks if the translation exists for the language
7041 * @param string $lang moodle translation language code
7042 * @param bool $includeall include also disabled translations
7043 * @return bool true if exists
7045 public function translation_exists($lang, $includeall = true) {
7047 if (strpos($lang, '_local') !== false) {
7048 // _local packs are not real translations
7049 return false;
7051 if (!$includeall and !empty($this->translist)) {
7052 if (!in_array($lang, $this->translist)) {
7053 return false;
7056 if ($lang === 'en') {
7057 // part of distribution
7058 return true;
7060 return file_exists("$this->otherroot/$lang/langconfig.php");
7064 * Returns localised list of installed translations
7066 * @param bool $returnall return all or just enabled
7067 * @return array moodle translation code => localised translation name
7069 public function get_list_of_translations($returnall = false) {
7070 global $CFG;
7072 $languages = array();
7074 if (!empty($CFG->langcache) and is_readable($this->menucache)) {
7075 // try to re-use the cached list of all available languages
7076 $cachedlist = json_decode(file_get_contents($this->menucache), true);
7078 if (is_array($cachedlist) and !empty($cachedlist)) {
7079 // the cache file is restored correctly
7081 if (!$returnall and !empty($this->translist)) {
7082 // return just enabled translations
7083 foreach ($cachedlist as $langcode => $langname) {
7084 if (in_array($langcode, $this->translist)) {
7085 $languages[$langcode] = $langname;
7088 return $languages;
7090 } else {
7091 // return all translations
7092 return $cachedlist;
7097 // the cached list of languages is not available, let us populate the list
7099 if (!$returnall and !empty($this->translist)) {
7100 // return only some translations
7101 foreach ($this->translist as $lang) {
7102 $lang = trim($lang); //Just trim spaces to be a bit more permissive
7103 if (strstr($lang, '_local') !== false) {
7104 continue;
7106 if (strstr($lang, '_utf8') !== false) {
7107 continue;
7109 if ($lang !== 'en' and !file_exists("$this->otherroot/$lang/langconfig.php")) {
7110 // some broken or missing lang - can not switch to it anyway
7111 continue;
7113 $string = $this->load_component_strings('langconfig', $lang);
7114 if (!empty($string['thislanguage'])) {
7115 $languages[$lang] = $string['thislanguage'].' ('. $lang .')';
7117 unset($string);
7120 } else {
7121 // return all languages available in system
7122 $langdirs = get_list_of_plugins('', '', $this->otherroot);
7124 $langdirs = array_merge($langdirs, array("$CFG->dirroot/lang/en"=>'en'));
7125 // Sort all
7127 // Loop through all langs and get info
7128 foreach ($langdirs as $lang) {
7129 if (strstr($lang, '_local') !== false) {
7130 continue;
7132 if (strstr($lang, '_utf8') !== false) {
7133 continue;
7135 $string = $this->load_component_strings('langconfig', $lang);
7136 if (!empty($string['thislanguage'])) {
7137 $languages[$lang] = $string['thislanguage'].' ('. $lang .')';
7139 unset($string);
7142 if (!empty($CFG->langcache) and !empty($this->menucache)) {
7143 // cache the list so that it can be used next time
7144 collatorlib::asort($languages);
7145 check_dir_exists(dirname($this->menucache), true, true);
7146 file_put_contents($this->menucache, json_encode($languages));
7150 collatorlib::asort($languages);
7152 return $languages;
7156 * Returns localised list of currencies.
7158 * @param string $lang moodle translation language, NULL means use current
7159 * @return array currency code => localised currency name
7161 public function get_list_of_currencies($lang = NULL) {
7162 if ($lang === NULL) {
7163 $lang = current_language();
7166 $currencies = $this->load_component_strings('core_currencies', $lang);
7167 asort($currencies);
7169 return $currencies;
7173 * Clears both in-memory and on-disk caches
7174 * @param bool $phpunitreset true means called from our PHPUnit integration test reset
7176 public function reset_caches($phpunitreset = false) {
7177 global $CFG;
7178 require_once("$CFG->libdir/filelib.php");
7180 // clear the on-disk disk with aggregated string files
7181 $this->cache->purge();
7183 if (!$phpunitreset) {
7184 // Increment the revision counter.
7185 $langrev = get_config('core', 'langrev');
7186 $next = time();
7187 if ($langrev !== false and $next <= $langrev and $langrev - $next < 60*60) {
7188 // This resolves problems when reset is requested repeatedly within 1s,
7189 // the < 1h condition prevents accidental switching to future dates
7190 // because we might not recover from it.
7191 $next = $langrev+1;
7193 set_config('langrev', $next);
7196 // clear the cache containing the list of available translations
7197 // and re-populate it again
7198 fulldelete($this->menucache);
7199 $this->get_list_of_translations(true);
7203 * Returns string revision counter, this is incremented after any
7204 * string cache reset.
7205 * @return int lang string revision counter, -1 if unknown
7207 public function get_revision() {
7208 global $CFG;
7209 if (isset($CFG->langrev)) {
7210 return (int)$CFG->langrev;
7211 } else {
7212 return -1;
7216 /// End of external API ////////////////////////////////////////////////////
7219 * Helper method that recursively loads all parents of the given language.
7221 * @see self::get_language_dependencies()
7222 * @param string $lang language code
7223 * @param array $stack list of parent languages already populated in previous recursive calls
7224 * @return array list of all parents of the given language with the $lang itself added as the last element
7226 protected function populate_parent_languages($lang, array $stack = array()) {
7228 // English does not have a parent language.
7229 if ($lang === 'en') {
7230 return $stack;
7233 // Prevent circular dependency (and thence the infinitive recursion loop).
7234 if (in_array($lang, $stack)) {
7235 return $stack;
7238 // Load language configuration and look for the explicit parent language.
7239 if (!file_exists("$this->otherroot/$lang/langconfig.php")) {
7240 return $stack;
7242 $string = array();
7243 include("$this->otherroot/$lang/langconfig.php");
7245 if (empty($string['parentlanguage']) or $string['parentlanguage'] === 'en') {
7246 unset($string);
7247 return array_merge(array($lang), $stack);
7249 } else {
7250 $parentlang = $string['parentlanguage'];
7251 unset($string);
7252 return $this->populate_parent_languages($parentlang, array_merge(array($lang), $stack));
7259 * Fetches minimum strings for installation
7261 * Minimalistic string fetching implementation
7262 * that is used in installer before we fetch the wanted
7263 * language pack from moodle.org lang download site.
7265 * @package core
7266 * @copyright 2010 Petr Skoda (http://skodak.org)
7267 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
7269 class install_string_manager implements string_manager {
7270 /** @var string location of pre-install packs for all langs */
7271 protected $installroot;
7274 * Crate new instance of install string manager
7276 public function __construct() {
7277 global $CFG;
7278 $this->installroot = "$CFG->dirroot/install/lang";
7282 * Load all strings for one component
7283 * @param string $component The module the string is associated with
7284 * @param string $lang
7285 * @param bool $disablecache Do not use caches, force fetching the strings from sources
7286 * @param bool $disablelocal Do not use customized strings in xx_local language packs
7287 * @return array of all string for given component and lang
7289 public function load_component_strings($component, $lang, $disablecache=false, $disablelocal=false) {
7290 // not needed in installer
7291 return array();
7295 * Does the string actually exist?
7297 * get_string() is throwing debug warnings, sometimes we do not want them
7298 * or we want to display better explanation of the problem.
7300 * Use with care!
7302 * @param string $identifier The identifier of the string to search for
7303 * @param string $component The module the string is associated with
7304 * @return boot true if exists
7306 public function string_exists($identifier, $component) {
7307 $identifier = clean_param($identifier, PARAM_STRINGID);
7308 if (empty($identifier)) {
7309 return false;
7311 // simple old style hack ;)
7312 $str = get_string($identifier, $component);
7313 return (strpos($str, '[[') === false);
7317 * Get String returns a requested string
7319 * @param string $identifier The identifier of the string to search for
7320 * @param string $component The module the string is associated with
7321 * @param string|object|array $a An object, string or number that can be used
7322 * within translation strings
7323 * @param string $lang moodle translation language, NULL means use current
7324 * @return string The String !
7326 public function get_string($identifier, $component = '', $a = NULL, $lang = NULL) {
7327 if (!$component) {
7328 $component = 'moodle';
7331 if ($lang === NULL) {
7332 $lang = current_language();
7335 //get parent lang
7336 $parent = '';
7337 if ($lang !== 'en' and $identifier !== 'parentlanguage' and $component !== 'langconfig') {
7338 if (file_exists("$this->installroot/$lang/langconfig.php")) {
7339 $string = array();
7340 include("$this->installroot/$lang/langconfig.php");
7341 if (isset($string['parentlanguage'])) {
7342 $parent = $string['parentlanguage'];
7344 unset($string);
7348 // include en string first
7349 if (!file_exists("$this->installroot/en/$component.php")) {
7350 return "[[$identifier]]";
7352 $string = array();
7353 include("$this->installroot/en/$component.php");
7355 // now override en with parent if defined
7356 if ($parent and $parent !== 'en' and file_exists("$this->installroot/$parent/$component.php")) {
7357 include("$this->installroot/$parent/$component.php");
7360 // finally override with requested language
7361 if ($lang !== 'en' and file_exists("$this->installroot/$lang/$component.php")) {
7362 include("$this->installroot/$lang/$component.php");
7365 if (!isset($string[$identifier])) {
7366 return "[[$identifier]]";
7369 $string = $string[$identifier];
7371 if ($a !== NULL) {
7372 if (is_object($a) or is_array($a)) {
7373 $a = (array)$a;
7374 $search = array();
7375 $replace = array();
7376 foreach ($a as $key=>$value) {
7377 if (is_int($key)) {
7378 // we do not support numeric keys - sorry!
7379 continue;
7381 $search[] = '{$a->'.$key.'}';
7382 $replace[] = (string)$value;
7384 if ($search) {
7385 $string = str_replace($search, $replace, $string);
7387 } else {
7388 $string = str_replace('{$a}', (string)$a, $string);
7392 return $string;
7396 * Returns a localised list of all country names, sorted by country keys.
7398 * @param bool $returnall return all or just enabled
7399 * @param string $lang moodle translation language, NULL means use current
7400 * @return array two-letter country code => translated name.
7402 public function get_list_of_countries($returnall = false, $lang = NULL) {
7403 //not used in installer
7404 return array();
7408 * Returns a localised list of languages, sorted by code keys.
7410 * @param string $lang moodle translation language, NULL means use current
7411 * @param string $standard language list standard
7412 * iso6392: three-letter language code (ISO 639-2/T) => translated name.
7413 * @return array language code => translated name
7415 public function get_list_of_languages($lang = NULL, $standard = 'iso6392') {
7416 //not used in installer
7417 return array();
7421 * Checks if the translation exists for the language
7423 * @param string $lang moodle translation language code
7424 * @param bool $includeall include also disabled translations
7425 * @return bool true if exists
7427 public function translation_exists($lang, $includeall = true) {
7428 return file_exists($this->installroot.'/'.$lang.'/langconfig.php');
7432 * Returns localised list of installed translations
7433 * @param bool $returnall return all or just enabled
7434 * @return array moodle translation code => localised translation name
7436 public function get_list_of_translations($returnall = false) {
7437 // return all is ignored here - we need to know all langs in installer
7438 $languages = array();
7439 // Get raw list of lang directories
7440 $langdirs = get_list_of_plugins('install/lang');
7441 asort($langdirs);
7442 // Get some info from each lang
7443 foreach ($langdirs as $lang) {
7444 if (file_exists($this->installroot.'/'.$lang.'/langconfig.php')) {
7445 $string = array();
7446 include($this->installroot.'/'.$lang.'/langconfig.php');
7447 if (!empty($string['thislanguage'])) {
7448 $languages[$lang] = $string['thislanguage'].' ('.$lang.')';
7452 // Return array
7453 return $languages;
7457 * Returns localised list of currencies.
7459 * @param string $lang moodle translation language, NULL means use current
7460 * @return array currency code => localised currency name
7462 public function get_list_of_currencies($lang = NULL) {
7463 // not used in installer
7464 return array();
7468 * This implementation does not use any caches
7469 * @param bool $phpunitreset true means called from our PHPUnit integration test reset
7471 public function reset_caches($phpunitreset = false) {
7472 // Nothing to do.
7476 * Returns string revision counter, this is incremented after any
7477 * string cache reset.
7478 * @return int lang string revision counter, -1 if unknown
7480 public function get_revision() {
7481 return -1;
7487 * Returns a localized string.
7489 * Returns the translated string specified by $identifier as
7490 * for $module. Uses the same format files as STphp.
7491 * $a is an object, string or number that can be used
7492 * within translation strings
7494 * eg 'hello {$a->firstname} {$a->lastname}'
7495 * or 'hello {$a}'
7497 * If you would like to directly echo the localized string use
7498 * the function {@link print_string()}
7500 * Example usage of this function involves finding the string you would
7501 * like a local equivalent of and using its identifier and module information
7502 * to retrieve it.<br/>
7503 * If you open moodle/lang/en/moodle.php and look near line 278
7504 * you will find a string to prompt a user for their word for 'course'
7505 * <code>
7506 * $string['course'] = 'Course';
7507 * </code>
7508 * So if you want to display the string 'Course'
7509 * in any language that supports it on your site
7510 * you just need to use the identifier 'course'
7511 * <code>
7512 * $mystring = '<strong>'. get_string('course') .'</strong>';
7513 * or
7514 * </code>
7515 * If the string you want is in another file you'd take a slightly
7516 * different approach. Looking in moodle/lang/en/calendar.php you find
7517 * around line 75:
7518 * <code>
7519 * $string['typecourse'] = 'Course event';
7520 * </code>
7521 * If you want to display the string "Course event" in any language
7522 * supported you would use the identifier 'typecourse' and the module 'calendar'
7523 * (because it is in the file calendar.php):
7524 * <code>
7525 * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>';
7526 * </code>
7528 * As a last resort, should the identifier fail to map to a string
7529 * the returned string will be [[ $identifier ]]
7531 * In Moodle 2.3 there is a new argument to this function $lazyload.
7532 * Setting $lazyload to true causes get_string to return a lang_string object
7533 * rather than the string itself. The fetching of the string is then put off until
7534 * the string object is first used. The object can be used by calling it's out
7535 * method or by casting the object to a string, either directly e.g.
7536 * (string)$stringobject
7537 * or indirectly by using the string within another string or echoing it out e.g.
7538 * echo $stringobject
7539 * return "<p>{$stringobject}</p>";
7540 * It is worth noting that using $lazyload and attempting to use the string as an
7541 * array key will cause a fatal error as objects cannot be used as array keys.
7542 * But you should never do that anyway!
7543 * For more information {@see lang_string}
7545 * @category string
7546 * @param string $identifier The key identifier for the localized string
7547 * @param string $component The module where the key identifier is stored,
7548 * usually expressed as the filename in the language pack without the
7549 * .php on the end but can also be written as mod/forum or grade/export/xls.
7550 * If none is specified then moodle.php is used.
7551 * @param string|object|array $a An object, string or number that can be used
7552 * within translation strings
7553 * @param bool $lazyload If set to true a string object is returned instead of
7554 * the string itself. The string then isn't calculated until it is first used.
7555 * @return string The localized string.
7557 function get_string($identifier, $component = '', $a = NULL, $lazyload = false) {
7558 global $CFG;
7560 // If the lazy load argument has been supplied return a lang_string object
7561 // instead.
7562 // We need to make sure it is true (and a bool) as you will see below there
7563 // used to be a forth argument at one point.
7564 if ($lazyload === true) {
7565 return new lang_string($identifier, $component, $a);
7568 $identifier = clean_param($identifier, PARAM_STRINGID);
7569 if (empty($identifier)) {
7570 throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.');
7573 // There is now a forth argument again, this time it is a boolean however so
7574 // we can still check for the old extralocations parameter.
7575 if (!is_bool($lazyload) && !empty($lazyload)) {
7576 debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.');
7579 if (strpos($component, '/') !== false) {
7580 debugging('The module name you passed to get_string is the deprecated format ' .
7581 'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER);
7582 $componentpath = explode('/', $component);
7584 switch ($componentpath[0]) {
7585 case 'mod':
7586 $component = $componentpath[1];
7587 break;
7588 case 'blocks':
7589 case 'block':
7590 $component = 'block_'.$componentpath[1];
7591 break;
7592 case 'enrol':
7593 $component = 'enrol_'.$componentpath[1];
7594 break;
7595 case 'format':
7596 $component = 'format_'.$componentpath[1];
7597 break;
7598 case 'grade':
7599 $component = 'grade'.$componentpath[1].'_'.$componentpath[2];
7600 break;
7604 $result = get_string_manager()->get_string($identifier, $component, $a);
7606 // Debugging feature lets you display string identifier and component
7607 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
7608 $result .= ' {' . $identifier . '/' . $component . '}';
7610 return $result;
7614 * Converts an array of strings to their localized value.
7616 * @param array $array An array of strings
7617 * @param string $component The language module that these strings can be found in.
7618 * @return stdClass translated strings.
7620 function get_strings($array, $component = '') {
7621 $string = new stdClass;
7622 foreach ($array as $item) {
7623 $string->$item = get_string($item, $component);
7625 return $string;
7629 * Prints out a translated string.
7631 * Prints out a translated string using the return value from the {@link get_string()} function.
7633 * Example usage of this function when the string is in the moodle.php file:<br/>
7634 * <code>
7635 * echo '<strong>';
7636 * print_string('course');
7637 * echo '</strong>';
7638 * </code>
7640 * Example usage of this function when the string is not in the moodle.php file:<br/>
7641 * <code>
7642 * echo '<h1>';
7643 * print_string('typecourse', 'calendar');
7644 * echo '</h1>';
7645 * </code>
7647 * @category string
7648 * @param string $identifier The key identifier for the localized string
7649 * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used.
7650 * @param string|object|array $a An object, string or number that can be used within translation strings
7652 function print_string($identifier, $component = '', $a = NULL) {
7653 echo get_string($identifier, $component, $a);
7657 * Returns a list of charset codes
7659 * Returns a list of charset codes. It's hardcoded, so they should be added manually
7660 * (checking that such charset is supported by the texlib library!)
7662 * @return array And associative array with contents in the form of charset => charset
7664 function get_list_of_charsets() {
7666 $charsets = array(
7667 'EUC-JP' => 'EUC-JP',
7668 'ISO-2022-JP'=> 'ISO-2022-JP',
7669 'ISO-8859-1' => 'ISO-8859-1',
7670 'SHIFT-JIS' => 'SHIFT-JIS',
7671 'GB2312' => 'GB2312',
7672 'GB18030' => 'GB18030', // gb18030 not supported by typo and mbstring
7673 'UTF-8' => 'UTF-8');
7675 asort($charsets);
7677 return $charsets;
7681 * Returns a list of valid and compatible themes
7683 * @return array
7685 function get_list_of_themes() {
7686 global $CFG;
7688 $themes = array();
7690 if (!empty($CFG->themelist)) { // use admin's list of themes
7691 $themelist = explode(',', $CFG->themelist);
7692 } else {
7693 $themelist = array_keys(get_plugin_list("theme"));
7696 foreach ($themelist as $key => $themename) {
7697 $theme = theme_config::load($themename);
7698 $themes[$themename] = $theme;
7701 collatorlib::asort_objects_by_method($themes, 'get_theme_name');
7703 return $themes;
7707 * Returns a list of timezones in the current language
7709 * @global object
7710 * @global object
7711 * @return array
7713 function get_list_of_timezones() {
7714 global $CFG, $DB;
7716 static $timezones;
7718 if (!empty($timezones)) { // This function has been called recently
7719 return $timezones;
7722 $timezones = array();
7724 if ($rawtimezones = $DB->get_records_sql("SELECT MAX(id), name FROM {timezone} GROUP BY name")) {
7725 foreach($rawtimezones as $timezone) {
7726 if (!empty($timezone->name)) {
7727 if (get_string_manager()->string_exists(strtolower($timezone->name), 'timezones')) {
7728 $timezones[$timezone->name] = get_string(strtolower($timezone->name), 'timezones');
7729 } else {
7730 $timezones[$timezone->name] = $timezone->name;
7732 if (substr($timezones[$timezone->name], 0, 1) == '[') { // No translation found
7733 $timezones[$timezone->name] = $timezone->name;
7739 asort($timezones);
7741 for ($i = -13; $i <= 13; $i += .5) {
7742 $tzstring = 'UTC';
7743 if ($i < 0) {
7744 $timezones[sprintf("%.1f", $i)] = $tzstring . $i;
7745 } else if ($i > 0) {
7746 $timezones[sprintf("%.1f", $i)] = $tzstring . '+' . $i;
7747 } else {
7748 $timezones[sprintf("%.1f", $i)] = $tzstring;
7752 return $timezones;
7756 * Factory function for emoticon_manager
7758 * @return emoticon_manager singleton
7760 function get_emoticon_manager() {
7761 static $singleton = null;
7763 if (is_null($singleton)) {
7764 $singleton = new emoticon_manager();
7767 return $singleton;
7771 * Provides core support for plugins that have to deal with
7772 * emoticons (like HTML editor or emoticon filter).
7774 * Whenever this manager mentiones 'emoticon object', the following data
7775 * structure is expected: stdClass with properties text, imagename, imagecomponent,
7776 * altidentifier and altcomponent
7778 * @see admin_setting_emoticons
7780 class emoticon_manager {
7783 * Returns the currently enabled emoticons
7785 * @return array of emoticon objects
7787 public function get_emoticons() {
7788 global $CFG;
7790 if (empty($CFG->emoticons)) {
7791 return array();
7794 $emoticons = $this->decode_stored_config($CFG->emoticons);
7796 if (!is_array($emoticons)) {
7797 // something is wrong with the format of stored setting
7798 debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL);
7799 return array();
7802 return $emoticons;
7806 * Converts emoticon object into renderable pix_emoticon object
7808 * @param stdClass $emoticon emoticon object
7809 * @param array $attributes explicit HTML attributes to set
7810 * @return pix_emoticon
7812 public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array()) {
7813 $stringmanager = get_string_manager();
7814 if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) {
7815 $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent);
7816 } else {
7817 $alt = s($emoticon->text);
7819 return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes);
7823 * Encodes the array of emoticon objects into a string storable in config table
7825 * @see self::decode_stored_config()
7826 * @param array $emoticons array of emtocion objects
7827 * @return string
7829 public function encode_stored_config(array $emoticons) {
7830 return json_encode($emoticons);
7834 * Decodes the string into an array of emoticon objects
7836 * @see self::encode_stored_config()
7837 * @param string $encoded
7838 * @return string|null
7840 public function decode_stored_config($encoded) {
7841 $decoded = json_decode($encoded);
7842 if (!is_array($decoded)) {
7843 return null;
7845 return $decoded;
7849 * Returns default set of emoticons supported by Moodle
7851 * @return array of sdtClasses
7853 public function default_emoticons() {
7854 return array(
7855 $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'),
7856 $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'),
7857 $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'),
7858 $this->prepare_emoticon_object(";-)", 's/wink', 'wink'),
7859 $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'),
7860 $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'),
7861 $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'),
7862 $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'),
7863 $this->prepare_emoticon_object("B-)", 's/cool', 'cool'),
7864 $this->prepare_emoticon_object("^-)", 's/approve', 'approve'),
7865 $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'),
7866 $this->prepare_emoticon_object(":o)", 's/clown', 'clown'),
7867 $this->prepare_emoticon_object(":-(", 's/sad', 'sad'),
7868 $this->prepare_emoticon_object(":(", 's/sad', 'sad'),
7869 $this->prepare_emoticon_object("8-.", 's/shy', 'shy'),
7870 $this->prepare_emoticon_object(":-I", 's/blush', 'blush'),
7871 $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'),
7872 $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'),
7873 $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'),
7874 $this->prepare_emoticon_object("8-[", 's/angry', 'angry'),
7875 $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'),
7876 $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'),
7877 $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'),
7878 $this->prepare_emoticon_object("}-]", 's/evil', 'evil'),
7879 $this->prepare_emoticon_object("(h)", 's/heart', 'heart'),
7880 $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'),
7881 $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'),
7882 $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'),
7883 $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'),
7884 $this->prepare_emoticon_object("( )", 's/egg', 'egg'),
7889 * Helper method preparing the stdClass with the emoticon properties
7891 * @param string|array $text or array of strings
7892 * @param string $imagename to be used by {@see pix_emoticon}
7893 * @param string $altidentifier alternative string identifier, null for no alt
7894 * @param array $altcomponent where the alternative string is defined
7895 * @param string $imagecomponent to be used by {@see pix_emoticon}
7896 * @return stdClass
7898 protected function prepare_emoticon_object($text, $imagename, $altidentifier = null, $altcomponent = 'core_pix', $imagecomponent = 'core') {
7899 return (object)array(
7900 'text' => $text,
7901 'imagename' => $imagename,
7902 'imagecomponent' => $imagecomponent,
7903 'altidentifier' => $altidentifier,
7904 'altcomponent' => $altcomponent,
7909 /// ENCRYPTION ////////////////////////////////////////////////
7912 * rc4encrypt
7914 * Please note that in this version of moodle that the default for rc4encryption is
7915 * using the slightly more secure password key. There may be an issue when upgrading
7916 * from an older version of moodle.
7918 * @todo MDL-31836 Remove the old password key in version 2.4
7919 * Code also needs to be changed in sessionlib.php
7920 * @see get_moodle_cookie()
7921 * @see set_moodle_cookie()
7923 * @param string $data Data to encrypt.
7924 * @param bool $usesecurekey Lets us know if we are using the old or new secure password key.
7925 * @return string The now encrypted data.
7927 function rc4encrypt($data, $usesecurekey = true) {
7928 if (!$usesecurekey) {
7929 $passwordkey = 'nfgjeingjk';
7930 } else {
7931 $passwordkey = get_site_identifier();
7933 return endecrypt($passwordkey, $data, '');
7937 * rc4decrypt
7939 * Please note that in this version of moodle that the default for rc4encryption is
7940 * using the slightly more secure password key. There may be an issue when upgrading
7941 * from an older version of moodle.
7943 * @todo MDL-31836 Remove the old password key in version 2.4
7944 * Code also needs to be changed in sessionlib.php
7945 * @see get_moodle_cookie()
7946 * @see set_moodle_cookie()
7948 * @param string $data Data to decrypt.
7949 * @param bool $usesecurekey Lets us know if we are using the old or new secure password key.
7950 * @return string The now decrypted data.
7952 function rc4decrypt($data, $usesecurekey = true) {
7953 if (!$usesecurekey) {
7954 $passwordkey = 'nfgjeingjk';
7955 } else {
7956 $passwordkey = get_site_identifier();
7958 return endecrypt($passwordkey, $data, 'de');
7962 * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com]
7964 * @todo Finish documenting this function
7966 * @param string $pwd The password to use when encrypting or decrypting
7967 * @param string $data The data to be decrypted/encrypted
7968 * @param string $case Either 'de' for decrypt or '' for encrypt
7969 * @return string
7971 function endecrypt ($pwd, $data, $case) {
7973 if ($case == 'de') {
7974 $data = urldecode($data);
7977 $key[] = '';
7978 $box[] = '';
7979 $temp_swap = '';
7980 $pwd_length = 0;
7982 $pwd_length = strlen($pwd);
7984 for ($i = 0; $i <= 255; $i++) {
7985 $key[$i] = ord(substr($pwd, ($i % $pwd_length), 1));
7986 $box[$i] = $i;
7989 $x = 0;
7991 for ($i = 0; $i <= 255; $i++) {
7992 $x = ($x + $box[$i] + $key[$i]) % 256;
7993 $temp_swap = $box[$i];
7994 $box[$i] = $box[$x];
7995 $box[$x] = $temp_swap;
7998 $temp = '';
7999 $k = '';
8001 $cipherby = '';
8002 $cipher = '';
8004 $a = 0;
8005 $j = 0;
8007 for ($i = 0; $i < strlen($data); $i++) {
8008 $a = ($a + 1) % 256;
8009 $j = ($j + $box[$a]) % 256;
8010 $temp = $box[$a];
8011 $box[$a] = $box[$j];
8012 $box[$j] = $temp;
8013 $k = $box[(($box[$a] + $box[$j]) % 256)];
8014 $cipherby = ord(substr($data, $i, 1)) ^ $k;
8015 $cipher .= chr($cipherby);
8018 if ($case == 'de') {
8019 $cipher = urldecode(urlencode($cipher));
8020 } else {
8021 $cipher = urlencode($cipher);
8024 return $cipher;
8027 /// ENVIRONMENT CHECKING ////////////////////////////////////////////////////////////
8030 * Returns the exact absolute path to plugin directory.
8032 * @param string $plugintype type of plugin
8033 * @param string $name name of the plugin
8034 * @return string full path to plugin directory; NULL if not found
8036 function get_plugin_directory($plugintype, $name) {
8037 global $CFG;
8039 if ($plugintype === '') {
8040 $plugintype = 'mod';
8043 $types = get_plugin_types(true);
8044 if (!array_key_exists($plugintype, $types)) {
8045 return NULL;
8047 $name = clean_param($name, PARAM_SAFEDIR); // just in case ;-)
8049 if (!empty($CFG->themedir) and $plugintype === 'theme') {
8050 if (!is_dir($types['theme'] . '/' . $name)) {
8051 // ok, so the theme is supposed to be in the $CFG->themedir
8052 return $CFG->themedir . '/' . $name;
8056 return $types[$plugintype].'/'.$name;
8060 * Return exact absolute path to a plugin directory.
8062 * @param string $component name such as 'moodle', 'mod_forum'
8063 * @return string full path to component directory; NULL if not found
8065 function get_component_directory($component) {
8066 global $CFG;
8068 list($type, $plugin) = normalize_component($component);
8070 if ($type === 'core') {
8071 if ($plugin === NULL ) {
8072 $path = $CFG->libdir;
8073 } else {
8074 $subsystems = get_core_subsystems();
8075 if (isset($subsystems[$plugin])) {
8076 $path = $CFG->dirroot.'/'.$subsystems[$plugin];
8077 } else {
8078 $path = NULL;
8082 } else {
8083 $path = get_plugin_directory($type, $plugin);
8086 return $path;
8090 * Normalize the component name using the "frankenstyle" names.
8091 * @param string $component
8092 * @return array $type+$plugin elements
8094 function normalize_component($component) {
8095 if ($component === 'moodle' or $component === 'core') {
8096 $type = 'core';
8097 $plugin = NULL;
8099 } else if (strpos($component, '_') === false) {
8100 $subsystems = get_core_subsystems();
8101 if (array_key_exists($component, $subsystems)) {
8102 $type = 'core';
8103 $plugin = $component;
8104 } else {
8105 // everything else is a module
8106 $type = 'mod';
8107 $plugin = $component;
8110 } else {
8111 list($type, $plugin) = explode('_', $component, 2);
8112 $plugintypes = get_plugin_types(false);
8113 if ($type !== 'core' and !array_key_exists($type, $plugintypes)) {
8114 $type = 'mod';
8115 $plugin = $component;
8119 return array($type, $plugin);
8123 * List all core subsystems and their location
8125 * This is a whitelist of components that are part of the core and their
8126 * language strings are defined in /lang/en/<<subsystem>>.php. If a given
8127 * plugin is not listed here and it does not have proper plugintype prefix,
8128 * then it is considered as course activity module.
8130 * The location is dirroot relative path. NULL means there is no special
8131 * directory for this subsystem. If the location is set, the subsystem's
8132 * renderer.php is expected to be there.
8134 * @return array of (string)name => (string|null)location
8136 function get_core_subsystems() {
8137 global $CFG;
8139 static $info = null;
8141 if (!$info) {
8142 $info = array(
8143 'access' => NULL,
8144 'admin' => $CFG->admin,
8145 'auth' => 'auth',
8146 'backup' => 'backup/util/ui',
8147 'badges' => 'badges',
8148 'block' => 'blocks',
8149 'blog' => 'blog',
8150 'bulkusers' => NULL,
8151 'cache' => 'cache',
8152 'calendar' => 'calendar',
8153 'cohort' => 'cohort',
8154 'condition' => NULL,
8155 'completion' => NULL,
8156 'countries' => NULL,
8157 'course' => 'course',
8158 'currencies' => NULL,
8159 'dbtransfer' => NULL,
8160 'debug' => NULL,
8161 'dock' => NULL,
8162 'editor' => 'lib/editor',
8163 'edufields' => NULL,
8164 'enrol' => 'enrol',
8165 'error' => NULL,
8166 'filepicker' => NULL,
8167 'files' => 'files',
8168 'filters' => NULL,
8169 'fonts' => NULL,
8170 'form' => 'lib/form',
8171 'grades' => 'grade',
8172 'grading' => 'grade/grading',
8173 'group' => 'group',
8174 'help' => NULL,
8175 'hub' => NULL,
8176 'imscc' => NULL,
8177 'install' => NULL,
8178 'iso6392' => NULL,
8179 'langconfig' => NULL,
8180 'license' => NULL,
8181 'mathslib' => NULL,
8182 'media' => 'media',
8183 'message' => 'message',
8184 'mimetypes' => NULL,
8185 'mnet' => 'mnet',
8186 'moodle.org' => NULL, // the dot is nasty, watch out! should be renamed to moodleorg
8187 'my' => 'my',
8188 'notes' => 'notes',
8189 'pagetype' => NULL,
8190 'pix' => NULL,
8191 'plagiarism' => 'plagiarism',
8192 'plugin' => NULL,
8193 'portfolio' => 'portfolio',
8194 'publish' => 'course/publish',
8195 'question' => 'question',
8196 'rating' => 'rating',
8197 'register' => 'admin/registration', //TODO: this is wrong, unfortunately we would need to modify hub code to pass around the correct url
8198 'repository' => 'repository',
8199 'rss' => 'rss',
8200 'role' => $CFG->admin.'/role',
8201 'search' => 'search',
8202 'table' => NULL,
8203 'tag' => 'tag',
8204 'timezones' => NULL,
8205 'user' => 'user',
8206 'userkey' => NULL,
8207 'webservice' => 'webservice',
8211 return $info;
8215 * Lists all plugin types
8216 * @param bool $fullpaths false means relative paths from dirroot
8217 * @return array Array of strings - name=>location
8219 function get_plugin_types($fullpaths=true) {
8220 global $CFG;
8222 $cache = cache::make('core', 'plugintypes');
8224 if ($fullpaths) {
8225 $cached = $cache->get(1);
8226 } else {
8227 $cached = $cache->get(0);
8230 if ($cached !== false) {
8231 return $cached;
8233 } else {
8234 $info = array('qtype' => 'question/type',
8235 'mod' => 'mod',
8236 'auth' => 'auth',
8237 'enrol' => 'enrol',
8238 'message' => 'message/output',
8239 'block' => 'blocks',
8240 'filter' => 'filter',
8241 'editor' => 'lib/editor',
8242 'format' => 'course/format',
8243 'profilefield' => 'user/profile/field',
8244 'report' => 'report',
8245 'coursereport' => 'course/report', // must be after system reports
8246 'gradeexport' => 'grade/export',
8247 'gradeimport' => 'grade/import',
8248 'gradereport' => 'grade/report',
8249 'gradingform' => 'grade/grading/form',
8250 'mnetservice' => 'mnet/service',
8251 'webservice' => 'webservice',
8252 'repository' => 'repository',
8253 'portfolio' => 'portfolio',
8254 'qbehaviour' => 'question/behaviour',
8255 'qformat' => 'question/format',
8256 'plagiarism' => 'plagiarism',
8257 'tool' => $CFG->admin.'/tool',
8258 'cachestore' => 'cache/stores',
8259 'cachelock' => 'cache/locks',
8260 'theme' => 'theme', // this is a bit hacky, themes may be in $CFG->themedir too
8263 $subpluginowners = array_merge(array_values(get_plugin_list('mod')),
8264 array_values(get_plugin_list('editor')));
8265 foreach ($subpluginowners as $ownerdir) {
8266 if (file_exists("$ownerdir/db/subplugins.php")) {
8267 $subplugins = array();
8268 include("$ownerdir/db/subplugins.php");
8269 foreach ($subplugins as $subtype=>$dir) {
8270 $info[$subtype] = $dir;
8275 // local is always last!
8276 $info['local'] = 'local';
8278 $fullinfo = array();
8279 foreach ($info as $type => $dir) {
8280 $fullinfo[$type] = $CFG->dirroot.'/'.$dir;
8283 $cache->set(0, $info);
8284 $cache->set(1, $fullinfo);
8286 return ($fullpaths ? $fullinfo : $info);
8291 * Simplified version of get_list_of_plugins()
8292 * @param string $plugintype type of plugin
8293 * @return array name=>fulllocation pairs of plugins of given type
8295 function get_plugin_list($plugintype) {
8296 global $CFG;
8298 $cache = cache::make('core', 'pluginlist');
8299 $cached = $cache->get($plugintype);
8300 if ($cached !== false) {
8301 return $cached;
8304 $ignored = array('CVS', '_vti_cnf', 'simpletest', 'db', 'yui', 'tests');
8305 if ($plugintype == 'auth') {
8306 // Historically we have had an auth plugin called 'db', so allow a special case.
8307 $key = array_search('db', $ignored);
8308 if ($key !== false) {
8309 unset($ignored[$key]);
8313 if ($plugintype === '') {
8314 $plugintype = 'mod';
8317 $fulldirs = array();
8319 if ($plugintype === 'mod') {
8320 // mod is an exception because we have to call this function from get_plugin_types()
8321 $fulldirs[] = $CFG->dirroot.'/mod';
8323 } else if ($plugintype === 'editor') {
8324 // Exception also needed for editor for same reason.
8325 $fulldirs[] = $CFG->dirroot . '/lib/editor';
8327 } else if ($plugintype === 'theme') {
8328 $fulldirs[] = $CFG->dirroot.'/theme';
8329 // themes are special because they may be stored also in separate directory
8330 if (!empty($CFG->themedir) and file_exists($CFG->themedir) and is_dir($CFG->themedir) ) {
8331 $fulldirs[] = $CFG->themedir;
8334 } else {
8335 $types = get_plugin_types(true);
8336 if (!array_key_exists($plugintype, $types)) {
8337 $cache->set($plugintype, array());
8338 return array();
8340 $fulldir = $types[$plugintype];
8341 if (!file_exists($fulldir)) {
8342 $cache->set($plugintype, array());
8343 return array();
8345 $fulldirs[] = $fulldir;
8347 $result = array();
8349 foreach ($fulldirs as $fulldir) {
8350 if (!is_dir($fulldir)) {
8351 continue;
8353 $items = new DirectoryIterator($fulldir);
8354 foreach ($items as $item) {
8355 if ($item->isDot() or !$item->isDir()) {
8356 continue;
8358 $pluginname = $item->getFilename();
8359 if (in_array($pluginname, $ignored)) {
8360 continue;
8362 $pluginname = clean_param($pluginname, PARAM_PLUGIN);
8363 if (empty($pluginname)) {
8364 // better ignore plugins with problematic names here
8365 continue;
8367 $result[$pluginname] = $fulldir.'/'.$pluginname;
8368 unset($item);
8370 unset($items);
8373 //TODO: implement better sorting once we migrated all plugin names to 'pluginname', ksort does not work for unicode, that is why we have to sort by the dir name, not the strings!
8374 ksort($result);
8375 $cache->set($plugintype, $result);
8376 return $result;
8380 * Get a list of all the plugins of a given type that contain a particular file.
8381 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
8382 * @param string $file the name of file that must be present in the plugin.
8383 * (e.g. 'view.php', 'db/install.xml').
8384 * @param bool $include if true (default false), the file will be include_once-ed if found.
8385 * @return array with plugin name as keys (e.g. 'forum', 'courselist') and the path
8386 * to the file relative to dirroot as value (e.g. "$CFG->dirroot/mod/forum/view.php").
8388 function get_plugin_list_with_file($plugintype, $file, $include = false) {
8389 global $CFG; // Necessary in case it is referenced by include()d PHP scripts.
8391 $plugins = array();
8393 foreach(get_plugin_list($plugintype) as $plugin => $dir) {
8394 $path = $dir . '/' . $file;
8395 if (file_exists($path)) {
8396 if ($include) {
8397 include_once($path);
8399 $plugins[$plugin] = $path;
8403 return $plugins;
8407 * Get a list of all the plugins of a given type that define a certain API function
8408 * in a certain file. The plugin component names and function names are returned.
8410 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
8411 * @param string $function the part of the name of the function after the
8412 * frankenstyle prefix. e.g 'hook' if you are looking for functions with
8413 * names like report_courselist_hook.
8414 * @param string $file the name of file within the plugin that defines the
8415 * function. Defaults to lib.php.
8416 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
8417 * and the function names as values (e.g. 'report_courselist_hook', 'forum_hook').
8419 function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php') {
8420 $pluginfunctions = array();
8421 foreach (get_plugin_list_with_file($plugintype, $file, true) as $plugin => $notused) {
8422 $fullfunction = $plugintype . '_' . $plugin . '_' . $function;
8424 if (function_exists($fullfunction)) {
8425 // Function exists with standard name. Store, indexed by
8426 // frankenstyle name of plugin
8427 $pluginfunctions[$plugintype . '_' . $plugin] = $fullfunction;
8429 } else if ($plugintype === 'mod') {
8430 // For modules, we also allow plugin without full frankenstyle
8431 // but just starting with the module name
8432 $shortfunction = $plugin . '_' . $function;
8433 if (function_exists($shortfunction)) {
8434 $pluginfunctions[$plugintype . '_' . $plugin] = $shortfunction;
8438 return $pluginfunctions;
8442 * Get a list of all the plugins of a given type that define a certain class
8443 * in a certain file. The plugin component names and class names are returned.
8445 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
8446 * @param string $class the part of the name of the class after the
8447 * frankenstyle prefix. e.g 'thing' if you are looking for classes with
8448 * names like report_courselist_thing. If you are looking for classes with
8449 * the same name as the plugin name (e.g. qtype_multichoice) then pass ''.
8450 * @param string $file the name of file within the plugin that defines the class.
8451 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
8452 * and the class names as values (e.g. 'report_courselist_thing', 'qtype_multichoice').
8454 function get_plugin_list_with_class($plugintype, $class, $file) {
8455 if ($class) {
8456 $suffix = '_' . $class;
8457 } else {
8458 $suffix = '';
8461 $pluginclasses = array();
8462 foreach (get_plugin_list_with_file($plugintype, $file, true) as $plugin => $notused) {
8463 $classname = $plugintype . '_' . $plugin . $suffix;
8464 if (class_exists($classname)) {
8465 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
8469 return $pluginclasses;
8473 * Lists plugin-like directories within specified directory
8475 * This function was originally used for standard Moodle plugins, please use
8476 * new get_plugin_list() now.
8478 * This function is used for general directory listing and backwards compatility.
8480 * @param string $directory relative directory from root
8481 * @param string $exclude dir name to exclude from the list (defaults to none)
8482 * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot)
8483 * @return array Sorted array of directory names found under the requested parameters
8485 function get_list_of_plugins($directory='mod', $exclude='', $basedir='') {
8486 global $CFG;
8488 $plugins = array();
8490 if (empty($basedir)) {
8491 $basedir = $CFG->dirroot .'/'. $directory;
8493 } else {
8494 $basedir = $basedir .'/'. $directory;
8497 if (file_exists($basedir) && filetype($basedir) == 'dir') {
8498 if (!$dirhandle = opendir($basedir)) {
8499 debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER);
8500 return array();
8502 while (false !== ($dir = readdir($dirhandle))) {
8503 $firstchar = substr($dir, 0, 1);
8504 if ($firstchar === '.' or $dir === 'CVS' or $dir === '_vti_cnf' or $dir === 'simpletest' or $dir === 'yui' or $dir === 'phpunit' or $dir === $exclude) {
8505 continue;
8507 if (filetype($basedir .'/'. $dir) != 'dir') {
8508 continue;
8510 $plugins[] = $dir;
8512 closedir($dirhandle);
8514 if ($plugins) {
8515 asort($plugins);
8517 return $plugins;
8521 * Invoke plugin's callback functions
8523 * @param string $type plugin type e.g. 'mod'
8524 * @param string $name plugin name
8525 * @param string $feature feature name
8526 * @param string $action feature's action
8527 * @param array $params parameters of callback function, should be an array
8528 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
8529 * @return mixed
8531 * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743
8533 function plugin_callback($type, $name, $feature, $action, $params = null, $default = null) {
8534 return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default);
8538 * Invoke component's callback functions
8540 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
8541 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
8542 * @param array $params parameters of callback function
8543 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
8544 * @return mixed
8546 function component_callback($component, $function, array $params = array(), $default = null) {
8547 global $CFG; // this is needed for require_once() below
8549 $cleancomponent = clean_param($component, PARAM_COMPONENT);
8550 if (empty($cleancomponent)) {
8551 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
8553 $component = $cleancomponent;
8555 list($type, $name) = normalize_component($component);
8556 $component = $type . '_' . $name;
8558 $oldfunction = $name.'_'.$function;
8559 $function = $component.'_'.$function;
8561 $dir = get_component_directory($component);
8562 if (empty($dir)) {
8563 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
8566 // Load library and look for function
8567 if (file_exists($dir.'/lib.php')) {
8568 require_once($dir.'/lib.php');
8571 if (!function_exists($function) and function_exists($oldfunction)) {
8572 if ($type !== 'mod' and $type !== 'core') {
8573 debugging("Please use new function name $function instead of legacy $oldfunction");
8575 $function = $oldfunction;
8578 if (function_exists($function)) {
8579 // Function exists, so just return function result
8580 $ret = call_user_func_array($function, $params);
8581 if (is_null($ret)) {
8582 return $default;
8583 } else {
8584 return $ret;
8587 return $default;
8591 * Checks whether a plugin supports a specified feature.
8593 * @param string $type Plugin type e.g. 'mod'
8594 * @param string $name Plugin name e.g. 'forum'
8595 * @param string $feature Feature code (FEATURE_xx constant)
8596 * @param mixed $default default value if feature support unknown
8597 * @return mixed Feature result (false if not supported, null if feature is unknown,
8598 * otherwise usually true but may have other feature-specific value such as array)
8600 function plugin_supports($type, $name, $feature, $default = NULL) {
8601 global $CFG;
8603 if ($type === 'mod' and $name === 'NEWMODULE') {
8604 //somebody forgot to rename the module template
8605 return false;
8608 $component = clean_param($type . '_' . $name, PARAM_COMPONENT);
8609 if (empty($component)) {
8610 throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name);
8613 $function = null;
8615 if ($type === 'mod') {
8616 // we need this special case because we support subplugins in modules,
8617 // otherwise it would end up in infinite loop
8618 if (file_exists("$CFG->dirroot/mod/$name/lib.php")) {
8619 include_once("$CFG->dirroot/mod/$name/lib.php");
8620 $function = $component.'_supports';
8621 if (!function_exists($function)) {
8622 // legacy non-frankenstyle function name
8623 $function = $name.'_supports';
8625 } else {
8626 // invalid module
8629 } else {
8630 if (!$path = get_plugin_directory($type, $name)) {
8631 // non existent plugin type
8632 return false;
8634 if (file_exists("$path/lib.php")) {
8635 include_once("$path/lib.php");
8636 $function = $component.'_supports';
8640 if ($function and function_exists($function)) {
8641 $supports = $function($feature);
8642 if (is_null($supports)) {
8643 // plugin does not know - use default
8644 return $default;
8645 } else {
8646 return $supports;
8650 //plugin does not care, so use default
8651 return $default;
8655 * Returns true if the current version of PHP is greater that the specified one.
8657 * @todo Check PHP version being required here is it too low?
8659 * @param string $version The version of php being tested.
8660 * @return bool
8662 function check_php_version($version='5.2.4') {
8663 return (version_compare(phpversion(), $version) >= 0);
8667 * Checks to see if is the browser operating system matches the specified
8668 * brand.
8670 * Known brand: 'Windows','Linux','Macintosh','SGI','SunOS','HP-UX'
8672 * @uses $_SERVER
8673 * @param string $brand The operating system identifier being tested
8674 * @return bool true if the given brand below to the detected operating system
8676 function check_browser_operating_system($brand) {
8677 if (empty($_SERVER['HTTP_USER_AGENT'])) {
8678 return false;
8681 if (preg_match("/$brand/i", $_SERVER['HTTP_USER_AGENT'])) {
8682 return true;
8685 return false;
8689 * Checks to see if is a browser matches the specified
8690 * brand and is equal or better version.
8692 * @uses $_SERVER
8693 * @param string $brand The browser identifier being tested
8694 * @param int $version The version of the browser, if not specified any version (except 5.5 for IE for BC reasons)
8695 * @return bool true if the given version is below that of the detected browser
8697 function check_browser_version($brand, $version = null) {
8698 if (empty($_SERVER['HTTP_USER_AGENT'])) {
8699 return false;
8702 $agent = $_SERVER['HTTP_USER_AGENT'];
8704 switch ($brand) {
8706 case 'Camino': /// OSX browser using Gecke engine
8707 if (strpos($agent, 'Camino') === false) {
8708 return false;
8710 if (empty($version)) {
8711 return true; // no version specified
8713 if (preg_match("/Camino\/([0-9\.]+)/i", $agent, $match)) {
8714 if (version_compare($match[1], $version) >= 0) {
8715 return true;
8718 break;
8721 case 'Firefox': /// Mozilla Firefox browsers
8722 if (strpos($agent, 'Iceweasel') === false and strpos($agent, 'Firefox') === false) {
8723 return false;
8725 if (empty($version)) {
8726 return true; // no version specified
8728 if (preg_match("/(Iceweasel|Firefox)\/([0-9\.]+)/i", $agent, $match)) {
8729 if (version_compare($match[2], $version) >= 0) {
8730 return true;
8733 break;
8736 case 'Gecko': /// Gecko based browsers
8737 // Do not look for dates any more, we expect real Firefox version here.
8738 if (empty($version)) {
8739 $version = 1;
8740 } else if ($version > 20000000) {
8741 // This is just a guess, it is not supposed to be 100% accurate!
8742 if (preg_match('/^201/', $version)) {
8743 $version = 3.6;
8744 } else if (preg_match('/^200[7-9]/', $version)) {
8745 $version = 3;
8746 } else if (preg_match('/^2006/', $version)) {
8747 $version = 2;
8748 } else {
8749 $version = 1.5;
8752 if (preg_match("/(Iceweasel|Firefox)\/([0-9\.]+)/i", $agent, $match)) {
8753 // Use real Firefox version if specified in user agent string.
8754 if (version_compare($match[2], $version) >= 0) {
8755 return true;
8757 } else if (preg_match("/Gecko\/([0-9\.]+)/i", $agent, $match)) {
8758 // Gecko might contain date or Firefox revision, let's just guess the Firefox version from the date.
8759 $browserver = $match[1];
8760 if ($browserver > 20000000) {
8761 // This is just a guess, it is not supposed to be 100% accurate!
8762 if (preg_match('/^201/', $browserver)) {
8763 $browserver = 3.6;
8764 } else if (preg_match('/^200[7-9]/', $browserver)) {
8765 $browserver = 3;
8766 } else if (preg_match('/^2006/', $version)) {
8767 $browserver = 2;
8768 } else {
8769 $browserver = 1.5;
8772 if (version_compare($browserver, $version) >= 0) {
8773 return true;
8776 break;
8779 case 'MSIE': /// Internet Explorer
8780 if (strpos($agent, 'Opera') !== false) { // Reject Opera
8781 return false;
8783 // In case of IE we have to deal with BC of the version parameter.
8784 if (is_null($version)) {
8785 $version = 5.5; // Anything older is not considered a browser at all!
8787 // IE uses simple versions, let's cast it to float to simplify the logic here.
8788 $version = round($version, 1);
8789 // See: http://www.useragentstring.com/pages/Internet%20Explorer/
8790 if (preg_match("/MSIE ([0-9\.]+)/", $agent, $match)) {
8791 $browser = $match[1];
8792 } else {
8793 return false;
8795 // IE8 and later versions may pretend to be IE7 for intranet sites, use Trident version instead,
8796 // the Trident should always describe the capabilities of IE in any emulation mode.
8797 if ($browser === '7.0' and preg_match("/Trident\/([0-9\.]+)/", $agent, $match)) {
8798 $browser = $match[1] + 4; // NOTE: Hopefully this will work also for future IE versions.
8800 $browser = round($browser, 1);
8801 return ($browser >= $version);
8802 break;
8805 case 'Opera': /// Opera
8806 if (strpos($agent, 'Opera') === false) {
8807 return false;
8809 if (empty($version)) {
8810 return true; // no version specified
8812 // Recent Opera useragents have Version/ with the actual version, e.g.:
8813 // Opera/9.80 (Windows NT 6.1; WOW64; U; en) Presto/2.10.289 Version/12.01
8814 // That's Opera 12.01, not 9.8.
8815 if (preg_match("/Version\/([0-9\.]+)/i", $agent, $match)) {
8816 if (version_compare($match[1], $version) >= 0) {
8817 return true;
8819 } else if (preg_match("/Opera\/([0-9\.]+)/i", $agent, $match)) {
8820 if (version_compare($match[1], $version) >= 0) {
8821 return true;
8824 break;
8827 case 'WebKit': /// WebKit based browser - everything derived from it (Safari, Chrome, iOS, Android and other mobiles)
8828 if (strpos($agent, 'AppleWebKit') === false) {
8829 return false;
8831 if (empty($version)) {
8832 return true; // no version specified
8834 if (preg_match("/AppleWebKit\/([0-9.]+)/i", $agent, $match)) {
8835 if (version_compare($match[1], $version) >= 0) {
8836 return true;
8839 break;
8842 case 'Safari': /// Desktop version of Apple Safari browser - no mobile or touch devices
8843 if (strpos($agent, 'AppleWebKit') === false) {
8844 return false;
8846 // Look for AppleWebKit, excluding strings with OmniWeb, Shiira and SymbianOS and any other mobile devices
8847 if (strpos($agent, 'OmniWeb')) { // Reject OmniWeb
8848 return false;
8850 if (strpos($agent, 'Shiira')) { // Reject Shiira
8851 return false;
8853 if (strpos($agent, 'SymbianOS')) { // Reject SymbianOS
8854 return false;
8856 if (strpos($agent, 'Android')) { // Reject Androids too
8857 return false;
8859 if (strpos($agent, 'iPhone') or strpos($agent, 'iPad') or strpos($agent, 'iPod')) {
8860 // No Apple mobile devices here - editor does not work, course ajax is not touch compatible, etc.
8861 return false;
8863 if (strpos($agent, 'Chrome')) { // Reject chrome browsers - it needs to be tested explicitly
8864 return false;
8867 if (empty($version)) {
8868 return true; // no version specified
8870 if (preg_match("/AppleWebKit\/([0-9.]+)/i", $agent, $match)) {
8871 if (version_compare($match[1], $version) >= 0) {
8872 return true;
8875 break;
8878 case 'Chrome':
8879 if (strpos($agent, 'Chrome') === false) {
8880 return false;
8882 if (empty($version)) {
8883 return true; // no version specified
8885 if (preg_match("/Chrome\/(.*)[ ]+/i", $agent, $match)) {
8886 if (version_compare($match[1], $version) >= 0) {
8887 return true;
8890 break;
8893 case 'Safari iOS': /// Safari on iPhone, iPad and iPod touch
8894 if (strpos($agent, 'AppleWebKit') === false or strpos($agent, 'Safari') === false) {
8895 return false;
8897 if (!strpos($agent, 'iPhone') and !strpos($agent, 'iPad') and !strpos($agent, 'iPod')) {
8898 return false;
8900 if (empty($version)) {
8901 return true; // no version specified
8903 if (preg_match("/AppleWebKit\/([0-9]+)/i", $agent, $match)) {
8904 if (version_compare($match[1], $version) >= 0) {
8905 return true;
8908 break;
8911 case 'WebKit Android': /// WebKit browser on Android
8912 if (strpos($agent, 'Linux; U; Android') === false) {
8913 return false;
8915 if (empty($version)) {
8916 return true; // no version specified
8918 if (preg_match("/AppleWebKit\/([0-9]+)/i", $agent, $match)) {
8919 if (version_compare($match[1], $version) >= 0) {
8920 return true;
8923 break;
8927 return false;
8931 * Returns whether a device/browser combination is mobile, tablet, legacy, default or the result of
8932 * an optional admin specified regular expression. If enabledevicedetection is set to no or not set
8933 * it returns default
8935 * @return string device type
8937 function get_device_type() {
8938 global $CFG;
8940 if (empty($CFG->enabledevicedetection) || empty($_SERVER['HTTP_USER_AGENT'])) {
8941 return 'default';
8944 $useragent = $_SERVER['HTTP_USER_AGENT'];
8946 if (!empty($CFG->devicedetectregex)) {
8947 $regexes = json_decode($CFG->devicedetectregex);
8949 foreach ($regexes as $value=>$regex) {
8950 if (preg_match($regex, $useragent)) {
8951 return $value;
8956 //mobile detection PHP direct copy from open source detectmobilebrowser.com
8957 $phonesregex = '/android .+ mobile|avantgo|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i';
8958 $modelsregex = '/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|e\-|e\/|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(di|rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|xda(\-|2|g)|yas\-|your|zeto|zte\-/i';
8959 if (preg_match($phonesregex,$useragent) || preg_match($modelsregex,substr($useragent, 0, 4))){
8960 return 'mobile';
8963 $tabletregex = '/Tablet browser|android|iPad|iProd|GT-P1000|GT-I9000|SHW-M180S|SGH-T849|SCH-I800|Build\/ERE27|sholest/i';
8964 if (preg_match($tabletregex, $useragent)) {
8965 return 'tablet';
8968 // Safe way to check for IE6 and not get false positives for some IE 7/8 users
8969 if (substr($_SERVER['HTTP_USER_AGENT'], 0, 34) === 'Mozilla/4.0 (compatible; MSIE 6.0;') {
8970 return 'legacy';
8973 return 'default';
8977 * Returns a list of the device types supporting by Moodle
8979 * @param boolean $incusertypes includes types specified using the devicedetectregex admin setting
8980 * @return array $types
8982 function get_device_type_list($incusertypes = true) {
8983 global $CFG;
8985 $types = array('default', 'legacy', 'mobile', 'tablet');
8987 if ($incusertypes && !empty($CFG->devicedetectregex)) {
8988 $regexes = json_decode($CFG->devicedetectregex);
8990 foreach ($regexes as $value => $regex) {
8991 $types[] = $value;
8995 return $types;
8999 * Returns the theme selected for a particular device or false if none selected.
9001 * @param string $devicetype
9002 * @return string|false The name of the theme to use for the device or the false if not set
9004 function get_selected_theme_for_device_type($devicetype = null) {
9005 global $CFG;
9007 if (empty($devicetype)) {
9008 $devicetype = get_user_device_type();
9011 $themevarname = get_device_cfg_var_name($devicetype);
9012 if (empty($CFG->$themevarname)) {
9013 return false;
9016 return $CFG->$themevarname;
9020 * Returns the name of the device type theme var in $CFG (because there is not a standard convention to allow backwards compatability
9022 * @param string $devicetype
9023 * @return string The config variable to use to determine the theme
9025 function get_device_cfg_var_name($devicetype = null) {
9026 if ($devicetype == 'default' || empty($devicetype)) {
9027 return 'theme';
9030 return 'theme' . $devicetype;
9034 * Allows the user to switch the device they are seeing the theme for.
9035 * This allows mobile users to switch back to the default theme, or theme for any other device.
9037 * @param string $newdevice The device the user is currently using.
9038 * @return string The device the user has switched to
9040 function set_user_device_type($newdevice) {
9041 global $USER;
9043 $devicetype = get_device_type();
9044 $devicetypes = get_device_type_list();
9046 if ($newdevice == $devicetype) {
9047 unset_user_preference('switchdevice'.$devicetype);
9048 } else if (in_array($newdevice, $devicetypes)) {
9049 set_user_preference('switchdevice'.$devicetype, $newdevice);
9054 * Returns the device the user is currently using, or if the user has chosen to switch devices
9055 * for the current device type the type they have switched to.
9057 * @return string The device the user is currently using or wishes to use
9059 function get_user_device_type() {
9060 $device = get_device_type();
9061 $switched = get_user_preferences('switchdevice'.$device, false);
9062 if ($switched != false) {
9063 return $switched;
9065 return $device;
9069 * Returns one or several CSS class names that match the user's browser. These can be put
9070 * in the body tag of the page to apply browser-specific rules without relying on CSS hacks
9072 * @return array An array of browser version classes
9074 function get_browser_version_classes() {
9075 $classes = array();
9077 if (check_browser_version("MSIE", "0")) {
9078 $classes[] = 'ie';
9079 for($i=12; $i>=6; $i--) {
9080 if (check_browser_version("MSIE", $i)) {
9081 $classes[] = 'ie'.$i;
9082 break;
9086 } else if (check_browser_version("Firefox") || check_browser_version("Gecko") || check_browser_version("Camino")) {
9087 $classes[] = 'gecko';
9088 if (preg_match('/rv\:([1-2])\.([0-9])/', $_SERVER['HTTP_USER_AGENT'], $matches)) {
9089 $classes[] = "gecko{$matches[1]}{$matches[2]}";
9092 } else if (check_browser_version("WebKit")) {
9093 $classes[] = 'safari';
9094 if (check_browser_version("Safari iOS")) {
9095 $classes[] = 'ios';
9097 } else if (check_browser_version("WebKit Android")) {
9098 $classes[] = 'android';
9101 } else if (check_browser_version("Opera")) {
9102 $classes[] = 'opera';
9106 return $classes;
9110 * Can handle rotated text. Whether it is safe to use the trickery in textrotate.js.
9112 * @return bool True for yes, false for no
9114 function can_use_rotated_text() {
9115 return check_browser_version('MSIE', 9) || check_browser_version('Firefox', 2) ||
9116 check_browser_version('Chrome', 21) || check_browser_version('Safari', 536.25) ||
9117 check_browser_version('Opera', 12) || check_browser_version('Safari iOS', 533);
9121 * Determine if moodle installation requires update
9123 * Checks version numbers of main code and all modules to see
9124 * if there are any mismatches
9126 * @global moodle_database $DB
9127 * @return bool
9129 function moodle_needs_upgrading() {
9130 global $CFG, $DB, $OUTPUT;
9132 if (empty($CFG->version)) {
9133 return true;
9136 // We have to purge plugin related caches now to be sure we have fresh data
9137 // and new plugins can be detected.
9138 cache::make('core', 'plugintypes')->purge();
9139 cache::make('core', 'pluginlist')->purge();
9140 cache::make('core', 'plugininfo_base')->purge();
9141 cache::make('core', 'plugininfo_mod')->purge();
9142 cache::make('core', 'plugininfo_block')->purge();
9143 cache::make('core', 'plugininfo_filter')->purge();
9144 cache::make('core', 'plugininfo_repository')->purge();
9145 cache::make('core', 'plugininfo_portfolio')->purge();
9147 // Check the main version first.
9148 $version = null;
9149 include($CFG->dirroot.'/version.php'); // defines $version and upgrades
9150 if ($version > $CFG->version) {
9151 return true;
9154 // modules
9155 $mods = get_plugin_list('mod');
9156 $installed = $DB->get_records('modules', array(), '', 'name, version');
9157 foreach ($mods as $mod => $fullmod) {
9158 if ($mod === 'NEWMODULE') { // Someone has unzipped the template, ignore it
9159 continue;
9161 $module = new stdClass();
9162 $plugin = new stdClass();
9163 if (!is_readable($fullmod.'/version.php')) {
9164 continue;
9166 include($fullmod.'/version.php'); // defines $module with version etc
9167 if (!isset($module->version) and isset($plugin->version)) {
9168 $module = $plugin;
9170 if (empty($installed[$mod])) {
9171 return true;
9172 } else if ($module->version > $installed[$mod]->version) {
9173 return true;
9176 unset($installed);
9178 // blocks
9179 $blocks = get_plugin_list('block');
9180 $installed = $DB->get_records('block', array(), '', 'name, version');
9181 require_once($CFG->dirroot.'/blocks/moodleblock.class.php');
9182 foreach ($blocks as $blockname=>$fullblock) {
9183 if ($blockname === 'NEWBLOCK') { // Someone has unzipped the template, ignore it
9184 continue;
9186 if (!is_readable($fullblock.'/version.php')) {
9187 continue;
9189 $plugin = new stdClass();
9190 $plugin->version = NULL;
9191 include($fullblock.'/version.php');
9192 if (empty($installed[$blockname])) {
9193 return true;
9194 } else if ($plugin->version > $installed[$blockname]->version) {
9195 return true;
9198 unset($installed);
9200 // now the rest of plugins
9201 $plugintypes = get_plugin_types();
9202 unset($plugintypes['mod']);
9203 unset($plugintypes['block']);
9205 $versions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin, value');
9206 foreach ($plugintypes as $type=>$unused) {
9207 $plugs = get_plugin_list($type);
9208 foreach ($plugs as $plug=>$fullplug) {
9209 $component = $type.'_'.$plug;
9210 if (!is_readable($fullplug.'/version.php')) {
9211 continue;
9213 $plugin = new stdClass();
9214 include($fullplug.'/version.php'); // defines $plugin with version etc
9215 if (array_key_exists($component, $versions)) {
9216 $installedversion = $versions[$component];
9217 } else {
9218 $installedversion = get_config($component, 'version');
9220 if (empty($installedversion)) { // new installation
9221 return true;
9222 } else if ($installedversion < $plugin->version) { // upgrade
9223 return true;
9228 return false;
9232 * Returns the major version of this site
9234 * Moodle version numbers consist of three numbers separated by a dot, for
9235 * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so
9236 * called major version. This function extracts the major version from either
9237 * $CFG->release (default) or eventually from the $release variable defined in
9238 * the main version.php.
9240 * @param bool $fromdisk should the version if source code files be used
9241 * @return string|false the major version like '2.3', false if could not be determined
9243 function moodle_major_version($fromdisk = false) {
9244 global $CFG;
9246 if ($fromdisk) {
9247 $release = null;
9248 require($CFG->dirroot.'/version.php');
9249 if (empty($release)) {
9250 return false;
9253 } else {
9254 if (empty($CFG->release)) {
9255 return false;
9257 $release = $CFG->release;
9260 if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) {
9261 return $matches[0];
9262 } else {
9263 return false;
9267 /// MISCELLANEOUS ////////////////////////////////////////////////////////////////////
9270 * Sets the system locale
9272 * @category string
9273 * @param string $locale Can be used to force a locale
9275 function moodle_setlocale($locale='') {
9276 global $CFG;
9278 static $currentlocale = ''; // last locale caching
9280 $oldlocale = $currentlocale;
9282 /// Fetch the correct locale based on ostype
9283 if ($CFG->ostype == 'WINDOWS') {
9284 $stringtofetch = 'localewin';
9285 } else {
9286 $stringtofetch = 'locale';
9289 /// the priority is the same as in get_string() - parameter, config, course, session, user, global language
9290 if (!empty($locale)) {
9291 $currentlocale = $locale;
9292 } else if (!empty($CFG->locale)) { // override locale for all language packs
9293 $currentlocale = $CFG->locale;
9294 } else {
9295 $currentlocale = get_string($stringtofetch, 'langconfig');
9298 /// do nothing if locale already set up
9299 if ($oldlocale == $currentlocale) {
9300 return;
9303 /// Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values,
9304 /// set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7
9305 /// Some day, numeric, monetary and other categories should be set too, I think. :-/
9307 /// Get current values
9308 $monetary= setlocale (LC_MONETARY, 0);
9309 $numeric = setlocale (LC_NUMERIC, 0);
9310 $ctype = setlocale (LC_CTYPE, 0);
9311 if ($CFG->ostype != 'WINDOWS') {
9312 $messages= setlocale (LC_MESSAGES, 0);
9314 /// Set locale to all
9315 setlocale (LC_ALL, $currentlocale);
9316 /// Set old values
9317 setlocale (LC_MONETARY, $monetary);
9318 setlocale (LC_NUMERIC, $numeric);
9319 if ($CFG->ostype != 'WINDOWS') {
9320 setlocale (LC_MESSAGES, $messages);
9322 if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') { // To workaround a well-known PHP problem with Turkish letter Ii
9323 setlocale (LC_CTYPE, $ctype);
9328 * Count words in a string.
9330 * Words are defined as things between whitespace.
9332 * @category string
9333 * @param string $string The text to be searched for words.
9334 * @return int The count of words in the specified string
9336 function count_words($string) {
9337 $string = strip_tags($string);
9338 return count(preg_split("/\w\b/", $string)) - 1;
9341 /** Count letters in a string.
9343 * Letters are defined as chars not in tags and different from whitespace.
9345 * @category string
9346 * @param string $string The text to be searched for letters.
9347 * @return int The count of letters in the specified text.
9349 function count_letters($string) {
9350 /// Loading the textlib singleton instance. We are going to need it.
9351 $string = strip_tags($string); // Tags are out now
9352 $string = preg_replace('/[[:space:]]*/','',$string); //Whitespace are out now
9354 return textlib::strlen($string);
9358 * Generate and return a random string of the specified length.
9360 * @param int $length The length of the string to be created.
9361 * @return string
9363 function random_string ($length=15) {
9364 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
9365 $pool .= 'abcdefghijklmnopqrstuvwxyz';
9366 $pool .= '0123456789';
9367 $poollen = strlen($pool);
9368 mt_srand ((double) microtime() * 1000000);
9369 $string = '';
9370 for ($i = 0; $i < $length; $i++) {
9371 $string .= substr($pool, (mt_rand()%($poollen)), 1);
9373 return $string;
9377 * Generate a complex random string (useful for md5 salts)
9379 * This function is based on the above {@link random_string()} however it uses a
9380 * larger pool of characters and generates a string between 24 and 32 characters
9382 * @param int $length Optional if set generates a string to exactly this length
9383 * @return string
9385 function complex_random_string($length=null) {
9386 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
9387 $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
9388 $poollen = strlen($pool);
9389 mt_srand ((double) microtime() * 1000000);
9390 if ($length===null) {
9391 $length = floor(rand(24,32));
9393 $string = '';
9394 for ($i = 0; $i < $length; $i++) {
9395 $string .= $pool[(mt_rand()%$poollen)];
9397 return $string;
9401 * Given some text (which may contain HTML) and an ideal length,
9402 * this function truncates the text neatly on a word boundary if possible
9404 * @category string
9405 * @global stdClass $CFG
9406 * @param string $text text to be shortened
9407 * @param int $ideal ideal string length
9408 * @param boolean $exact if false, $text will not be cut mid-word
9409 * @param string $ending The string to append if the passed string is truncated
9410 * @return string $truncate shortened string
9412 function shorten_text($text, $ideal=30, $exact = false, $ending='...') {
9414 global $CFG;
9416 // if the plain text is shorter than the maximum length, return the whole text
9417 if (textlib::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) {
9418 return $text;
9421 // Splits on HTML tags. Each open/close/empty tag will be the first thing
9422 // and only tag in its 'line'
9423 preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
9425 $total_length = textlib::strlen($ending);
9426 $truncate = '';
9428 // This array stores information about open and close tags and their position
9429 // in the truncated string. Each item in the array is an object with fields
9430 // ->open (true if open), ->tag (tag name in lower case), and ->pos
9431 // (byte position in truncated text)
9432 $tagdetails = array();
9434 foreach ($lines as $line_matchings) {
9435 // if there is any html-tag in this line, handle it and add it (uncounted) to the output
9436 if (!empty($line_matchings[1])) {
9437 // if it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>)
9438 if (preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $line_matchings[1])) {
9439 // do nothing
9440 // if tag is a closing tag (f.e. </b>)
9441 } else if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $line_matchings[1], $tag_matchings)) {
9442 // record closing tag
9443 $tagdetails[] = (object)array('open'=>false,
9444 'tag'=>textlib::strtolower($tag_matchings[1]), 'pos'=>textlib::strlen($truncate));
9445 // if tag is an opening tag (f.e. <b>)
9446 } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $line_matchings[1], $tag_matchings)) {
9447 // record opening tag
9448 $tagdetails[] = (object)array('open'=>true,
9449 'tag'=>textlib::strtolower($tag_matchings[1]), 'pos'=>textlib::strlen($truncate));
9451 // add html-tag to $truncate'd text
9452 $truncate .= $line_matchings[1];
9455 // calculate the length of the plain text part of the line; handle entities as one character
9456 $content_length = textlib::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $line_matchings[2]));
9457 if ($total_length+$content_length > $ideal) {
9458 // the number of characters which are left
9459 $left = $ideal - $total_length;
9460 $entities_length = 0;
9461 // search for html entities
9462 if (preg_match_all('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', $line_matchings[2], $entities, PREG_OFFSET_CAPTURE)) {
9463 // calculate the real length of all entities in the legal range
9464 foreach ($entities[0] as $entity) {
9465 if ($entity[1]+1-$entities_length <= $left) {
9466 $left--;
9467 $entities_length += textlib::strlen($entity[0]);
9468 } else {
9469 // no more characters left
9470 break;
9474 $truncate .= textlib::substr($line_matchings[2], 0, $left+$entities_length);
9475 // maximum length is reached, so get off the loop
9476 break;
9477 } else {
9478 $truncate .= $line_matchings[2];
9479 $total_length += $content_length;
9482 // if the maximum length is reached, get off the loop
9483 if($total_length >= $ideal) {
9484 break;
9488 // if the words shouldn't be cut in the middle...
9489 if (!$exact) {
9490 // ...search the last occurence of a space...
9491 for ($k=textlib::strlen($truncate);$k>0;$k--) {
9492 if ($char = textlib::substr($truncate, $k, 1)) {
9493 if ($char === '.' or $char === ' ') {
9494 $breakpos = $k+1;
9495 break;
9496 } else if (strlen($char) > 2) { // Chinese/Japanese/Korean text
9497 $breakpos = $k+1; // can be truncated at any UTF-8
9498 break; // character boundary.
9503 if (isset($breakpos)) {
9504 // ...and cut the text in this position
9505 $truncate = textlib::substr($truncate, 0, $breakpos);
9509 // add the defined ending to the text
9510 $truncate .= $ending;
9512 // Now calculate the list of open html tags based on the truncate position
9513 $open_tags = array();
9514 foreach ($tagdetails as $taginfo) {
9515 if(isset($breakpos) && $taginfo->pos >= $breakpos) {
9516 // Don't include tags after we made the break!
9517 break;
9519 if($taginfo->open) {
9520 // add tag to the beginning of $open_tags list
9521 array_unshift($open_tags, $taginfo->tag);
9522 } else {
9523 $pos = array_search($taginfo->tag, array_reverse($open_tags, true)); // can have multiple exact same open tags, close the last one
9524 if ($pos !== false) {
9525 unset($open_tags[$pos]);
9530 // close all unclosed html-tags
9531 foreach ($open_tags as $tag) {
9532 $truncate .= '</' . $tag . '>';
9535 return $truncate;
9540 * Given dates in seconds, how many weeks is the date from startdate
9541 * The first week is 1, the second 2 etc ...
9543 * @todo Finish documenting this function
9545 * @uses WEEKSECS
9546 * @param int $startdate Timestamp for the start date
9547 * @param int $thedate Timestamp for the end date
9548 * @return string
9550 function getweek ($startdate, $thedate) {
9551 if ($thedate < $startdate) { // error
9552 return 0;
9555 return floor(($thedate - $startdate) / WEEKSECS) + 1;
9559 * returns a randomly generated password of length $maxlen. inspired by
9561 * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and
9562 * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254}
9564 * @global stdClass $CFG
9565 * @param int $maxlen The maximum size of the password being generated.
9566 * @return string
9568 function generate_password($maxlen=10) {
9569 global $CFG;
9571 if (empty($CFG->passwordpolicy)) {
9572 $fillers = PASSWORD_DIGITS;
9573 $wordlist = file($CFG->wordlist);
9574 $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]);
9575 $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]);
9576 $filler1 = $fillers[rand(0, strlen($fillers) - 1)];
9577 $password = $word1 . $filler1 . $word2;
9578 } else {
9579 $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0;
9580 $digits = $CFG->minpassworddigits;
9581 $lower = $CFG->minpasswordlower;
9582 $upper = $CFG->minpasswordupper;
9583 $nonalphanum = $CFG->minpasswordnonalphanum;
9584 $total = $lower + $upper + $digits + $nonalphanum;
9585 // minlength should be the greater one of the two ( $minlen and $total )
9586 $minlen = $minlen < $total ? $total : $minlen;
9587 // maxlen can never be smaller than minlen
9588 $maxlen = $minlen > $maxlen ? $minlen : $maxlen;
9589 $additional = $maxlen - $total;
9591 // Make sure we have enough characters to fulfill
9592 // complexity requirements
9593 $passworddigits = PASSWORD_DIGITS;
9594 while ($digits > strlen($passworddigits)) {
9595 $passworddigits .= PASSWORD_DIGITS;
9597 $passwordlower = PASSWORD_LOWER;
9598 while ($lower > strlen($passwordlower)) {
9599 $passwordlower .= PASSWORD_LOWER;
9601 $passwordupper = PASSWORD_UPPER;
9602 while ($upper > strlen($passwordupper)) {
9603 $passwordupper .= PASSWORD_UPPER;
9605 $passwordnonalphanum = PASSWORD_NONALPHANUM;
9606 while ($nonalphanum > strlen($passwordnonalphanum)) {
9607 $passwordnonalphanum .= PASSWORD_NONALPHANUM;
9610 // Now mix and shuffle it all
9611 $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) .
9612 substr(str_shuffle ($passwordupper), 0, $upper) .
9613 substr(str_shuffle ($passworddigits), 0, $digits) .
9614 substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) .
9615 substr(str_shuffle ($passwordlower .
9616 $passwordupper .
9617 $passworddigits .
9618 $passwordnonalphanum), 0 , $additional));
9621 return substr ($password, 0, $maxlen);
9625 * Given a float, prints it nicely.
9626 * Localized floats must not be used in calculations!
9628 * The stripzeros feature is intended for making numbers look nicer in small
9629 * areas where it is not necessary to indicate the degree of accuracy by showing
9630 * ending zeros. If you turn it on with $decimalpoints set to 3, for example,
9631 * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'.
9633 * @param float $float The float to print
9634 * @param int $decimalpoints The number of decimal places to print.
9635 * @param bool $localized use localized decimal separator
9636 * @param bool $stripzeros If true, removes final zeros after decimal point
9637 * @return string locale float
9639 function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=false) {
9640 if (is_null($float)) {
9641 return '';
9643 if ($localized) {
9644 $separator = get_string('decsep', 'langconfig');
9645 } else {
9646 $separator = '.';
9648 $result = number_format($float, $decimalpoints, $separator, '');
9649 if ($stripzeros) {
9650 // Remove zeros and final dot if not needed
9651 $result = preg_replace('~(' . preg_quote($separator) . ')?0+$~', '', $result);
9653 return $result;
9657 * Converts locale specific floating point/comma number back to standard PHP float value
9658 * Do NOT try to do any math operations before this conversion on any user submitted floats!
9660 * @param string $locale_float locale aware float representation
9661 * @return float
9663 function unformat_float($locale_float) {
9664 $locale_float = trim($locale_float);
9666 if ($locale_float == '') {
9667 return null;
9670 $locale_float = str_replace(' ', '', $locale_float); // no spaces - those might be used as thousand separators
9672 return (float)str_replace(get_string('decsep', 'langconfig'), '.', $locale_float);
9676 * Given a simple array, this shuffles it up just like shuffle()
9677 * Unlike PHP's shuffle() this function works on any machine.
9679 * @param array $array The array to be rearranged
9680 * @return array
9682 function swapshuffle($array) {
9684 srand ((double) microtime() * 10000000);
9685 $last = count($array) - 1;
9686 for ($i=0;$i<=$last;$i++) {
9687 $from = rand(0,$last);
9688 $curr = $array[$i];
9689 $array[$i] = $array[$from];
9690 $array[$from] = $curr;
9692 return $array;
9696 * Like {@link swapshuffle()}, but works on associative arrays
9698 * @param array $array The associative array to be rearranged
9699 * @return array
9701 function swapshuffle_assoc($array) {
9703 $newarray = array();
9704 $newkeys = swapshuffle(array_keys($array));
9706 foreach ($newkeys as $newkey) {
9707 $newarray[$newkey] = $array[$newkey];
9709 return $newarray;
9713 * Given an arbitrary array, and a number of draws,
9714 * this function returns an array with that amount
9715 * of items. The indexes are retained.
9717 * @todo Finish documenting this function
9719 * @param array $array
9720 * @param int $draws
9721 * @return array
9723 function draw_rand_array($array, $draws) {
9724 srand ((double) microtime() * 10000000);
9726 $return = array();
9728 $last = count($array);
9730 if ($draws > $last) {
9731 $draws = $last;
9734 while ($draws > 0) {
9735 $last--;
9737 $keys = array_keys($array);
9738 $rand = rand(0, $last);
9740 $return[$keys[$rand]] = $array[$keys[$rand]];
9741 unset($array[$keys[$rand]]);
9743 $draws--;
9746 return $return;
9750 * Calculate the difference between two microtimes
9752 * @param string $a The first Microtime
9753 * @param string $b The second Microtime
9754 * @return string
9756 function microtime_diff($a, $b) {
9757 list($a_dec, $a_sec) = explode(' ', $a);
9758 list($b_dec, $b_sec) = explode(' ', $b);
9759 return $b_sec - $a_sec + $b_dec - $a_dec;
9763 * Given a list (eg a,b,c,d,e) this function returns
9764 * an array of 1->a, 2->b, 3->c etc
9766 * @param string $list The string to explode into array bits
9767 * @param string $separator The separator used within the list string
9768 * @return array The now assembled array
9770 function make_menu_from_list($list, $separator=',') {
9772 $array = array_reverse(explode($separator, $list), true);
9773 foreach ($array as $key => $item) {
9774 $outarray[$key+1] = trim($item);
9776 return $outarray;
9780 * Creates an array that represents all the current grades that
9781 * can be chosen using the given grading type.
9783 * Negative numbers
9784 * are scales, zero is no grade, and positive numbers are maximum
9785 * grades.
9787 * @todo Finish documenting this function or better deprecated this completely!
9789 * @param int $gradingtype
9790 * @return array
9792 function make_grades_menu($gradingtype) {
9793 global $DB;
9795 $grades = array();
9796 if ($gradingtype < 0) {
9797 if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) {
9798 return make_menu_from_list($scale->scale);
9800 } else if ($gradingtype > 0) {
9801 for ($i=$gradingtype; $i>=0; $i--) {
9802 $grades[$i] = $i .' / '. $gradingtype;
9804 return $grades;
9806 return $grades;
9810 * This function returns the number of activities
9811 * using scaleid in a courseid
9813 * @todo Finish documenting this function
9815 * @global object
9816 * @global object
9817 * @param int $courseid ?
9818 * @param int $scaleid ?
9819 * @return int
9821 function course_scale_used($courseid, $scaleid) {
9822 global $CFG, $DB;
9824 $return = 0;
9826 if (!empty($scaleid)) {
9827 if ($cms = get_course_mods($courseid)) {
9828 foreach ($cms as $cm) {
9829 //Check cm->name/lib.php exists
9830 if (file_exists($CFG->dirroot.'/mod/'.$cm->modname.'/lib.php')) {
9831 include_once($CFG->dirroot.'/mod/'.$cm->modname.'/lib.php');
9832 $function_name = $cm->modname.'_scale_used';
9833 if (function_exists($function_name)) {
9834 if ($function_name($cm->instance,$scaleid)) {
9835 $return++;
9842 // check if any course grade item makes use of the scale
9843 $return += $DB->count_records('grade_items', array('courseid'=>$courseid, 'scaleid'=>$scaleid));
9845 // check if any outcome in the course makes use of the scale
9846 $return += $DB->count_records_sql("SELECT COUNT('x')
9847 FROM {grade_outcomes_courses} goc,
9848 {grade_outcomes} go
9849 WHERE go.id = goc.outcomeid
9850 AND go.scaleid = ? AND goc.courseid = ?",
9851 array($scaleid, $courseid));
9853 return $return;
9857 * This function returns the number of activities
9858 * using scaleid in the entire site
9860 * @param int $scaleid
9861 * @param array $courses
9862 * @return int
9864 function site_scale_used($scaleid, &$courses) {
9865 $return = 0;
9867 if (!is_array($courses) || count($courses) == 0) {
9868 $courses = get_courses("all",false,"c.id,c.shortname");
9871 if (!empty($scaleid)) {
9872 if (is_array($courses) && count($courses) > 0) {
9873 foreach ($courses as $course) {
9874 $return += course_scale_used($course->id,$scaleid);
9878 return $return;
9882 * make_unique_id_code
9884 * @todo Finish documenting this function
9886 * @uses $_SERVER
9887 * @param string $extra Extra string to append to the end of the code
9888 * @return string
9890 function make_unique_id_code($extra='') {
9892 $hostname = 'unknownhost';
9893 if (!empty($_SERVER['HTTP_HOST'])) {
9894 $hostname = $_SERVER['HTTP_HOST'];
9895 } else if (!empty($_ENV['HTTP_HOST'])) {
9896 $hostname = $_ENV['HTTP_HOST'];
9897 } else if (!empty($_SERVER['SERVER_NAME'])) {
9898 $hostname = $_SERVER['SERVER_NAME'];
9899 } else if (!empty($_ENV['SERVER_NAME'])) {
9900 $hostname = $_ENV['SERVER_NAME'];
9903 $date = gmdate("ymdHis");
9905 $random = random_string(6);
9907 if ($extra) {
9908 return $hostname .'+'. $date .'+'. $random .'+'. $extra;
9909 } else {
9910 return $hostname .'+'. $date .'+'. $random;
9916 * Function to check the passed address is within the passed subnet
9918 * The parameter is a comma separated string of subnet definitions.
9919 * Subnet strings can be in one of three formats:
9920 * 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn (number of bits in net mask)
9921 * 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)
9922 * 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. (incomplete address, a bit non-technical ;-)
9923 * Code for type 1 modified from user posted comments by mediator at
9924 * {@link http://au.php.net/manual/en/function.ip2long.php}
9926 * @param string $addr The address you are checking
9927 * @param string $subnetstr The string of subnet addresses
9928 * @return bool
9930 function address_in_subnet($addr, $subnetstr) {
9932 if ($addr == '0.0.0.0') {
9933 return false;
9935 $subnets = explode(',', $subnetstr);
9936 $found = false;
9937 $addr = trim($addr);
9938 $addr = cleanremoteaddr($addr, false); // normalise
9939 if ($addr === null) {
9940 return false;
9942 $addrparts = explode(':', $addr);
9944 $ipv6 = strpos($addr, ':');
9946 foreach ($subnets as $subnet) {
9947 $subnet = trim($subnet);
9948 if ($subnet === '') {
9949 continue;
9952 if (strpos($subnet, '/') !== false) {
9953 ///1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn
9954 list($ip, $mask) = explode('/', $subnet);
9955 $mask = trim($mask);
9956 if (!is_number($mask)) {
9957 continue; // incorect mask number, eh?
9959 $ip = cleanremoteaddr($ip, false); // normalise
9960 if ($ip === null) {
9961 continue;
9963 if (strpos($ip, ':') !== false) {
9964 // IPv6
9965 if (!$ipv6) {
9966 continue;
9968 if ($mask > 128 or $mask < 0) {
9969 continue; // nonsense
9971 if ($mask == 0) {
9972 return true; // any address
9974 if ($mask == 128) {
9975 if ($ip === $addr) {
9976 return true;
9978 continue;
9980 $ipparts = explode(':', $ip);
9981 $modulo = $mask % 16;
9982 $ipnet = array_slice($ipparts, 0, ($mask-$modulo)/16);
9983 $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16);
9984 if (implode(':', $ipnet) === implode(':', $addrnet)) {
9985 if ($modulo == 0) {
9986 return true;
9988 $pos = ($mask-$modulo)/16;
9989 $ipnet = hexdec($ipparts[$pos]);
9990 $addrnet = hexdec($addrparts[$pos]);
9991 $mask = 0xffff << (16 - $modulo);
9992 if (($addrnet & $mask) == ($ipnet & $mask)) {
9993 return true;
9997 } else {
9998 // IPv4
9999 if ($ipv6) {
10000 continue;
10002 if ($mask > 32 or $mask < 0) {
10003 continue; // nonsense
10005 if ($mask == 0) {
10006 return true;
10008 if ($mask == 32) {
10009 if ($ip === $addr) {
10010 return true;
10012 continue;
10014 $mask = 0xffffffff << (32 - $mask);
10015 if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
10016 return true;
10020 } else if (strpos($subnet, '-') !== false) {
10021 /// 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.
10022 $parts = explode('-', $subnet);
10023 if (count($parts) != 2) {
10024 continue;
10027 if (strpos($subnet, ':') !== false) {
10028 // IPv6
10029 if (!$ipv6) {
10030 continue;
10032 $ipstart = cleanremoteaddr(trim($parts[0]), false); // normalise
10033 if ($ipstart === null) {
10034 continue;
10036 $ipparts = explode(':', $ipstart);
10037 $start = hexdec(array_pop($ipparts));
10038 $ipparts[] = trim($parts[1]);
10039 $ipend = cleanremoteaddr(implode(':', $ipparts), false); // normalise
10040 if ($ipend === null) {
10041 continue;
10043 $ipparts[7] = '';
10044 $ipnet = implode(':', $ipparts);
10045 if (strpos($addr, $ipnet) !== 0) {
10046 continue;
10048 $ipparts = explode(':', $ipend);
10049 $end = hexdec($ipparts[7]);
10051 $addrend = hexdec($addrparts[7]);
10053 if (($addrend >= $start) and ($addrend <= $end)) {
10054 return true;
10057 } else {
10058 // IPv4
10059 if ($ipv6) {
10060 continue;
10062 $ipstart = cleanremoteaddr(trim($parts[0]), false); // normalise
10063 if ($ipstart === null) {
10064 continue;
10066 $ipparts = explode('.', $ipstart);
10067 $ipparts[3] = trim($parts[1]);
10068 $ipend = cleanremoteaddr(implode('.', $ipparts), false); // normalise
10069 if ($ipend === null) {
10070 continue;
10073 if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
10074 return true;
10078 } else {
10079 /// 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
10080 if (strpos($subnet, ':') !== false) {
10081 // IPv6
10082 if (!$ipv6) {
10083 continue;
10085 $parts = explode(':', $subnet);
10086 $count = count($parts);
10087 if ($parts[$count-1] === '') {
10088 unset($parts[$count-1]); // trim trailing :
10089 $count--;
10090 $subnet = implode('.', $parts);
10092 $isip = cleanremoteaddr($subnet, false); // normalise
10093 if ($isip !== null) {
10094 if ($isip === $addr) {
10095 return true;
10097 continue;
10098 } else if ($count > 8) {
10099 continue;
10101 $zeros = array_fill(0, 8-$count, '0');
10102 $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16);
10103 if (address_in_subnet($addr, $subnet)) {
10104 return true;
10107 } else {
10108 // IPv4
10109 if ($ipv6) {
10110 continue;
10112 $parts = explode('.', $subnet);
10113 $count = count($parts);
10114 if ($parts[$count-1] === '') {
10115 unset($parts[$count-1]); // trim trailing .
10116 $count--;
10117 $subnet = implode('.', $parts);
10119 if ($count == 4) {
10120 $subnet = cleanremoteaddr($subnet, false); // normalise
10121 if ($subnet === $addr) {
10122 return true;
10124 continue;
10125 } else if ($count > 4) {
10126 continue;
10128 $zeros = array_fill(0, 4-$count, '0');
10129 $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8);
10130 if (address_in_subnet($addr, $subnet)) {
10131 return true;
10137 return false;
10141 * For outputting debugging info
10143 * @uses STDOUT
10144 * @param string $string The string to write
10145 * @param string $eol The end of line char(s) to use
10146 * @param string $sleep Period to make the application sleep
10147 * This ensures any messages have time to display before redirect
10149 function mtrace($string, $eol="\n", $sleep=0) {
10151 if (defined('STDOUT') and !PHPUNIT_TEST) {
10152 fwrite(STDOUT, $string.$eol);
10153 } else {
10154 echo $string . $eol;
10157 flush();
10159 //delay to keep message on user's screen in case of subsequent redirect
10160 if ($sleep) {
10161 sleep($sleep);
10166 * Replace 1 or more slashes or backslashes to 1 slash
10168 * @param string $path The path to strip
10169 * @return string the path with double slashes removed
10171 function cleardoubleslashes ($path) {
10172 return preg_replace('/(\/|\\\){1,}/','/',$path);
10176 * Is current ip in give list?
10178 * @param string $list
10179 * @return bool
10181 function remoteip_in_list($list){
10182 $inlist = false;
10183 $client_ip = getremoteaddr(null);
10185 if(!$client_ip){
10186 // ensure access on cli
10187 return true;
10190 $list = explode("\n", $list);
10191 foreach($list as $subnet) {
10192 $subnet = trim($subnet);
10193 if (address_in_subnet($client_ip, $subnet)) {
10194 $inlist = true;
10195 break;
10198 return $inlist;
10202 * Returns most reliable client address
10204 * @global object
10205 * @param string $default If an address can't be determined, then return this
10206 * @return string The remote IP address
10208 function getremoteaddr($default='0.0.0.0') {
10209 global $CFG;
10211 if (empty($CFG->getremoteaddrconf)) {
10212 // This will happen, for example, before just after the upgrade, as the
10213 // user is redirected to the admin screen.
10214 $variablestoskip = 0;
10215 } else {
10216 $variablestoskip = $CFG->getremoteaddrconf;
10218 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) {
10219 if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
10220 $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']);
10221 return $address ? $address : $default;
10224 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
10225 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
10226 $address = cleanremoteaddr($_SERVER['HTTP_X_FORWARDED_FOR']);
10227 return $address ? $address : $default;
10230 if (!empty($_SERVER['REMOTE_ADDR'])) {
10231 $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']);
10232 return $address ? $address : $default;
10233 } else {
10234 return $default;
10239 * Cleans an ip address. Internal addresses are now allowed.
10240 * (Originally local addresses were not allowed.)
10242 * @param string $addr IPv4 or IPv6 address
10243 * @param bool $compress use IPv6 address compression
10244 * @return string normalised ip address string, null if error
10246 function cleanremoteaddr($addr, $compress=false) {
10247 $addr = trim($addr);
10249 //TODO: maybe add a separate function is_addr_public() or something like this
10251 if (strpos($addr, ':') !== false) {
10252 // can be only IPv6
10253 $parts = explode(':', $addr);
10254 $count = count($parts);
10256 if (strpos($parts[$count-1], '.') !== false) {
10257 //legacy ipv4 notation
10258 $last = array_pop($parts);
10259 $ipv4 = cleanremoteaddr($last, true);
10260 if ($ipv4 === null) {
10261 return null;
10263 $bits = explode('.', $ipv4);
10264 $parts[] = dechex($bits[0]).dechex($bits[1]);
10265 $parts[] = dechex($bits[2]).dechex($bits[3]);
10266 $count = count($parts);
10267 $addr = implode(':', $parts);
10270 if ($count < 3 or $count > 8) {
10271 return null; // severly malformed
10274 if ($count != 8) {
10275 if (strpos($addr, '::') === false) {
10276 return null; // malformed
10278 // uncompress ::
10279 $insertat = array_search('', $parts, true);
10280 $missing = array_fill(0, 1 + 8 - $count, '0');
10281 array_splice($parts, $insertat, 1, $missing);
10282 foreach ($parts as $key=>$part) {
10283 if ($part === '') {
10284 $parts[$key] = '0';
10289 $adr = implode(':', $parts);
10290 if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) {
10291 return null; // incorrect format - sorry
10294 // normalise 0s and case
10295 $parts = array_map('hexdec', $parts);
10296 $parts = array_map('dechex', $parts);
10298 $result = implode(':', $parts);
10300 if (!$compress) {
10301 return $result;
10304 if ($result === '0:0:0:0:0:0:0:0') {
10305 return '::'; // all addresses
10308 $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1);
10309 if ($compressed !== $result) {
10310 return $compressed;
10313 $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
10314 if ($compressed !== $result) {
10315 return $compressed;
10318 $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
10319 if ($compressed !== $result) {
10320 return $compressed;
10323 return $result;
10326 // first get all things that look like IPv4 addresses
10327 $parts = array();
10328 if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
10329 return null;
10331 unset($parts[0]);
10333 foreach ($parts as $key=>$match) {
10334 if ($match > 255) {
10335 return null;
10337 $parts[$key] = (int)$match; // normalise 0s
10340 return implode('.', $parts);
10344 * This function will make a complete copy of anything it's given,
10345 * regardless of whether it's an object or not.
10347 * @param mixed $thing Something you want cloned
10348 * @return mixed What ever it is you passed it
10350 function fullclone($thing) {
10351 return unserialize(serialize($thing));
10356 * This function expects to called during shutdown
10357 * should be set via register_shutdown_function()
10358 * in lib/setup.php .
10360 * @return void
10362 function moodle_request_shutdown() {
10363 global $CFG;
10365 // help apache server if possible
10366 $apachereleasemem = false;
10367 if (function_exists('apache_child_terminate') && function_exists('memory_get_usage')
10368 && ini_get_bool('child_terminate')) {
10370 $limit = (empty($CFG->apachemaxmem) ? 64*1024*1024 : $CFG->apachemaxmem); //64MB default
10371 if (memory_get_usage() > get_real_size($limit)) {
10372 $apachereleasemem = $limit;
10373 @apache_child_terminate();
10377 // deal with perf logging
10378 if (defined('MDL_PERF') || (!empty($CFG->perfdebug) and $CFG->perfdebug > 7)) {
10379 if ($apachereleasemem) {
10380 error_log('Mem usage over '.$apachereleasemem.': marking Apache child for reaping.');
10382 if (defined('MDL_PERFTOLOG')) {
10383 $perf = get_performance_info();
10384 error_log("PERF: " . $perf['txt']);
10386 if (defined('MDL_PERFINC')) {
10387 $inc = get_included_files();
10388 $ts = 0;
10389 foreach($inc as $f) {
10390 if (preg_match(':^/:', $f)) {
10391 $fs = filesize($f);
10392 $ts += $fs;
10393 $hfs = display_size($fs);
10394 error_log(substr($f,strlen($CFG->dirroot)) . " size: $fs ($hfs)"
10395 , NULL, NULL, 0);
10396 } else {
10397 error_log($f , NULL, NULL, 0);
10400 if ($ts > 0 ) {
10401 $hts = display_size($ts);
10402 error_log("Total size of files included: $ts ($hts)");
10409 * If new messages are waiting for the current user, then insert
10410 * JavaScript to pop up the messaging window into the page
10412 * @global moodle_page $PAGE
10413 * @return void
10415 function message_popup_window() {
10416 global $USER, $DB, $PAGE, $CFG, $SITE;
10418 if (!$PAGE->get_popup_notification_allowed() || empty($CFG->messaging)) {
10419 return;
10422 if (!isloggedin() || isguestuser()) {
10423 return;
10426 if (!isset($USER->message_lastpopup)) {
10427 $USER->message_lastpopup = 0;
10428 } else if ($USER->message_lastpopup > (time()-120)) {
10429 //dont run the query to check whether to display a popup if its been run in the last 2 minutes
10430 return;
10433 //a quick query to check whether the user has new messages
10434 $messagecount = $DB->count_records('message', array('useridto' => $USER->id));
10435 if ($messagecount<1) {
10436 return;
10439 //got unread messages so now do another query that joins with the user table
10440 $messagesql = "SELECT m.id, m.smallmessage, m.fullmessageformat, m.notification, u.firstname, u.lastname
10441 FROM {message} m
10442 JOIN {message_working} mw ON m.id=mw.unreadmessageid
10443 JOIN {message_processors} p ON mw.processorid=p.id
10444 JOIN {user} u ON m.useridfrom=u.id
10445 WHERE m.useridto = :userid
10446 AND p.name='popup'";
10448 //if the user was last notified over an hour ago we can renotify them of old messages
10449 //so don't worry about when the new message was sent
10450 $lastnotifiedlongago = $USER->message_lastpopup < (time()-3600);
10451 if (!$lastnotifiedlongago) {
10452 $messagesql .= 'AND m.timecreated > :lastpopuptime';
10455 $message_users = $DB->get_records_sql($messagesql, array('userid'=>$USER->id, 'lastpopuptime'=>$USER->message_lastpopup));
10457 //if we have new messages to notify the user about
10458 if (!empty($message_users)) {
10460 $strmessages = '';
10461 if (count($message_users)>1) {
10462 $strmessages = get_string('unreadnewmessages', 'message', count($message_users));
10463 } else {
10464 $message_users = reset($message_users);
10466 //show who the message is from if its not a notification
10467 if (!$message_users->notification) {
10468 $strmessages = get_string('unreadnewmessage', 'message', fullname($message_users) );
10471 //try to display the small version of the message
10472 $smallmessage = null;
10473 if (!empty($message_users->smallmessage)) {
10474 //display the first 200 chars of the message in the popup
10475 $smallmessage = null;
10476 if (textlib::strlen($message_users->smallmessage) > 200) {
10477 $smallmessage = textlib::substr($message_users->smallmessage,0,200).'...';
10478 } else {
10479 $smallmessage = $message_users->smallmessage;
10482 //prevent html symbols being displayed
10483 if ($message_users->fullmessageformat == FORMAT_HTML) {
10484 $smallmessage = html_to_text($smallmessage);
10485 } else {
10486 $smallmessage = s($smallmessage);
10488 } else if ($message_users->notification) {
10489 //its a notification with no smallmessage so just say they have a notification
10490 $smallmessage = get_string('unreadnewnotification', 'message');
10492 if (!empty($smallmessage)) {
10493 $strmessages .= '<div id="usermessage">'.s($smallmessage).'</div>';
10497 $strgomessage = get_string('gotomessages', 'message');
10498 $strstaymessage = get_string('ignore','admin');
10500 $url = $CFG->wwwroot.'/message/index.php';
10501 $content = html_writer::start_tag('div', array('id'=>'newmessageoverlay','class'=>'mdl-align')).
10502 html_writer::start_tag('div', array('id'=>'newmessagetext')).
10503 $strmessages.
10504 html_writer::end_tag('div').
10506 html_writer::start_tag('div', array('id'=>'newmessagelinks')).
10507 html_writer::link($url, $strgomessage, array('id'=>'notificationyes')).'&nbsp;&nbsp;&nbsp;'.
10508 html_writer::link('', $strstaymessage, array('id'=>'notificationno')).
10509 html_writer::end_tag('div');
10510 html_writer::end_tag('div');
10512 $PAGE->requires->js_init_call('M.core_message.init_notification', array('', $content, $url));
10514 $USER->message_lastpopup = time();
10519 * Used to make sure that $min <= $value <= $max
10521 * Make sure that value is between min, and max
10523 * @param int $min The minimum value
10524 * @param int $value The value to check
10525 * @param int $max The maximum value
10527 function bounded_number($min, $value, $max) {
10528 if($value < $min) {
10529 return $min;
10531 if($value > $max) {
10532 return $max;
10534 return $value;
10538 * Check if there is a nested array within the passed array
10540 * @param array $array
10541 * @return bool true if there is a nested array false otherwise
10543 function array_is_nested($array) {
10544 foreach ($array as $value) {
10545 if (is_array($value)) {
10546 return true;
10549 return false;
10553 * get_performance_info() pairs up with init_performance_info()
10554 * loaded in setup.php. Returns an array with 'html' and 'txt'
10555 * values ready for use, and each of the individual stats provided
10556 * separately as well.
10558 * @global object
10559 * @global object
10560 * @global object
10561 * @return array
10563 function get_performance_info() {
10564 global $CFG, $PERF, $DB, $PAGE;
10566 $info = array();
10567 $info['html'] = ''; // holds userfriendly HTML representation
10568 $info['txt'] = me() . ' '; // holds log-friendly representation
10570 $info['realtime'] = microtime_diff($PERF->starttime, microtime());
10572 $info['html'] .= '<span class="timeused">'.$info['realtime'].' secs</span> ';
10573 $info['txt'] .= 'time: '.$info['realtime'].'s ';
10575 if (function_exists('memory_get_usage')) {
10576 $info['memory_total'] = memory_get_usage();
10577 $info['memory_growth'] = memory_get_usage() - $PERF->startmemory;
10578 $info['html'] .= '<span class="memoryused">RAM: '.display_size($info['memory_total']).'</span> ';
10579 $info['txt'] .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '.$info['memory_growth'].'B ('.display_size($info['memory_growth']).') ';
10582 if (function_exists('memory_get_peak_usage')) {
10583 $info['memory_peak'] = memory_get_peak_usage();
10584 $info['html'] .= '<span class="memoryused">RAM peak: '.display_size($info['memory_peak']).'</span> ';
10585 $info['txt'] .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') ';
10588 $inc = get_included_files();
10589 //error_log(print_r($inc,1));
10590 $info['includecount'] = count($inc);
10591 $info['html'] .= '<span class="included">Included '.$info['includecount'].' files</span> ';
10592 $info['txt'] .= 'includecount: '.$info['includecount'].' ';
10594 if (!empty($CFG->early_install_lang)) {
10595 // We can not track more performance before installation, sorry.
10596 return $info;
10599 $filtermanager = filter_manager::instance();
10600 if (method_exists($filtermanager, 'get_performance_summary')) {
10601 list($filterinfo, $nicenames) = $filtermanager->get_performance_summary();
10602 $info = array_merge($filterinfo, $info);
10603 foreach ($filterinfo as $key => $value) {
10604 $info['html'] .= "<span class='$key'>$nicenames[$key]: $value </span> ";
10605 $info['txt'] .= "$key: $value ";
10609 $stringmanager = get_string_manager();
10610 if (method_exists($stringmanager, 'get_performance_summary')) {
10611 list($filterinfo, $nicenames) = $stringmanager->get_performance_summary();
10612 $info = array_merge($filterinfo, $info);
10613 foreach ($filterinfo as $key => $value) {
10614 $info['html'] .= "<span class='$key'>$nicenames[$key]: $value </span> ";
10615 $info['txt'] .= "$key: $value ";
10619 $jsmodules = $PAGE->requires->get_loaded_modules();
10620 if ($jsmodules) {
10621 $yuicount = 0;
10622 $othercount = 0;
10623 $details = '';
10624 foreach ($jsmodules as $module => $backtraces) {
10625 if (strpos($module, 'yui') === 0) {
10626 $yuicount += 1;
10627 } else {
10628 $othercount += 1;
10630 if (!empty($CFG->yuimoduledebug)) {
10631 // hidden feature for developers working on YUI module infrastructure
10632 $details .= "<div class='yui-module'><p>$module</p>";
10633 foreach ($backtraces as $backtrace) {
10634 $details .= "<div class='backtrace'>$backtrace</div>";
10636 $details .= '</div>';
10639 $info['html'] .= "<span class='includedyuimodules'>Included YUI modules: $yuicount</span> ";
10640 $info['txt'] .= "includedyuimodules: $yuicount ";
10641 $info['html'] .= "<span class='includedjsmodules'>Other JavaScript modules: $othercount</span> ";
10642 $info['txt'] .= "includedjsmodules: $othercount ";
10643 if ($details) {
10644 $info['html'] .= '<div id="yui-module-debug" class="notifytiny">'.$details.'</div>';
10648 if (!empty($PERF->logwrites)) {
10649 $info['logwrites'] = $PERF->logwrites;
10650 $info['html'] .= '<span class="logwrites">Log DB writes '.$info['logwrites'].'</span> ';
10651 $info['txt'] .= 'logwrites: '.$info['logwrites'].' ';
10654 $info['dbqueries'] = $DB->perf_get_reads().'/'.($DB->perf_get_writes() - $PERF->logwrites);
10655 $info['html'] .= '<span class="dbqueries">DB reads/writes: '.$info['dbqueries'].'</span> ';
10656 $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' ';
10658 if (function_exists('posix_times')) {
10659 $ptimes = posix_times();
10660 if (is_array($ptimes)) {
10661 foreach ($ptimes as $key => $val) {
10662 $info[$key] = $ptimes[$key] - $PERF->startposixtimes[$key];
10664 $info['html'] .= "<span class=\"posixtimes\">ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</span> ";
10665 $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] ";
10669 // Grab the load average for the last minute
10670 // /proc will only work under some linux configurations
10671 // while uptime is there under MacOSX/Darwin and other unices
10672 if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) {
10673 list($server_load) = explode(' ', $loadavg[0]);
10674 unset($loadavg);
10675 } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) {
10676 if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) {
10677 $server_load = $matches[1];
10678 } else {
10679 trigger_error('Could not parse uptime output!');
10682 if (!empty($server_load)) {
10683 $info['serverload'] = $server_load;
10684 $info['html'] .= '<span class="serverload">Load average: '.$info['serverload'].'</span> ';
10685 $info['txt'] .= "serverload: {$info['serverload']} ";
10688 // Display size of session if session started
10689 if (session_id()) {
10690 $info['sessionsize'] = display_size(strlen(session_encode()));
10691 $info['html'] .= '<span class="sessionsize">Session: ' . $info['sessionsize'] . '</span> ';
10692 $info['txt'] .= "Session: {$info['sessionsize']} ";
10695 if ($stats = cache_helper::get_stats()) {
10696 $html = '<span class="cachesused">';
10697 $html .= '<span class="cache-stats-heading">Caches used (hits/misses/sets)</span>';
10698 $text = 'Caches used (hits/misses/sets): ';
10699 $hits = 0;
10700 $misses = 0;
10701 $sets = 0;
10702 foreach ($stats as $definition => $stores) {
10703 $html .= '<span class="cache-definition-stats">';
10704 $html .= '<span class="cache-definition-stats-heading">'.$definition.'</span>';
10705 $text .= "$definition {";
10706 foreach ($stores as $store => $data) {
10707 $hits += $data['hits'];
10708 $misses += $data['misses'];
10709 $sets += $data['sets'];
10710 if ($data['hits'] == 0 and $data['misses'] > 0) {
10711 $cachestoreclass = 'nohits';
10712 } else if ($data['hits'] < $data['misses']) {
10713 $cachestoreclass = 'lowhits';
10714 } else {
10715 $cachestoreclass = 'hihits';
10717 $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
10718 $html .= "<span class=\"cache-store-stats $cachestoreclass\">$store: $data[hits] / $data[misses] / $data[sets]</span>";
10720 $html .= '</span>';
10721 $text .= '} ';
10723 $html .= "<span class='cache-total-stats'>Total: $hits / $misses / $sets</span>";
10724 $html .= '</span> ';
10725 $info['cachesused'] = "$hits / $misses / $sets";
10726 $info['html'] .= $html;
10727 $info['txt'] .= $text.'. ';
10728 } else {
10729 $info['cachesused'] = '0 / 0 / 0';
10730 $info['html'] .= '<span class="cachesused">Caches used (hits/misses/sets): 0/0/0</span>';
10731 $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 ';
10734 $info['html'] = '<div class="performanceinfo siteinfo">'.$info['html'].'</div>';
10735 return $info;
10739 * @todo Document this function linux people
10741 function apd_get_profiling() {
10742 return shell_exec('pprofp -u ' . ini_get('apd.dumpdir') . '/pprof.' . getmypid() . '.*');
10746 * Delete directory or only its content
10748 * @param string $dir directory path
10749 * @param bool $content_only
10750 * @return bool success, true also if dir does not exist
10752 function remove_dir($dir, $content_only=false) {
10753 if (!file_exists($dir)) {
10754 // nothing to do
10755 return true;
10757 if (!$handle = opendir($dir)) {
10758 return false;
10760 $result = true;
10761 while (false!==($item = readdir($handle))) {
10762 if($item != '.' && $item != '..') {
10763 if(is_dir($dir.'/'.$item)) {
10764 $result = remove_dir($dir.'/'.$item) && $result;
10765 }else{
10766 $result = unlink($dir.'/'.$item) && $result;
10770 closedir($handle);
10771 if ($content_only) {
10772 clearstatcache(); // make sure file stat cache is properly invalidated
10773 return $result;
10775 $result = rmdir($dir); // if anything left the result will be false, no need for && $result
10776 clearstatcache(); // make sure file stat cache is properly invalidated
10777 return $result;
10781 * Detect if an object or a class contains a given property
10782 * will take an actual object or the name of a class
10784 * @param mix $obj Name of class or real object to test
10785 * @param string $property name of property to find
10786 * @return bool true if property exists
10788 function object_property_exists( $obj, $property ) {
10789 if (is_string( $obj )) {
10790 $properties = get_class_vars( $obj );
10792 else {
10793 $properties = get_object_vars( $obj );
10795 return array_key_exists( $property, $properties );
10799 * Converts an object into an associative array
10801 * This function converts an object into an associative array by iterating
10802 * over its public properties. Because this function uses the foreach
10803 * construct, Iterators are respected. It works recursively on arrays of objects.
10804 * Arrays and simple values are returned as is.
10806 * If class has magic properties, it can implement IteratorAggregate
10807 * and return all available properties in getIterator()
10809 * @param mixed $var
10810 * @return array
10812 function convert_to_array($var) {
10813 $result = array();
10815 // loop over elements/properties
10816 foreach ($var as $key => $value) {
10817 // recursively convert objects
10818 if (is_object($value) || is_array($value)) {
10819 $result[$key] = convert_to_array($value);
10820 } else {
10821 // simple values are untouched
10822 $result[$key] = $value;
10825 return $result;
10829 * Detect a custom script replacement in the data directory that will
10830 * replace an existing moodle script
10832 * @return string|bool full path name if a custom script exists, false if no custom script exists
10834 function custom_script_path() {
10835 global $CFG, $SCRIPT;
10837 if ($SCRIPT === null) {
10838 // Probably some weird external script
10839 return false;
10842 $scriptpath = $CFG->customscripts . $SCRIPT;
10844 // check the custom script exists
10845 if (file_exists($scriptpath) and is_file($scriptpath)) {
10846 return $scriptpath;
10847 } else {
10848 return false;
10853 * Returns whether or not the user object is a remote MNET user. This function
10854 * is in moodlelib because it does not rely on loading any of the MNET code.
10856 * @global object
10857 * @param object $user A valid user object
10858 * @return bool True if the user is from a remote Moodle.
10860 function is_mnet_remote_user($user) {
10861 global $CFG;
10863 if (!isset($CFG->mnet_localhost_id)) {
10864 include_once $CFG->dirroot . '/mnet/lib.php';
10865 $env = new mnet_environment();
10866 $env->init();
10867 unset($env);
10870 return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id);
10874 * This function will search for browser prefereed languages, setting Moodle
10875 * to use the best one available if $SESSION->lang is undefined
10877 * @global object
10878 * @global object
10879 * @global object
10881 function setup_lang_from_browser() {
10883 global $CFG, $SESSION, $USER;
10885 if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) {
10886 // Lang is defined in session or user profile, nothing to do
10887 return;
10890 if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do
10891 return;
10894 /// Extract and clean langs from headers
10895 $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
10896 $rawlangs = str_replace('-', '_', $rawlangs); // we are using underscores
10897 $rawlangs = explode(',', $rawlangs); // Convert to array
10898 $langs = array();
10900 $order = 1.0;
10901 foreach ($rawlangs as $lang) {
10902 if (strpos($lang, ';') === false) {
10903 $langs[(string)$order] = $lang;
10904 $order = $order-0.01;
10905 } else {
10906 $parts = explode(';', $lang);
10907 $pos = strpos($parts[1], '=');
10908 $langs[substr($parts[1], $pos+1)] = $parts[0];
10911 krsort($langs, SORT_NUMERIC);
10913 /// Look for such langs under standard locations
10914 foreach ($langs as $lang) {
10915 $lang = strtolower(clean_param($lang, PARAM_SAFEDIR)); // clean it properly for include
10916 if (get_string_manager()->translation_exists($lang, false)) {
10917 $SESSION->lang = $lang; /// Lang exists, set it in session
10918 break; /// We have finished. Go out
10921 return;
10925 * check if $url matches anything in proxybypass list
10927 * any errors just result in the proxy being used (least bad)
10929 * @global object
10930 * @param string $url url to check
10931 * @return boolean true if we should bypass the proxy
10933 function is_proxybypass( $url ) {
10934 global $CFG;
10936 // sanity check
10937 if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) {
10938 return false;
10941 // get the host part out of the url
10942 if (!$host = parse_url( $url, PHP_URL_HOST )) {
10943 return false;
10946 // get the possible bypass hosts into an array
10947 $matches = explode( ',', $CFG->proxybypass );
10949 // check for a match
10950 // (IPs need to match the left hand side and hosts the right of the url,
10951 // but we can recklessly check both as there can't be a false +ve)
10952 $bypass = false;
10953 foreach ($matches as $match) {
10954 $match = trim($match);
10956 // try for IP match (Left side)
10957 $lhs = substr($host,0,strlen($match));
10958 if (strcasecmp($match,$lhs)==0) {
10959 return true;
10962 // try for host match (Right side)
10963 $rhs = substr($host,-strlen($match));
10964 if (strcasecmp($match,$rhs)==0) {
10965 return true;
10969 // nothing matched.
10970 return false;
10974 ////////////////////////////////////////////////////////////////////////////////
10977 * Check if the passed navigation is of the new style
10979 * @param mixed $navigation
10980 * @return bool true for yes false for no
10982 function is_newnav($navigation) {
10983 if (is_array($navigation) && !empty($navigation['newnav'])) {
10984 return true;
10985 } else {
10986 return false;
10991 * Checks whether the given variable name is defined as a variable within the given object.
10993 * This will NOT work with stdClass objects, which have no class variables.
10995 * @param string $var The variable name
10996 * @param object $object The object to check
10997 * @return boolean
10999 function in_object_vars($var, $object) {
11000 $class_vars = get_class_vars(get_class($object));
11001 $class_vars = array_keys($class_vars);
11002 return in_array($var, $class_vars);
11006 * Returns an array without repeated objects.
11007 * This function is similar to array_unique, but for arrays that have objects as values
11009 * @param array $array
11010 * @param bool $keep_key_assoc
11011 * @return array
11013 function object_array_unique($array, $keep_key_assoc = true) {
11014 $duplicate_keys = array();
11015 $tmp = array();
11017 foreach ($array as $key=>$val) {
11018 // convert objects to arrays, in_array() does not support objects
11019 if (is_object($val)) {
11020 $val = (array)$val;
11023 if (!in_array($val, $tmp)) {
11024 $tmp[] = $val;
11025 } else {
11026 $duplicate_keys[] = $key;
11030 foreach ($duplicate_keys as $key) {
11031 unset($array[$key]);
11034 return $keep_key_assoc ? $array : array_values($array);
11038 * Is a userid the primary administrator?
11040 * @param int $userid int id of user to check
11041 * @return boolean
11043 function is_primary_admin($userid){
11044 $primaryadmin = get_admin();
11046 if($userid == $primaryadmin->id){
11047 return true;
11048 }else{
11049 return false;
11054 * Returns the site identifier
11056 * @global object
11057 * @return string $CFG->siteidentifier, first making sure it is properly initialised.
11059 function get_site_identifier() {
11060 global $CFG;
11061 // Check to see if it is missing. If so, initialise it.
11062 if (empty($CFG->siteidentifier)) {
11063 set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']);
11065 // Return it.
11066 return $CFG->siteidentifier;
11070 * Check whether the given password has no more than the specified
11071 * number of consecutive identical characters.
11073 * @param string $password password to be checked against the password policy
11074 * @param integer $maxchars maximum number of consecutive identical characters
11076 function check_consecutive_identical_characters($password, $maxchars) {
11078 if ($maxchars < 1) {
11079 return true; // 0 is to disable this check
11081 if (strlen($password) <= $maxchars) {
11082 return true; // too short to fail this test
11085 $previouschar = '';
11086 $consecutivecount = 1;
11087 foreach (str_split($password) as $char) {
11088 if ($char != $previouschar) {
11089 $consecutivecount = 1;
11091 else {
11092 $consecutivecount++;
11093 if ($consecutivecount > $maxchars) {
11094 return false; // check failed already
11098 $previouschar = $char;
11101 return true;
11105 * helper function to do partial function binding
11106 * so we can use it for preg_replace_callback, for example
11107 * this works with php functions, user functions, static methods and class methods
11108 * it returns you a callback that you can pass on like so:
11110 * $callback = partial('somefunction', $arg1, $arg2);
11111 * or
11112 * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
11113 * or even
11114 * $obj = new someclass();
11115 * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2);
11117 * and then the arguments that are passed through at calltime are appended to the argument list.
11119 * @param mixed $function a php callback
11120 * $param mixed $arg1.. $argv arguments to partially bind with
11122 * @return callback
11124 function partial() {
11125 if (!class_exists('partial')) {
11126 class partial{
11127 var $values = array();
11128 var $func;
11130 function __construct($func, $args) {
11131 $this->values = $args;
11132 $this->func = $func;
11135 function method() {
11136 $args = func_get_args();
11137 return call_user_func_array($this->func, array_merge($this->values, $args));
11141 $args = func_get_args();
11142 $func = array_shift($args);
11143 $p = new partial($func, $args);
11144 return array($p, 'method');
11148 * helper function to load up and initialise the mnet environment
11149 * this must be called before you use mnet functions.
11151 * @return mnet_environment the equivalent of old $MNET global
11153 function get_mnet_environment() {
11154 global $CFG;
11155 require_once($CFG->dirroot . '/mnet/lib.php');
11156 static $instance = null;
11157 if (empty($instance)) {
11158 $instance = new mnet_environment();
11159 $instance->init();
11161 return $instance;
11165 * during xmlrpc server code execution, any code wishing to access
11166 * information about the remote peer must use this to get it.
11168 * @return mnet_remote_client the equivalent of old $MNET_REMOTE_CLIENT global
11170 function get_mnet_remote_client() {
11171 if (!defined('MNET_SERVER')) {
11172 debugging(get_string('notinxmlrpcserver', 'mnet'));
11173 return false;
11175 global $MNET_REMOTE_CLIENT;
11176 if (isset($MNET_REMOTE_CLIENT)) {
11177 return $MNET_REMOTE_CLIENT;
11179 return false;
11183 * during the xmlrpc server code execution, this will be called
11184 * to setup the object returned by {@see get_mnet_remote_client}
11186 * @param mnet_remote_client $client the client to set up
11188 function set_mnet_remote_client($client) {
11189 if (!defined('MNET_SERVER')) {
11190 throw new moodle_exception('notinxmlrpcserver', 'mnet');
11192 global $MNET_REMOTE_CLIENT;
11193 $MNET_REMOTE_CLIENT = $client;
11197 * return the jump url for a given remote user
11198 * this is used for rewriting forum post links in emails, etc
11200 * @param stdclass $user the user to get the idp url for
11202 function mnet_get_idp_jump_url($user) {
11203 global $CFG;
11205 static $mnetjumps = array();
11206 if (!array_key_exists($user->mnethostid, $mnetjumps)) {
11207 $idp = mnet_get_peer_host($user->mnethostid);
11208 $idpjumppath = mnet_get_app_jumppath($idp->applicationid);
11209 $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl=';
11211 return $mnetjumps[$user->mnethostid];
11215 * Gets the homepage to use for the current user
11217 * @return int One of HOMEPAGE_*
11219 function get_home_page() {
11220 global $CFG;
11222 if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) {
11223 if ($CFG->defaulthomepage == HOMEPAGE_MY) {
11224 return HOMEPAGE_MY;
11225 } else {
11226 return (int)get_user_preferences('user_home_page_preference', HOMEPAGE_MY);
11229 return HOMEPAGE_SITE;
11233 * Gets the name of a course to be displayed when showing a list of courses.
11234 * By default this is just $course->fullname but user can configure it. The
11235 * result of this function should be passed through print_string.
11236 * @param stdClass|course_in_list $course Moodle course object
11237 * @return string Display name of course (either fullname or short + fullname)
11239 function get_course_display_name_for_list($course) {
11240 global $CFG;
11241 if (!empty($CFG->courselistshortnames)) {
11242 if (!($course instanceof stdClass)) {
11243 $course = (object)convert_to_array($course);
11245 return get_string('courseextendednamedisplay', '', $course);
11246 } else {
11247 return $course->fullname;
11252 * The lang_string class
11254 * This special class is used to create an object representation of a string request.
11255 * It is special because processing doesn't occur until the object is first used.
11256 * The class was created especially to aid performance in areas where strings were
11257 * required to be generated but were not necessarily used.
11258 * As an example the admin tree when generated uses over 1500 strings, of which
11259 * normally only 1/3 are ever actually printed at any time.
11260 * The performance advantage is achieved by not actually processing strings that
11261 * arn't being used, as such reducing the processing required for the page.
11263 * How to use the lang_string class?
11264 * There are two methods of using the lang_string class, first through the
11265 * forth argument of the get_string function, and secondly directly.
11266 * The following are examples of both.
11267 * 1. Through get_string calls e.g.
11268 * $string = get_string($identifier, $component, $a, true);
11269 * $string = get_string('yes', 'moodle', null, true);
11270 * 2. Direct instantiation
11271 * $string = new lang_string($identifier, $component, $a, $lang);
11272 * $string = new lang_string('yes');
11274 * How do I use a lang_string object?
11275 * The lang_string object makes use of a magic __toString method so that you
11276 * are able to use the object exactly as you would use a string in most cases.
11277 * This means you are able to collect it into a variable and then directly
11278 * echo it, or concatenate it into another string, or similar.
11279 * The other thing you can do is manually get the string by calling the
11280 * lang_strings out method e.g.
11281 * $string = new lang_string('yes');
11282 * $string->out();
11283 * Also worth noting is that the out method can take one argument, $lang which
11284 * allows the developer to change the language on the fly.
11286 * When should I use a lang_string object?
11287 * The lang_string object is designed to be used in any situation where a
11288 * string may not be needed, but needs to be generated.
11289 * The admin tree is a good example of where lang_string objects should be
11290 * used.
11291 * A more practical example would be any class that requries strings that may
11292 * not be printed (after all classes get renderer by renderers and who knows
11293 * what they will do ;))
11295 * When should I not use a lang_string object?
11296 * Don't use lang_strings when you are going to use a string immediately.
11297 * There is no need as it will be processed immediately and there will be no
11298 * advantage, and in fact perhaps a negative hit as a class has to be
11299 * instantiated for a lang_string object, however get_string won't require
11300 * that.
11302 * Limitations:
11303 * 1. You cannot use a lang_string object as an array offset. Doing so will
11304 * result in PHP throwing an error. (You can use it as an object property!)
11306 * @package core
11307 * @category string
11308 * @copyright 2011 Sam Hemelryk
11309 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
11311 class lang_string {
11313 /** @var string The strings identifier */
11314 protected $identifier;
11315 /** @var string The strings component. Default '' */
11316 protected $component = '';
11317 /** @var array|stdClass Any arguments required for the string. Default null */
11318 protected $a = null;
11319 /** @var string The language to use when processing the string. Default null */
11320 protected $lang = null;
11322 /** @var string The processed string (once processed) */
11323 protected $string = null;
11326 * A special boolean. If set to true then the object has been woken up and
11327 * cannot be regenerated. If this is set then $this->string MUST be used.
11328 * @var bool
11330 protected $forcedstring = false;
11333 * Constructs a lang_string object
11335 * This function should do as little processing as possible to ensure the best
11336 * performance for strings that won't be used.
11338 * @param string $identifier The strings identifier
11339 * @param string $component The strings component
11340 * @param stdClass|array $a Any arguments the string requires
11341 * @param string $lang The language to use when processing the string.
11343 public function __construct($identifier, $component = '', $a = null, $lang = null) {
11344 if (empty($component)) {
11345 $component = 'moodle';
11348 $this->identifier = $identifier;
11349 $this->component = $component;
11350 $this->lang = $lang;
11352 // We MUST duplicate $a to ensure that it if it changes by reference those
11353 // changes are not carried across.
11354 // To do this we always ensure $a or its properties/values are strings
11355 // and that any properties/values that arn't convertable are forgotten.
11356 if (!empty($a)) {
11357 if (is_scalar($a)) {
11358 $this->a = $a;
11359 } else if ($a instanceof lang_string) {
11360 $this->a = $a->out();
11361 } else if (is_object($a) or is_array($a)) {
11362 $a = (array)$a;
11363 $this->a = array();
11364 foreach ($a as $key => $value) {
11365 // Make sure conversion errors don't get displayed (results in '')
11366 if (is_array($value)) {
11367 $this->a[$key] = '';
11368 } else if (is_object($value)) {
11369 if (method_exists($value, '__toString')) {
11370 $this->a[$key] = $value->__toString();
11371 } else {
11372 $this->a[$key] = '';
11374 } else {
11375 $this->a[$key] = (string)$value;
11381 if (debugging(false, DEBUG_DEVELOPER)) {
11382 if (clean_param($this->identifier, PARAM_STRINGID) == '') {
11383 throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition');
11385 if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') {
11386 throw new coding_exception('Invalid string compontent. Please check your string definition');
11388 if (!get_string_manager()->string_exists($this->identifier, $this->component)) {
11389 debugging('String does not exist. Please check your string definition for '.$this->identifier.'/'.$this->component, DEBUG_DEVELOPER);
11395 * Processes the string.
11397 * This function actually processes the string, stores it in the string property
11398 * and then returns it.
11399 * You will notice that this function is VERY similar to the get_string method.
11400 * That is because it is pretty much doing the same thing.
11401 * However as this function is an upgrade it isn't as tolerant to backwards
11402 * compatability.
11404 * @return string
11406 protected function get_string() {
11407 global $CFG;
11409 // Check if we need to process the string
11410 if ($this->string === null) {
11411 // Check the quality of the identifier.
11412 if (clean_param($this->identifier, PARAM_STRINGID) == '') {
11413 throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition');
11416 // Process the string
11417 $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang);
11418 // Debugging feature lets you display string identifier and component
11419 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
11420 $this->string .= ' {' . $this->identifier . '/' . $this->component . '}';
11423 // Return the string
11424 return $this->string;
11428 * Returns the string
11430 * @param string $lang The langauge to use when processing the string
11431 * @return string
11433 public function out($lang = null) {
11434 if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) {
11435 if ($this->forcedstring) {
11436 debugging('lang_string objects that have been serialised and unserialised cannot be printed in another language. ('.$this->lang.' used)', DEBUG_DEVELOPER);
11437 return $this->get_string();
11439 $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang);
11440 return $translatedstring->out();
11442 return $this->get_string();
11446 * Magic __toString method for printing a string
11448 * @return string
11450 public function __toString() {
11451 return $this->get_string();
11455 * Magic __set_state method used for var_export
11457 * @return string
11459 public function __set_state() {
11460 return $this->get_string();
11464 * Prepares the lang_string for sleep and stores only the forcedstring and
11465 * string properties... the string cannot be regenerated so we need to ensure
11466 * it is generated for this.
11468 * @return string
11470 public function __sleep() {
11471 $this->get_string();
11472 $this->forcedstring = true;
11473 return array('forcedstring', 'string', 'lang');