Merge branch 'wip-mdl-41843-m25' of git://github.com/rajeshtaneja/moodle into MOODLE_...
[moodle.git] / lib / moodlelib.php
blob04c0eb1ed5290c4e7c537e2fdd87c08547363ba0
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 (!is_valid_plugin_name($param)) {
916 return '';
918 return $param;
920 case PARAM_SAFEDIR: // Remove everything not a-zA-Z0-9_-
921 return preg_replace('/[^a-zA-Z0-9_-]/i', '', $param);
923 case PARAM_SAFEPATH: // Remove everything not a-zA-Z0-9/_-
924 return preg_replace('/[^a-zA-Z0-9\/_-]/i', '', $param);
926 case PARAM_FILE: // Strip all suspicious characters from filename
927 $param = fix_utf8($param);
928 $param = preg_replace('~[[:cntrl:]]|[&<>"`\|\':\\\\/]~u', '', $param);
929 if ($param === '.' || $param === '..') {
930 $param = '';
932 return $param;
934 case PARAM_PATH: // Strip all suspicious characters from file path
935 $param = fix_utf8($param);
936 $param = str_replace('\\', '/', $param);
938 // Explode the path and clean each element using the PARAM_FILE rules.
939 $breadcrumb = explode('/', $param);
940 foreach ($breadcrumb as $key => $crumb) {
941 if ($crumb === '.' && $key === 0) {
942 // Special condition to allow for relative current path such as ./currentdirfile.txt.
943 } else {
944 $crumb = clean_param($crumb, PARAM_FILE);
946 $breadcrumb[$key] = $crumb;
948 $param = implode('/', $breadcrumb);
950 // Remove multiple current path (./././) and multiple slashes (///).
951 $param = preg_replace('~//+~', '/', $param);
952 $param = preg_replace('~/(\./)+~', '/', $param);
953 return $param;
955 case PARAM_HOST: // allow FQDN or IPv4 dotted quad
956 $param = preg_replace('/[^\.\d\w-]/','', $param ); // only allowed chars
957 // match ipv4 dotted quad
958 if (preg_match('/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/',$param, $match)){
959 // confirm values are ok
960 if ( $match[0] > 255
961 || $match[1] > 255
962 || $match[3] > 255
963 || $match[4] > 255 ) {
964 // hmmm, what kind of dotted quad is this?
965 $param = '';
967 } elseif ( preg_match('/^[\w\d\.-]+$/', $param) // dots, hyphens, numbers
968 && !preg_match('/^[\.-]/', $param) // no leading dots/hyphens
969 && !preg_match('/[\.-]$/', $param) // no trailing dots/hyphens
971 // all is ok - $param is respected
972 } else {
973 // all is not ok...
974 $param='';
976 return $param;
978 case PARAM_URL: // allow safe ftp, http, mailto urls
979 $param = fix_utf8($param);
980 include_once($CFG->dirroot . '/lib/validateurlsyntax.php');
981 if (!empty($param) && validateUrlSyntax($param, 's?H?S?F?E?u-P-a?I?p?f?q?r?')) {
982 // all is ok, param is respected
983 } else {
984 $param =''; // not really ok
986 return $param;
988 case PARAM_LOCALURL: // allow http absolute, root relative and relative URLs within wwwroot
989 $param = clean_param($param, PARAM_URL);
990 if (!empty($param)) {
991 if (preg_match(':^/:', $param)) {
992 // root-relative, ok!
993 } elseif (preg_match('/^'.preg_quote($CFG->wwwroot, '/').'/i',$param)) {
994 // absolute, and matches our wwwroot
995 } else {
996 // relative - let's make sure there are no tricks
997 if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?')) {
998 // looks ok.
999 } else {
1000 $param = '';
1004 return $param;
1006 case PARAM_PEM:
1007 $param = trim($param);
1008 // PEM formatted strings may contain letters/numbers and the symbols
1009 // forward slash: /
1010 // plus sign: +
1011 // equal sign: =
1012 // , surrounded by BEGIN and END CERTIFICATE prefix and suffixes
1013 if (preg_match('/^-----BEGIN CERTIFICATE-----([\s\w\/\+=]+)-----END CERTIFICATE-----$/', trim($param), $matches)) {
1014 list($wholething, $body) = $matches;
1015 unset($wholething, $matches);
1016 $b64 = clean_param($body, PARAM_BASE64);
1017 if (!empty($b64)) {
1018 return "-----BEGIN CERTIFICATE-----\n$b64\n-----END CERTIFICATE-----\n";
1019 } else {
1020 return '';
1023 return '';
1025 case PARAM_BASE64:
1026 if (!empty($param)) {
1027 // PEM formatted strings may contain letters/numbers and the symbols
1028 // forward slash: /
1029 // plus sign: +
1030 // equal sign: =
1031 if (0 >= preg_match('/^([\s\w\/\+=]+)$/', trim($param))) {
1032 return '';
1034 $lines = preg_split('/[\s]+/', $param, -1, PREG_SPLIT_NO_EMPTY);
1035 // Each line of base64 encoded data must be 64 characters in
1036 // length, except for the last line which may be less than (or
1037 // equal to) 64 characters long.
1038 for ($i=0, $j=count($lines); $i < $j; $i++) {
1039 if ($i + 1 == $j) {
1040 if (64 < strlen($lines[$i])) {
1041 return '';
1043 continue;
1046 if (64 != strlen($lines[$i])) {
1047 return '';
1050 return implode("\n",$lines);
1051 } else {
1052 return '';
1055 case PARAM_TAG:
1056 $param = fix_utf8($param);
1057 // Please note it is not safe to use the tag name directly anywhere,
1058 // it must be processed with s(), urlencode() before embedding anywhere.
1059 // remove some nasties
1060 $param = preg_replace('~[[:cntrl:]]|[<>`]~u', '', $param);
1061 //convert many whitespace chars into one
1062 $param = preg_replace('/\s+/', ' ', $param);
1063 $param = textlib::substr(trim($param), 0, TAG_MAX_LENGTH);
1064 return $param;
1066 case PARAM_TAGLIST:
1067 $param = fix_utf8($param);
1068 $tags = explode(',', $param);
1069 $result = array();
1070 foreach ($tags as $tag) {
1071 $res = clean_param($tag, PARAM_TAG);
1072 if ($res !== '') {
1073 $result[] = $res;
1076 if ($result) {
1077 return implode(',', $result);
1078 } else {
1079 return '';
1082 case PARAM_CAPABILITY:
1083 if (get_capability_info($param)) {
1084 return $param;
1085 } else {
1086 return '';
1089 case PARAM_PERMISSION:
1090 $param = (int)$param;
1091 if (in_array($param, array(CAP_INHERIT, CAP_ALLOW, CAP_PREVENT, CAP_PROHIBIT))) {
1092 return $param;
1093 } else {
1094 return CAP_INHERIT;
1097 case PARAM_AUTH:
1098 $param = clean_param($param, PARAM_PLUGIN);
1099 if (empty($param)) {
1100 return '';
1101 } else if (exists_auth_plugin($param)) {
1102 return $param;
1103 } else {
1104 return '';
1107 case PARAM_LANG:
1108 $param = clean_param($param, PARAM_SAFEDIR);
1109 if (get_string_manager()->translation_exists($param)) {
1110 return $param;
1111 } else {
1112 return ''; // Specified language is not installed or param malformed
1115 case PARAM_THEME:
1116 $param = clean_param($param, PARAM_PLUGIN);
1117 if (empty($param)) {
1118 return '';
1119 } else if (file_exists("$CFG->dirroot/theme/$param/config.php")) {
1120 return $param;
1121 } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$param/config.php")) {
1122 return $param;
1123 } else {
1124 return ''; // Specified theme is not installed
1127 case PARAM_USERNAME:
1128 $param = fix_utf8($param);
1129 $param = str_replace(" " , "", $param);
1130 $param = textlib::strtolower($param); // Convert uppercase to lowercase MDL-16919
1131 if (empty($CFG->extendedusernamechars)) {
1132 // regular expression, eliminate all chars EXCEPT:
1133 // alphanum, dash (-), underscore (_), at sign (@) and period (.) characters.
1134 $param = preg_replace('/[^-\.@_a-z0-9]/', '', $param);
1136 return $param;
1138 case PARAM_EMAIL:
1139 $param = fix_utf8($param);
1140 if (validate_email($param)) {
1141 return $param;
1142 } else {
1143 return '';
1146 case PARAM_STRINGID:
1147 if (preg_match('|^[a-zA-Z][a-zA-Z0-9\.:/_-]*$|', $param)) {
1148 return $param;
1149 } else {
1150 return '';
1153 case PARAM_TIMEZONE: //can be int, float(with .5 or .0) or string seperated by '/' and can have '-_'
1154 $param = fix_utf8($param);
1155 $timezonepattern = '/^(([+-]?(0?[0-9](\.[5|0])?|1[0-3](\.0)?|1[0-2]\.5))|(99)|[[:alnum:]]+(\/?[[:alpha:]_-])+)$/';
1156 if (preg_match($timezonepattern, $param)) {
1157 return $param;
1158 } else {
1159 return '';
1162 default: // throw error, switched parameters in optional_param or another serious problem
1163 print_error("unknownparamtype", '', '', $type);
1168 * Makes sure the data is using valid utf8, invalid characters are discarded.
1170 * Note: this function is not intended for full objects with methods and private properties.
1172 * @param mixed $value
1173 * @return mixed with proper utf-8 encoding
1175 function fix_utf8($value) {
1176 if (is_null($value) or $value === '') {
1177 return $value;
1179 } else if (is_string($value)) {
1180 if ((string)(int)$value === $value) {
1181 // shortcut
1182 return $value;
1184 // No null bytes expected in our data, so let's remove it.
1185 $value = str_replace("\0", '', $value);
1187 // Lower error reporting because glibc throws bogus notices.
1188 $olderror = error_reporting();
1189 if ($olderror & E_NOTICE) {
1190 error_reporting($olderror ^ E_NOTICE);
1193 // Note: this duplicates min_fix_utf8() intentionally.
1194 static $buggyiconv = null;
1195 if ($buggyiconv === null) {
1196 $buggyiconv = (!function_exists('iconv') or iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€');
1199 if ($buggyiconv) {
1200 if (function_exists('mb_convert_encoding')) {
1201 $subst = mb_substitute_character();
1202 mb_substitute_character('');
1203 $result = mb_convert_encoding($value, 'utf-8', 'utf-8');
1204 mb_substitute_character($subst);
1206 } else {
1207 // Warn admins on admin/index.php page.
1208 $result = $value;
1211 } else {
1212 $result = iconv('UTF-8', 'UTF-8//IGNORE', $value);
1215 if ($olderror & E_NOTICE) {
1216 error_reporting($olderror);
1219 return $result;
1221 } else if (is_array($value)) {
1222 foreach ($value as $k=>$v) {
1223 $value[$k] = fix_utf8($v);
1225 return $value;
1227 } else if (is_object($value)) {
1228 $value = clone($value); // do not modify original
1229 foreach ($value as $k=>$v) {
1230 $value->$k = fix_utf8($v);
1232 return $value;
1234 } else {
1235 // this is some other type, no utf-8 here
1236 return $value;
1241 * Return true if given value is integer or string with integer value
1243 * @param mixed $value String or Int
1244 * @return bool true if number, false if not
1246 function is_number($value) {
1247 if (is_int($value)) {
1248 return true;
1249 } else if (is_string($value)) {
1250 return ((string)(int)$value) === $value;
1251 } else {
1252 return false;
1257 * Returns host part from url
1258 * @param string $url full url
1259 * @return string host, null if not found
1261 function get_host_from_url($url) {
1262 preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches);
1263 if ($matches) {
1264 return $matches[1];
1266 return null;
1270 * Tests whether anything was returned by text editor
1272 * This function is useful for testing whether something you got back from
1273 * the HTML editor actually contains anything. Sometimes the HTML editor
1274 * appear to be empty, but actually you get back a <br> tag or something.
1276 * @param string $string a string containing HTML.
1277 * @return boolean does the string contain any actual content - that is text,
1278 * images, objects, etc.
1280 function html_is_blank($string) {
1281 return trim(strip_tags($string, '<img><object><applet><input><select><textarea><hr>')) == '';
1285 * Set a key in global configuration
1287 * Set a key/value pair in both this session's {@link $CFG} global variable
1288 * and in the 'config' database table for future sessions.
1290 * Can also be used to update keys for plugin-scoped configs in config_plugin table.
1291 * In that case it doesn't affect $CFG.
1293 * A NULL value will delete the entry.
1295 * @global object
1296 * @global object
1297 * @param string $name the key to set
1298 * @param string $value the value to set (without magic quotes)
1299 * @param string $plugin (optional) the plugin scope, default NULL
1300 * @return bool true or exception
1302 function set_config($name, $value, $plugin=NULL) {
1303 global $CFG, $DB;
1305 if (empty($plugin)) {
1306 if (!array_key_exists($name, $CFG->config_php_settings)) {
1307 // So it's defined for this invocation at least
1308 if (is_null($value)) {
1309 unset($CFG->$name);
1310 } else {
1311 $CFG->$name = (string)$value; // settings from db are always strings
1315 if ($DB->get_field('config', 'name', array('name'=>$name))) {
1316 if ($value === null) {
1317 $DB->delete_records('config', array('name'=>$name));
1318 } else {
1319 $DB->set_field('config', 'value', $value, array('name'=>$name));
1321 } else {
1322 if ($value !== null) {
1323 $config = new stdClass();
1324 $config->name = $name;
1325 $config->value = $value;
1326 $DB->insert_record('config', $config, false);
1329 if ($name === 'siteidentifier') {
1330 cache_helper::update_site_identifier($value);
1332 cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1333 } else { // plugin scope
1334 if ($id = $DB->get_field('config_plugins', 'id', array('name'=>$name, 'plugin'=>$plugin))) {
1335 if ($value===null) {
1336 $DB->delete_records('config_plugins', array('name'=>$name, 'plugin'=>$plugin));
1337 } else {
1338 $DB->set_field('config_plugins', 'value', $value, array('id'=>$id));
1340 } else {
1341 if ($value !== null) {
1342 $config = new stdClass();
1343 $config->plugin = $plugin;
1344 $config->name = $name;
1345 $config->value = $value;
1346 $DB->insert_record('config_plugins', $config, false);
1349 cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1352 return true;
1356 * Get configuration values from the global config table
1357 * or the config_plugins table.
1359 * If called with one parameter, it will load all the config
1360 * variables for one plugin, and return them as an object.
1362 * If called with 2 parameters it will return a string single
1363 * value or false if the value is not found.
1365 * @static $siteidentifier The site identifier is not cached. We use this static cache so
1366 * that we need only fetch it once per request.
1367 * @param string $plugin full component name
1368 * @param string $name default NULL
1369 * @return mixed hash-like object or single value, return false no config found
1371 function get_config($plugin, $name = NULL) {
1372 global $CFG, $DB;
1374 static $siteidentifier = null;
1376 if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) {
1377 $forced =& $CFG->config_php_settings;
1378 $iscore = true;
1379 $plugin = 'core';
1380 } else {
1381 if (array_key_exists($plugin, $CFG->forced_plugin_settings)) {
1382 $forced =& $CFG->forced_plugin_settings[$plugin];
1383 } else {
1384 $forced = array();
1386 $iscore = false;
1389 if ($siteidentifier === null) {
1390 try {
1391 // This may fail during installation.
1392 // If you have a look at {@link initialise_cfg()} you will see that this is how we detect the need to
1393 // install the database.
1394 $siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
1395 } catch (dml_exception $ex) {
1396 // Set siteidentifier to false. We don't want to trip this continually.
1397 $siteidentifier = false;
1398 throw $ex;
1402 if (!empty($name)) {
1403 if (array_key_exists($name, $forced)) {
1404 return (string)$forced[$name];
1405 } else if ($name === 'siteidentifier' && $plugin == 'core') {
1406 return $siteidentifier;
1410 $cache = cache::make('core', 'config');
1411 $result = $cache->get($plugin);
1412 if ($result === false) {
1413 // the user is after a recordset
1414 $result = new stdClass;
1415 if (!$iscore) {
1416 $result = $DB->get_records_menu('config_plugins', array('plugin'=>$plugin), '', 'name,value');
1417 } else {
1418 // this part is not really used any more, but anyway...
1419 $result = $DB->get_records_menu('config', array(), '', 'name,value');;
1421 $cache->set($plugin, $result);
1424 if (!empty($name)) {
1425 if (array_key_exists($name, $result)) {
1426 return $result[$name];
1428 return false;
1431 if ($plugin === 'core') {
1432 $result['siteidentifier'] = $siteidentifier;
1435 foreach ($forced as $key => $value) {
1436 if (is_null($value) or is_array($value) or is_object($value)) {
1437 // we do not want any extra mess here, just real settings that could be saved in db
1438 unset($result[$key]);
1439 } else {
1440 //convert to string as if it went through the DB
1441 $result[$key] = (string)$value;
1445 return (object)$result;
1449 * Removes a key from global configuration
1451 * @param string $name the key to set
1452 * @param string $plugin (optional) the plugin scope
1453 * @global object
1454 * @return boolean whether the operation succeeded.
1456 function unset_config($name, $plugin=NULL) {
1457 global $CFG, $DB;
1459 if (empty($plugin)) {
1460 unset($CFG->$name);
1461 $DB->delete_records('config', array('name'=>$name));
1462 cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1463 } else {
1464 $DB->delete_records('config_plugins', array('name'=>$name, 'plugin'=>$plugin));
1465 cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1468 return true;
1472 * Remove all the config variables for a given plugin.
1474 * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice';
1475 * @return boolean whether the operation succeeded.
1477 function unset_all_config_for_plugin($plugin) {
1478 global $DB;
1479 // Delete from the obvious config_plugins first
1480 $DB->delete_records('config_plugins', array('plugin' => $plugin));
1481 // Next delete any suspect settings from config
1482 $like = $DB->sql_like('name', '?', true, true, false, '|');
1483 $params = array($DB->sql_like_escape($plugin.'_', '|') . '%');
1484 $DB->delete_records_select('config', $like, $params);
1485 // Finally clear both the plugin cache and the core cache (suspect settings now removed from core).
1486 cache_helper::invalidate_by_definition('core', 'config', array(), array('core', $plugin));
1488 return true;
1492 * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability.
1494 * All users are verified if they still have the necessary capability.
1496 * @param string $value the value of the config setting.
1497 * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor.
1498 * @param bool $include admins, include administrators
1499 * @return array of user objects.
1501 function get_users_from_config($value, $capability, $includeadmins = true) {
1502 global $CFG, $DB;
1504 if (empty($value) or $value === '$@NONE@$') {
1505 return array();
1508 // we have to make sure that users still have the necessary capability,
1509 // it should be faster to fetch them all first and then test if they are present
1510 // instead of validating them one-by-one
1511 $users = get_users_by_capability(context_system::instance(), $capability);
1512 if ($includeadmins) {
1513 $admins = get_admins();
1514 foreach ($admins as $admin) {
1515 $users[$admin->id] = $admin;
1519 if ($value === '$@ALL@$') {
1520 return $users;
1523 $result = array(); // result in correct order
1524 $allowed = explode(',', $value);
1525 foreach ($allowed as $uid) {
1526 if (isset($users[$uid])) {
1527 $user = $users[$uid];
1528 $result[$user->id] = $user;
1532 return $result;
1537 * Invalidates browser caches and cached data in temp
1539 * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
1540 * {@see phpunit_util::reset_dataroot()}
1542 * @return void
1544 function purge_all_caches() {
1545 global $CFG;
1547 reset_text_filters_cache();
1548 js_reset_all_caches();
1549 theme_reset_all_caches();
1550 get_string_manager()->reset_caches();
1551 textlib::reset_caches();
1553 cache_helper::purge_all();
1555 // purge all other caches: rss, simplepie, etc.
1556 remove_dir($CFG->cachedir.'', true);
1558 // make sure cache dir is writable, throws exception if not
1559 make_cache_directory('');
1561 // hack: this script may get called after the purifier was initialised,
1562 // but we do not want to verify repeatedly this exists in each call
1563 make_cache_directory('htmlpurifier');
1567 * Get volatile flags
1569 * @param string $type
1570 * @param int $changedsince default null
1571 * @return records array
1573 function get_cache_flags($type, $changedsince=NULL) {
1574 global $DB;
1576 $params = array('type'=>$type, 'expiry'=>time());
1577 $sqlwhere = "flagtype = :type AND expiry >= :expiry";
1578 if ($changedsince !== NULL) {
1579 $params['changedsince'] = $changedsince;
1580 $sqlwhere .= " AND timemodified > :changedsince";
1582 $cf = array();
1584 if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) {
1585 foreach ($flags as $flag) {
1586 $cf[$flag->name] = $flag->value;
1589 return $cf;
1593 * Get volatile flags
1595 * @param string $type
1596 * @param string $name
1597 * @param int $changedsince default null
1598 * @return records array
1600 function get_cache_flag($type, $name, $changedsince=NULL) {
1601 global $DB;
1603 $params = array('type'=>$type, 'name'=>$name, 'expiry'=>time());
1605 $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry";
1606 if ($changedsince !== NULL) {
1607 $params['changedsince'] = $changedsince;
1608 $sqlwhere .= " AND timemodified > :changedsince";
1611 return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params);
1615 * Set a volatile flag
1617 * @param string $type the "type" namespace for the key
1618 * @param string $name the key to set
1619 * @param string $value the value to set (without magic quotes) - NULL will remove the flag
1620 * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs
1621 * @return bool Always returns true
1623 function set_cache_flag($type, $name, $value, $expiry=NULL) {
1624 global $DB;
1626 $timemodified = time();
1627 if ($expiry===NULL || $expiry < $timemodified) {
1628 $expiry = $timemodified + 24 * 60 * 60;
1629 } else {
1630 $expiry = (int)$expiry;
1633 if ($value === NULL) {
1634 unset_cache_flag($type,$name);
1635 return true;
1638 if ($f = $DB->get_record('cache_flags', array('name'=>$name, 'flagtype'=>$type), '*', IGNORE_MULTIPLE)) { // this is a potential problem in DEBUG_DEVELOPER
1639 if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) {
1640 return true; //no need to update
1642 $f->value = $value;
1643 $f->expiry = $expiry;
1644 $f->timemodified = $timemodified;
1645 $DB->update_record('cache_flags', $f);
1646 } else {
1647 $f = new stdClass();
1648 $f->flagtype = $type;
1649 $f->name = $name;
1650 $f->value = $value;
1651 $f->expiry = $expiry;
1652 $f->timemodified = $timemodified;
1653 $DB->insert_record('cache_flags', $f);
1655 return true;
1659 * Removes a single volatile flag
1661 * @global object
1662 * @param string $type the "type" namespace for the key
1663 * @param string $name the key to set
1664 * @return bool
1666 function unset_cache_flag($type, $name) {
1667 global $DB;
1668 $DB->delete_records('cache_flags', array('name'=>$name, 'flagtype'=>$type));
1669 return true;
1673 * Garbage-collect volatile flags
1675 * @return bool Always returns true
1677 function gc_cache_flags() {
1678 global $DB;
1679 $DB->delete_records_select('cache_flags', 'expiry < ?', array(time()));
1680 return true;
1683 // USER PREFERENCE API
1686 * Refresh user preference cache. This is used most often for $USER
1687 * object that is stored in session, but it also helps with performance in cron script.
1689 * Preferences for each user are loaded on first use on every page, then again after the timeout expires.
1691 * @package core
1692 * @category preference
1693 * @access public
1694 * @param stdClass $user User object. Preferences are preloaded into 'preference' property
1695 * @param int $cachelifetime Cache life time on the current page (in seconds)
1696 * @throws coding_exception
1697 * @return null
1699 function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120) {
1700 global $DB;
1701 static $loadedusers = array(); // Static cache, we need to check on each page load, not only every 2 minutes.
1703 if (!isset($user->id)) {
1704 throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field');
1707 if (empty($user->id) or isguestuser($user->id)) {
1708 // No permanent storage for not-logged-in users and guest
1709 if (!isset($user->preference)) {
1710 $user->preference = array();
1712 return;
1715 $timenow = time();
1717 if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) {
1718 // Already loaded at least once on this page. Are we up to date?
1719 if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) {
1720 // no need to reload - we are on the same page and we loaded prefs just a moment ago
1721 return;
1723 } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) {
1724 // no change since the lastcheck on this page
1725 $user->preference['_lastloaded'] = $timenow;
1726 return;
1730 // OK, so we have to reload all preferences
1731 $loadedusers[$user->id] = true;
1732 $user->preference = $DB->get_records_menu('user_preferences', array('userid'=>$user->id), '', 'name,value'); // All values
1733 $user->preference['_lastloaded'] = $timenow;
1737 * Called from set/unset_user_preferences, so that the prefs can
1738 * be correctly reloaded in different sessions.
1740 * NOTE: internal function, do not call from other code.
1742 * @package core
1743 * @access private
1744 * @param integer $userid the user whose prefs were changed.
1746 function mark_user_preferences_changed($userid) {
1747 global $CFG;
1749 if (empty($userid) or isguestuser($userid)) {
1750 // no cache flags for guest and not-logged-in users
1751 return;
1754 set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout);
1758 * Sets a preference for the specified user.
1760 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1762 * @package core
1763 * @category preference
1764 * @access public
1765 * @param string $name The key to set as preference for the specified user
1766 * @param string $value The value to set for the $name key in the specified user's
1767 * record, null means delete current value.
1768 * @param stdClass|int|null $user A moodle user object or id, null means current user
1769 * @throws coding_exception
1770 * @return bool Always true or exception
1772 function set_user_preference($name, $value, $user = null) {
1773 global $USER, $DB;
1775 if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1776 throw new coding_exception('Invalid preference name in set_user_preference() call');
1779 if (is_null($value)) {
1780 // null means delete current
1781 return unset_user_preference($name, $user);
1782 } else if (is_object($value)) {
1783 throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed');
1784 } else if (is_array($value)) {
1785 throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed');
1787 $value = (string)$value;
1788 if (textlib::strlen($value) > 1333) { //value column maximum length is 1333 characters
1789 throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column');
1792 if (is_null($user)) {
1793 $user = $USER;
1794 } else if (isset($user->id)) {
1795 // $user is valid object
1796 } else if (is_numeric($user)) {
1797 $user = (object)array('id'=>(int)$user);
1798 } else {
1799 throw new coding_exception('Invalid $user parameter in set_user_preference() call');
1802 check_user_preferences_loaded($user);
1804 if (empty($user->id) or isguestuser($user->id)) {
1805 // no permanent storage for not-logged-in users and guest
1806 $user->preference[$name] = $value;
1807 return true;
1810 if ($preference = $DB->get_record('user_preferences', array('userid'=>$user->id, 'name'=>$name))) {
1811 if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) {
1812 // preference already set to this value
1813 return true;
1815 $DB->set_field('user_preferences', 'value', $value, array('id'=>$preference->id));
1817 } else {
1818 $preference = new stdClass();
1819 $preference->userid = $user->id;
1820 $preference->name = $name;
1821 $preference->value = $value;
1822 $DB->insert_record('user_preferences', $preference);
1825 // update value in cache
1826 $user->preference[$name] = $value;
1828 // set reload flag for other sessions
1829 mark_user_preferences_changed($user->id);
1831 return true;
1835 * Sets a whole array of preferences for the current user
1837 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1839 * @package core
1840 * @category preference
1841 * @access public
1842 * @param array $prefarray An array of key/value pairs to be set
1843 * @param stdClass|int|null $user A moodle user object or id, null means current user
1844 * @return bool Always true or exception
1846 function set_user_preferences(array $prefarray, $user = null) {
1847 foreach ($prefarray as $name => $value) {
1848 set_user_preference($name, $value, $user);
1850 return true;
1854 * Unsets a preference completely by deleting it from the database
1856 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1858 * @package core
1859 * @category preference
1860 * @access public
1861 * @param string $name The key to unset as preference for the specified user
1862 * @param stdClass|int|null $user A moodle user object or id, null means current user
1863 * @throws coding_exception
1864 * @return bool Always true or exception
1866 function unset_user_preference($name, $user = null) {
1867 global $USER, $DB;
1869 if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1870 throw new coding_exception('Invalid preference name in unset_user_preference() call');
1873 if (is_null($user)) {
1874 $user = $USER;
1875 } else if (isset($user->id)) {
1876 // $user is valid object
1877 } else if (is_numeric($user)) {
1878 $user = (object)array('id'=>(int)$user);
1879 } else {
1880 throw new coding_exception('Invalid $user parameter in unset_user_preference() call');
1883 check_user_preferences_loaded($user);
1885 if (empty($user->id) or isguestuser($user->id)) {
1886 // no permanent storage for not-logged-in user and guest
1887 unset($user->preference[$name]);
1888 return true;
1891 // delete from DB
1892 $DB->delete_records('user_preferences', array('userid'=>$user->id, 'name'=>$name));
1894 // delete the preference from cache
1895 unset($user->preference[$name]);
1897 // set reload flag for other sessions
1898 mark_user_preferences_changed($user->id);
1900 return true;
1904 * Used to fetch user preference(s)
1906 * If no arguments are supplied this function will return
1907 * all of the current user preferences as an array.
1909 * If a name is specified then this function
1910 * attempts to return that particular preference value. If
1911 * none is found, then the optional value $default is returned,
1912 * otherwise NULL.
1914 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1916 * @package core
1917 * @category preference
1918 * @access public
1919 * @param string $name Name of the key to use in finding a preference value
1920 * @param mixed|null $default Value to be returned if the $name key is not set in the user preferences
1921 * @param stdClass|int|null $user A moodle user object or id, null means current user
1922 * @throws coding_exception
1923 * @return string|mixed|null A string containing the value of a single preference. An
1924 * array with all of the preferences or null
1926 function get_user_preferences($name = null, $default = null, $user = null) {
1927 global $USER;
1929 if (is_null($name)) {
1930 // all prefs
1931 } else if (is_numeric($name) or $name === '_lastloaded') {
1932 throw new coding_exception('Invalid preference name in get_user_preferences() call');
1935 if (is_null($user)) {
1936 $user = $USER;
1937 } else if (isset($user->id)) {
1938 // $user is valid object
1939 } else if (is_numeric($user)) {
1940 $user = (object)array('id'=>(int)$user);
1941 } else {
1942 throw new coding_exception('Invalid $user parameter in get_user_preferences() call');
1945 check_user_preferences_loaded($user);
1947 if (empty($name)) {
1948 return $user->preference; // All values
1949 } else if (isset($user->preference[$name])) {
1950 return $user->preference[$name]; // The single string value
1951 } else {
1952 return $default; // Default value (null if not specified)
1956 /// FUNCTIONS FOR HANDLING TIME ////////////////////////////////////////////
1959 * Given date parts in user time produce a GMT timestamp.
1961 * @package core
1962 * @category time
1963 * @param int $year The year part to create timestamp of
1964 * @param int $month The month part to create timestamp of
1965 * @param int $day The day part to create timestamp of
1966 * @param int $hour The hour part to create timestamp of
1967 * @param int $minute The minute part to create timestamp of
1968 * @param int $second The second part to create timestamp of
1969 * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset.
1970 * if 99 then default user's timezone is used {@link http://docs.moodle.org/dev/Time_API#Timezone}
1971 * @param bool $applydst Toggle Daylight Saving Time, default true, will be
1972 * applied only if timezone is 99 or string.
1973 * @return int GMT timestamp
1975 function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) {
1977 //save input timezone, required for dst offset check.
1978 $passedtimezone = $timezone;
1980 $timezone = get_user_timezone_offset($timezone);
1982 if (abs($timezone) > 13) { //server time
1983 $time = mktime((int)$hour, (int)$minute, (int)$second, (int)$month, (int)$day, (int)$year);
1984 } else {
1985 $time = gmmktime((int)$hour, (int)$minute, (int)$second, (int)$month, (int)$day, (int)$year);
1986 $time = usertime($time, $timezone);
1988 //Apply dst for string timezones or if 99 then try dst offset with user's default timezone
1989 if ($applydst && ((99 == $passedtimezone) || !is_numeric($passedtimezone))) {
1990 $time -= dst_offset_on($time, $passedtimezone);
1994 return $time;
1999 * Format a date/time (seconds) as weeks, days, hours etc as needed
2001 * Given an amount of time in seconds, returns string
2002 * formatted nicely as weeks, days, hours etc as needed
2004 * @package core
2005 * @category time
2006 * @uses MINSECS
2007 * @uses HOURSECS
2008 * @uses DAYSECS
2009 * @uses YEARSECS
2010 * @param int $totalsecs Time in seconds
2011 * @param object $str Should be a time object
2012 * @return string A nicely formatted date/time string
2014 function format_time($totalsecs, $str=NULL) {
2016 $totalsecs = abs($totalsecs);
2018 if (!$str) { // Create the str structure the slow way
2019 $str = new stdClass();
2020 $str->day = get_string('day');
2021 $str->days = get_string('days');
2022 $str->hour = get_string('hour');
2023 $str->hours = get_string('hours');
2024 $str->min = get_string('min');
2025 $str->mins = get_string('mins');
2026 $str->sec = get_string('sec');
2027 $str->secs = get_string('secs');
2028 $str->year = get_string('year');
2029 $str->years = get_string('years');
2033 $years = floor($totalsecs/YEARSECS);
2034 $remainder = $totalsecs - ($years*YEARSECS);
2035 $days = floor($remainder/DAYSECS);
2036 $remainder = $totalsecs - ($days*DAYSECS);
2037 $hours = floor($remainder/HOURSECS);
2038 $remainder = $remainder - ($hours*HOURSECS);
2039 $mins = floor($remainder/MINSECS);
2040 $secs = $remainder - ($mins*MINSECS);
2042 $ss = ($secs == 1) ? $str->sec : $str->secs;
2043 $sm = ($mins == 1) ? $str->min : $str->mins;
2044 $sh = ($hours == 1) ? $str->hour : $str->hours;
2045 $sd = ($days == 1) ? $str->day : $str->days;
2046 $sy = ($years == 1) ? $str->year : $str->years;
2048 $oyears = '';
2049 $odays = '';
2050 $ohours = '';
2051 $omins = '';
2052 $osecs = '';
2054 if ($years) $oyears = $years .' '. $sy;
2055 if ($days) $odays = $days .' '. $sd;
2056 if ($hours) $ohours = $hours .' '. $sh;
2057 if ($mins) $omins = $mins .' '. $sm;
2058 if ($secs) $osecs = $secs .' '. $ss;
2060 if ($years) return trim($oyears .' '. $odays);
2061 if ($days) return trim($odays .' '. $ohours);
2062 if ($hours) return trim($ohours .' '. $omins);
2063 if ($mins) return trim($omins .' '. $osecs);
2064 if ($secs) return $osecs;
2065 return get_string('now');
2069 * Returns a formatted string that represents a date in user time
2071 * Returns a formatted string that represents a date in user time
2072 * <b>WARNING: note that the format is for strftime(), not date().</b>
2073 * Because of a bug in most Windows time libraries, we can't use
2074 * the nicer %e, so we have to use %d which has leading zeroes.
2075 * A lot of the fuss in the function is just getting rid of these leading
2076 * zeroes as efficiently as possible.
2078 * If parameter fixday = true (default), then take off leading
2079 * zero from %d, else maintain it.
2081 * @package core
2082 * @category time
2083 * @param int $date the timestamp in UTC, as obtained from the database.
2084 * @param string $format strftime format. You should probably get this using
2085 * get_string('strftime...', 'langconfig');
2086 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
2087 * not 99 then daylight saving will not be added.
2088 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2089 * @param bool $fixday If true (default) then the leading zero from %d is removed.
2090 * If false then the leading zero is maintained.
2091 * @param bool $fixhour If true (default) then the leading zero from %I is removed.
2092 * @return string the formatted date/time.
2094 function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
2096 global $CFG;
2098 if (empty($format)) {
2099 $format = get_string('strftimedaydatetime', 'langconfig');
2102 if (!empty($CFG->nofixday)) { // Config.php can force %d not to be fixed.
2103 $fixday = false;
2104 } else if ($fixday) {
2105 $formatnoday = str_replace('%d', 'DD', $format);
2106 $fixday = ($formatnoday != $format);
2107 $format = $formatnoday;
2110 // Note: This logic about fixing 12-hour time to remove unnecessary leading
2111 // zero is required because on Windows, PHP strftime function does not
2112 // support the correct 'hour without leading zero' parameter (%l).
2113 if (!empty($CFG->nofixhour)) {
2114 // Config.php can force %I not to be fixed.
2115 $fixhour = false;
2116 } else if ($fixhour) {
2117 $formatnohour = str_replace('%I', 'HH', $format);
2118 $fixhour = ($formatnohour != $format);
2119 $format = $formatnohour;
2122 //add daylight saving offset for string timezones only, as we can't get dst for
2123 //float values. if timezone is 99 (user default timezone), then try update dst.
2124 if ((99 == $timezone) || !is_numeric($timezone)) {
2125 $date += dst_offset_on($date, $timezone);
2128 $timezone = get_user_timezone_offset($timezone);
2130 // If we are running under Windows convert to windows encoding and then back to UTF-8
2131 // (because it's impossible to specify UTF-8 to fetch locale info in Win32)
2133 if (abs($timezone) > 13) { /// Server time
2134 $datestring = date_format_string($date, $format, $timezone);
2135 if ($fixday) {
2136 $daystring = ltrim(str_replace(array(' 0', ' '), '', strftime(' %d', $date)));
2137 $datestring = str_replace('DD', $daystring, $datestring);
2139 if ($fixhour) {
2140 $hourstring = ltrim(str_replace(array(' 0', ' '), '', strftime(' %I', $date)));
2141 $datestring = str_replace('HH', $hourstring, $datestring);
2144 } else {
2145 $date += (int)($timezone * 3600);
2146 $datestring = date_format_string($date, $format, $timezone);
2147 if ($fixday) {
2148 $daystring = ltrim(str_replace(array(' 0', ' '), '', gmstrftime(' %d', $date)));
2149 $datestring = str_replace('DD', $daystring, $datestring);
2151 if ($fixhour) {
2152 $hourstring = ltrim(str_replace(array(' 0', ' '), '', gmstrftime(' %I', $date)));
2153 $datestring = str_replace('HH', $hourstring, $datestring);
2157 return $datestring;
2161 * Returns a formatted date ensuring it is UTF-8.
2163 * If we are running under Windows convert to Windows encoding and then back to UTF-8
2164 * (because it's impossible to specify UTF-8 to fetch locale info in Win32).
2166 * This function does not do any calculation regarding the user preferences and should
2167 * therefore receive the final date timestamp, format and timezone. Timezone being only used
2168 * to differenciate the use of server time or not (strftime() against gmstrftime()).
2170 * @param int $date the timestamp.
2171 * @param string $format strftime format.
2172 * @param int|float $timezone the numerical timezone, typically returned by {@link get_user_timezone_offset()}.
2173 * @return string the formatted date/time.
2174 * @since 2.3.3
2176 function date_format_string($date, $format, $tz = 99) {
2177 global $CFG;
2178 if (abs($tz) > 13) {
2179 if ($CFG->ostype == 'WINDOWS' and $localewincharset = get_string('localewincharset', 'langconfig')) {
2180 $format = textlib::convert($format, 'utf-8', $localewincharset);
2181 $datestring = strftime($format, $date);
2182 $datestring = textlib::convert($datestring, $localewincharset, 'utf-8');
2183 } else {
2184 $datestring = strftime($format, $date);
2186 } else {
2187 if ($CFG->ostype == 'WINDOWS' and $localewincharset = get_string('localewincharset', 'langconfig')) {
2188 $format = textlib::convert($format, 'utf-8', $localewincharset);
2189 $datestring = gmstrftime($format, $date);
2190 $datestring = textlib::convert($datestring, $localewincharset, 'utf-8');
2191 } else {
2192 $datestring = gmstrftime($format, $date);
2195 return $datestring;
2199 * Given a $time timestamp in GMT (seconds since epoch),
2200 * returns an array that represents the date in user time
2202 * @package core
2203 * @category time
2204 * @uses HOURSECS
2205 * @param int $time Timestamp in GMT
2206 * @param float|int|string $timezone offset's time with timezone, if float and not 99, then no
2207 * dst offset is applyed {@link http://docs.moodle.org/dev/Time_API#Timezone}
2208 * @return array An array that represents the date in user time
2210 function usergetdate($time, $timezone=99) {
2212 //save input timezone, required for dst offset check.
2213 $passedtimezone = $timezone;
2215 $timezone = get_user_timezone_offset($timezone);
2217 if (abs($timezone) > 13) { // Server time
2218 return getdate($time);
2221 //add daylight saving offset for string timezones only, as we can't get dst for
2222 //float values. if timezone is 99 (user default timezone), then try update dst.
2223 if ($passedtimezone == 99 || !is_numeric($passedtimezone)) {
2224 $time += dst_offset_on($time, $passedtimezone);
2227 $time += intval((float)$timezone * HOURSECS);
2229 $datestring = gmstrftime('%B_%A_%j_%Y_%m_%w_%d_%H_%M_%S', $time);
2231 //be careful to ensure the returned array matches that produced by getdate() above
2232 list(
2233 $getdate['month'],
2234 $getdate['weekday'],
2235 $getdate['yday'],
2236 $getdate['year'],
2237 $getdate['mon'],
2238 $getdate['wday'],
2239 $getdate['mday'],
2240 $getdate['hours'],
2241 $getdate['minutes'],
2242 $getdate['seconds']
2243 ) = explode('_', $datestring);
2245 // set correct datatype to match with getdate()
2246 $getdate['seconds'] = (int)$getdate['seconds'];
2247 $getdate['yday'] = (int)$getdate['yday'] - 1; // gettime returns 0 through 365
2248 $getdate['year'] = (int)$getdate['year'];
2249 $getdate['mon'] = (int)$getdate['mon'];
2250 $getdate['wday'] = (int)$getdate['wday'];
2251 $getdate['mday'] = (int)$getdate['mday'];
2252 $getdate['hours'] = (int)$getdate['hours'];
2253 $getdate['minutes'] = (int)$getdate['minutes'];
2254 return $getdate;
2258 * Given a GMT timestamp (seconds since epoch), offsets it by
2259 * the timezone. eg 3pm in India is 3pm GMT - 7 * 3600 seconds
2261 * @package core
2262 * @category time
2263 * @uses HOURSECS
2264 * @param int $date Timestamp in GMT
2265 * @param float|int|string $timezone timezone to calculate GMT time offset before
2266 * calculating user time, 99 is default user timezone
2267 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2268 * @return int
2270 function usertime($date, $timezone=99) {
2272 $timezone = get_user_timezone_offset($timezone);
2274 if (abs($timezone) > 13) {
2275 return $date;
2277 return $date - (int)($timezone * HOURSECS);
2281 * Given a time, return the GMT timestamp of the most recent midnight
2282 * for the current user.
2284 * @package core
2285 * @category time
2286 * @param int $date Timestamp in GMT
2287 * @param float|int|string $timezone timezone to calculate GMT time offset before
2288 * calculating user midnight time, 99 is default user timezone
2289 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2290 * @return int Returns a GMT timestamp
2292 function usergetmidnight($date, $timezone=99) {
2294 $userdate = usergetdate($date, $timezone);
2296 // Time of midnight of this user's day, in GMT
2297 return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone);
2302 * Returns a string that prints the user's timezone
2304 * @package core
2305 * @category time
2306 * @param float|int|string $timezone timezone to calculate GMT time offset before
2307 * calculating user timezone, 99 is default user timezone
2308 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2309 * @return string
2311 function usertimezone($timezone=99) {
2313 $tz = get_user_timezone($timezone);
2315 if (!is_float($tz)) {
2316 return $tz;
2319 if(abs($tz) > 13) { // Server time
2320 return get_string('serverlocaltime');
2323 if($tz == intval($tz)) {
2324 // Don't show .0 for whole hours
2325 $tz = intval($tz);
2328 if($tz == 0) {
2329 return 'UTC';
2331 else if($tz > 0) {
2332 return 'UTC+'.$tz;
2334 else {
2335 return 'UTC'.$tz;
2341 * Returns a float which represents the user's timezone difference from GMT in hours
2342 * Checks various settings and picks the most dominant of those which have a value
2344 * @package core
2345 * @category time
2346 * @param float|int|string $tz timezone to calculate GMT time offset for user,
2347 * 99 is default user timezone
2348 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2349 * @return float
2351 function get_user_timezone_offset($tz = 99) {
2353 global $USER, $CFG;
2355 $tz = get_user_timezone($tz);
2357 if (is_float($tz)) {
2358 return $tz;
2359 } else {
2360 $tzrecord = get_timezone_record($tz);
2361 if (empty($tzrecord)) {
2362 return 99.0;
2364 return (float)$tzrecord->gmtoff / HOURMINS;
2369 * Returns an int which represents the systems's timezone difference from GMT in seconds
2371 * @package core
2372 * @category time
2373 * @param float|int|string $tz timezone for which offset is required.
2374 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2375 * @return int|bool if found, false is timezone 99 or error
2377 function get_timezone_offset($tz) {
2378 global $CFG;
2380 if ($tz == 99) {
2381 return false;
2384 if (is_numeric($tz)) {
2385 return intval($tz * 60*60);
2388 if (!$tzrecord = get_timezone_record($tz)) {
2389 return false;
2391 return intval($tzrecord->gmtoff * 60);
2395 * Returns a float or a string which denotes the user's timezone
2396 * 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)
2397 * means that for this timezone there are also DST rules to be taken into account
2398 * Checks various settings and picks the most dominant of those which have a value
2400 * @package core
2401 * @category time
2402 * @param float|int|string $tz timezone to calculate GMT time offset before
2403 * calculating user timezone, 99 is default user timezone
2404 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2405 * @return float|string
2407 function get_user_timezone($tz = 99) {
2408 global $USER, $CFG;
2410 $timezones = array(
2411 $tz,
2412 isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99,
2413 isset($USER->timezone) ? $USER->timezone : 99,
2414 isset($CFG->timezone) ? $CFG->timezone : 99,
2417 $tz = 99;
2419 // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array
2420 while(((empty($tz) && !is_numeric($tz)) || $tz == 99) && $next = each($timezones)) {
2421 $tz = $next['value'];
2423 return is_numeric($tz) ? (float) $tz : $tz;
2427 * Returns cached timezone record for given $timezonename
2429 * @package core
2430 * @param string $timezonename name of the timezone
2431 * @return stdClass|bool timezonerecord or false
2433 function get_timezone_record($timezonename) {
2434 global $CFG, $DB;
2435 static $cache = NULL;
2437 if ($cache === NULL) {
2438 $cache = array();
2441 if (isset($cache[$timezonename])) {
2442 return $cache[$timezonename];
2445 return $cache[$timezonename] = $DB->get_record_sql('SELECT * FROM {timezone}
2446 WHERE name = ? ORDER BY year DESC', array($timezonename), IGNORE_MULTIPLE);
2450 * Build and store the users Daylight Saving Time (DST) table
2452 * @package core
2453 * @param int $from_year Start year for the table, defaults to 1971
2454 * @param int $to_year End year for the table, defaults to 2035
2455 * @param int|float|string $strtimezone, timezone to check if dst should be applyed.
2456 * @return bool
2458 function calculate_user_dst_table($from_year = NULL, $to_year = NULL, $strtimezone = NULL) {
2459 global $CFG, $SESSION, $DB;
2461 $usertz = get_user_timezone($strtimezone);
2463 if (is_float($usertz)) {
2464 // Trivial timezone, no DST
2465 return false;
2468 if (!empty($SESSION->dst_offsettz) && $SESSION->dst_offsettz != $usertz) {
2469 // We have precalculated values, but the user's effective TZ has changed in the meantime, so reset
2470 unset($SESSION->dst_offsets);
2471 unset($SESSION->dst_range);
2474 if (!empty($SESSION->dst_offsets) && empty($from_year) && empty($to_year)) {
2475 // Repeat calls which do not request specific year ranges stop here, we have already calculated the table
2476 // This will be the return path most of the time, pretty light computationally
2477 return true;
2480 // Reaching here means we either need to extend our table or create it from scratch
2482 // Remember which TZ we calculated these changes for
2483 $SESSION->dst_offsettz = $usertz;
2485 if(empty($SESSION->dst_offsets)) {
2486 // If we 're creating from scratch, put the two guard elements in there
2487 $SESSION->dst_offsets = array(1 => NULL, 0 => NULL);
2489 if(empty($SESSION->dst_range)) {
2490 // If creating from scratch
2491 $from = max((empty($from_year) ? intval(date('Y')) - 3 : $from_year), 1971);
2492 $to = min((empty($to_year) ? intval(date('Y')) + 3 : $to_year), 2035);
2494 // Fill in the array with the extra years we need to process
2495 $yearstoprocess = array();
2496 for($i = $from; $i <= $to; ++$i) {
2497 $yearstoprocess[] = $i;
2500 // Take note of which years we have processed for future calls
2501 $SESSION->dst_range = array($from, $to);
2503 else {
2504 // If needing to extend the table, do the same
2505 $yearstoprocess = array();
2507 $from = max((empty($from_year) ? $SESSION->dst_range[0] : $from_year), 1971);
2508 $to = min((empty($to_year) ? $SESSION->dst_range[1] : $to_year), 2035);
2510 if($from < $SESSION->dst_range[0]) {
2511 // Take note of which years we need to process and then note that we have processed them for future calls
2512 for($i = $from; $i < $SESSION->dst_range[0]; ++$i) {
2513 $yearstoprocess[] = $i;
2515 $SESSION->dst_range[0] = $from;
2517 if($to > $SESSION->dst_range[1]) {
2518 // Take note of which years we need to process and then note that we have processed them for future calls
2519 for($i = $SESSION->dst_range[1] + 1; $i <= $to; ++$i) {
2520 $yearstoprocess[] = $i;
2522 $SESSION->dst_range[1] = $to;
2526 if(empty($yearstoprocess)) {
2527 // This means that there was a call requesting a SMALLER range than we have already calculated
2528 return true;
2531 // From now on, we know that the array has at least the two guard elements, and $yearstoprocess has the years we need
2532 // Also, the array is sorted in descending timestamp order!
2534 // Get DB data
2536 static $presets_cache = array();
2537 if (!isset($presets_cache[$usertz])) {
2538 $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');
2540 if(empty($presets_cache[$usertz])) {
2541 return false;
2544 // Remove ending guard (first element of the array)
2545 reset($SESSION->dst_offsets);
2546 unset($SESSION->dst_offsets[key($SESSION->dst_offsets)]);
2548 // Add all required change timestamps
2549 foreach($yearstoprocess as $y) {
2550 // Find the record which is in effect for the year $y
2551 foreach($presets_cache[$usertz] as $year => $preset) {
2552 if($year <= $y) {
2553 break;
2557 $changes = dst_changes_for_year($y, $preset);
2559 if($changes === NULL) {
2560 continue;
2562 if($changes['dst'] != 0) {
2563 $SESSION->dst_offsets[$changes['dst']] = $preset->dstoff * MINSECS;
2565 if($changes['std'] != 0) {
2566 $SESSION->dst_offsets[$changes['std']] = 0;
2570 // Put in a guard element at the top
2571 $maxtimestamp = max(array_keys($SESSION->dst_offsets));
2572 $SESSION->dst_offsets[($maxtimestamp + DAYSECS)] = NULL; // DAYSECS is arbitrary, any "small" number will do
2574 // Sort again
2575 krsort($SESSION->dst_offsets);
2577 return true;
2581 * Calculates the required DST change and returns a Timestamp Array
2583 * @package core
2584 * @category time
2585 * @uses HOURSECS
2586 * @uses MINSECS
2587 * @param int|string $year Int or String Year to focus on
2588 * @param object $timezone Instatiated Timezone object
2589 * @return array|null Array dst=>xx, 0=>xx, std=>yy, 1=>yy or NULL
2591 function dst_changes_for_year($year, $timezone) {
2593 if($timezone->dst_startday == 0 && $timezone->dst_weekday == 0 && $timezone->std_startday == 0 && $timezone->std_weekday == 0) {
2594 return NULL;
2597 $monthdaydst = find_day_in_month($timezone->dst_startday, $timezone->dst_weekday, $timezone->dst_month, $year);
2598 $monthdaystd = find_day_in_month($timezone->std_startday, $timezone->std_weekday, $timezone->std_month, $year);
2600 list($dst_hour, $dst_min) = explode(':', $timezone->dst_time);
2601 list($std_hour, $std_min) = explode(':', $timezone->std_time);
2603 $timedst = make_timestamp($year, $timezone->dst_month, $monthdaydst, 0, 0, 0, 99, false);
2604 $timestd = make_timestamp($year, $timezone->std_month, $monthdaystd, 0, 0, 0, 99, false);
2606 // Instead of putting hour and minute in make_timestamp(), we add them afterwards.
2607 // This has the advantage of being able to have negative values for hour, i.e. for timezones
2608 // where GMT time would be in the PREVIOUS day than the local one on which DST changes.
2610 $timedst += $dst_hour * HOURSECS + $dst_min * MINSECS;
2611 $timestd += $std_hour * HOURSECS + $std_min * MINSECS;
2613 return array('dst' => $timedst, 0 => $timedst, 'std' => $timestd, 1 => $timestd);
2617 * Calculates the Daylight Saving Offset for a given date/time (timestamp)
2618 * - Note: Daylight saving only works for string timezones and not for float.
2620 * @package core
2621 * @category time
2622 * @param int $time must NOT be compensated at all, it has to be a pure timestamp
2623 * @param int|float|string $strtimezone timezone for which offset is expected, if 99 or null
2624 * then user's default timezone is used. {@link http://docs.moodle.org/dev/Time_API#Timezone}
2625 * @return int
2627 function dst_offset_on($time, $strtimezone = NULL) {
2628 global $SESSION;
2630 if(!calculate_user_dst_table(NULL, NULL, $strtimezone) || empty($SESSION->dst_offsets)) {
2631 return 0;
2634 reset($SESSION->dst_offsets);
2635 while(list($from, $offset) = each($SESSION->dst_offsets)) {
2636 if($from <= $time) {
2637 break;
2641 // This is the normal return path
2642 if($offset !== NULL) {
2643 return $offset;
2646 // Reaching this point means we haven't calculated far enough, do it now:
2647 // Calculate extra DST changes if needed and recurse. The recursion always
2648 // moves toward the stopping condition, so will always end.
2650 if($from == 0) {
2651 // We need a year smaller than $SESSION->dst_range[0]
2652 if($SESSION->dst_range[0] == 1971) {
2653 return 0;
2655 calculate_user_dst_table($SESSION->dst_range[0] - 5, NULL, $strtimezone);
2656 return dst_offset_on($time, $strtimezone);
2658 else {
2659 // We need a year larger than $SESSION->dst_range[1]
2660 if($SESSION->dst_range[1] == 2035) {
2661 return 0;
2663 calculate_user_dst_table(NULL, $SESSION->dst_range[1] + 5, $strtimezone);
2664 return dst_offset_on($time, $strtimezone);
2669 * Calculates when the day appears in specific month
2671 * @package core
2672 * @category time
2673 * @param int $startday starting day of the month
2674 * @param int $weekday The day when week starts (normally taken from user preferences)
2675 * @param int $month The month whose day is sought
2676 * @param int $year The year of the month whose day is sought
2677 * @return int
2679 function find_day_in_month($startday, $weekday, $month, $year) {
2681 $daysinmonth = days_in_month($month, $year);
2683 if($weekday == -1) {
2684 // Don't care about weekday, so return:
2685 // abs($startday) if $startday != -1
2686 // $daysinmonth otherwise
2687 return ($startday == -1) ? $daysinmonth : abs($startday);
2690 // From now on we 're looking for a specific weekday
2692 // Give "end of month" its actual value, since we know it
2693 if($startday == -1) {
2694 $startday = -1 * $daysinmonth;
2697 // Starting from day $startday, the sign is the direction
2699 if($startday < 1) {
2701 $startday = abs($startday);
2702 $lastmonthweekday = strftime('%w', mktime(12, 0, 0, $month, $daysinmonth, $year));
2704 // This is the last such weekday of the month
2705 $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday;
2706 if($lastinmonth > $daysinmonth) {
2707 $lastinmonth -= 7;
2710 // Find the first such weekday <= $startday
2711 while($lastinmonth > $startday) {
2712 $lastinmonth -= 7;
2715 return $lastinmonth;
2718 else {
2720 $indexweekday = strftime('%w', mktime(12, 0, 0, $month, $startday, $year));
2722 $diff = $weekday - $indexweekday;
2723 if($diff < 0) {
2724 $diff += 7;
2727 // This is the first such weekday of the month equal to or after $startday
2728 $firstfromindex = $startday + $diff;
2730 return $firstfromindex;
2736 * Calculate the number of days in a given month
2738 * @package core
2739 * @category time
2740 * @param int $month The month whose day count is sought
2741 * @param int $year The year of the month whose day count is sought
2742 * @return int
2744 function days_in_month($month, $year) {
2745 return intval(date('t', mktime(12, 0, 0, $month, 1, $year)));
2749 * Calculate the position in the week of a specific calendar day
2751 * @package core
2752 * @category time
2753 * @param int $day The day of the date whose position in the week is sought
2754 * @param int $month The month of the date whose position in the week is sought
2755 * @param int $year The year of the date whose position in the week is sought
2756 * @return int
2758 function dayofweek($day, $month, $year) {
2759 // I wonder if this is any different from
2760 // strftime('%w', mktime(12, 0, 0, $month, $daysinmonth, $year, 0));
2761 return intval(date('w', mktime(12, 0, 0, $month, $day, $year)));
2764 /// USER AUTHENTICATION AND LOGIN ////////////////////////////////////////
2767 * Returns full login url.
2769 * @return string login url
2771 function get_login_url() {
2772 global $CFG;
2774 $url = "$CFG->wwwroot/login/index.php";
2776 if (!empty($CFG->loginhttps)) {
2777 $url = str_replace('http:', 'https:', $url);
2780 return $url;
2784 * This function checks that the current user is logged in and has the
2785 * required privileges
2787 * This function checks that the current user is logged in, and optionally
2788 * whether they are allowed to be in a particular course and view a particular
2789 * course module.
2790 * If they are not logged in, then it redirects them to the site login unless
2791 * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which
2792 * case they are automatically logged in as guests.
2793 * If $courseid is given and the user is not enrolled in that course then the
2794 * user is redirected to the course enrolment page.
2795 * If $cm is given and the course module is hidden and the user is not a teacher
2796 * in the course then the user is redirected to the course home page.
2798 * When $cm parameter specified, this function sets page layout to 'module'.
2799 * You need to change it manually later if some other layout needed.
2801 * @package core_access
2802 * @category access
2804 * @param mixed $courseorid id of the course or course object
2805 * @param bool $autologinguest default true
2806 * @param object $cm course module object
2807 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2808 * true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2809 * in order to keep redirects working properly. MDL-14495
2810 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2811 * @return mixed Void, exit, and die depending on path
2813 function require_login($courseorid = NULL, $autologinguest = true, $cm = NULL, $setwantsurltome = true, $preventredirect = false) {
2814 global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT;
2816 // Must not redirect when byteserving already started.
2817 if (!empty($_SERVER['HTTP_RANGE'])) {
2818 $preventredirect = true;
2821 // setup global $COURSE, themes, language and locale
2822 if (!empty($courseorid)) {
2823 if (is_object($courseorid)) {
2824 $course = $courseorid;
2825 } else if ($courseorid == SITEID) {
2826 $course = clone($SITE);
2827 } else {
2828 $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST);
2830 if ($cm) {
2831 if ($cm->course != $course->id) {
2832 throw new coding_exception('course and cm parameters in require_login() call do not match!!');
2834 // make sure we have a $cm from get_fast_modinfo as this contains activity access details
2835 if (!($cm instanceof cm_info)) {
2836 // note: nearly all pages call get_fast_modinfo anyway and it does not make any
2837 // db queries so this is not really a performance concern, however it is obviously
2838 // better if you use get_fast_modinfo to get the cm before calling this.
2839 $modinfo = get_fast_modinfo($course);
2840 $cm = $modinfo->get_cm($cm->id);
2842 $PAGE->set_cm($cm, $course); // set's up global $COURSE
2843 $PAGE->set_pagelayout('incourse');
2844 } else {
2845 $PAGE->set_course($course); // set's up global $COURSE
2847 } else {
2848 // do not touch global $COURSE via $PAGE->set_course(),
2849 // the reasons is we need to be able to call require_login() at any time!!
2850 $course = $SITE;
2851 if ($cm) {
2852 throw new coding_exception('cm parameter in require_login() requires valid course parameter!');
2856 // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false.
2857 // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future
2858 // risk leading the user back to the AJAX request URL.
2859 if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) {
2860 $setwantsurltome = false;
2863 // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour.
2864 if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !$preventredirect && !empty($CFG->dbsessions)) {
2865 if ($setwantsurltome) {
2866 $SESSION->wantsurl = qualified_me();
2868 redirect(get_login_url());
2871 // If the user is not even logged in yet then make sure they are
2872 if (!isloggedin()) {
2873 if ($autologinguest and !empty($CFG->guestloginbutton) and !empty($CFG->autologinguests)) {
2874 if (!$guest = get_complete_user_data('id', $CFG->siteguest)) {
2875 // misconfigured site guest, just redirect to login page
2876 redirect(get_login_url());
2877 exit; // never reached
2879 $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang;
2880 complete_user_login($guest);
2881 $USER->autologinguest = true;
2882 $SESSION->lang = $lang;
2883 } else {
2884 //NOTE: $USER->site check was obsoleted by session test cookie,
2885 // $USER->confirmed test is in login/index.php
2886 if ($preventredirect) {
2887 throw new require_login_exception('You are not logged in');
2890 if ($setwantsurltome) {
2891 $SESSION->wantsurl = qualified_me();
2893 if (!empty($_SERVER['HTTP_REFERER'])) {
2894 $SESSION->fromurl = $_SERVER['HTTP_REFERER'];
2896 redirect(get_login_url());
2897 exit; // never reached
2901 // loginas as redirection if needed
2902 if ($course->id != SITEID and session_is_loggedinas()) {
2903 if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) {
2904 if ($USER->loginascontext->instanceid != $course->id) {
2905 print_error('loginasonecourse', '', $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid);
2910 // check whether the user should be changing password (but only if it is REALLY them)
2911 if (get_user_preferences('auth_forcepasswordchange') && !session_is_loggedinas()) {
2912 $userauth = get_auth_plugin($USER->auth);
2913 if ($userauth->can_change_password() and !$preventredirect) {
2914 if ($setwantsurltome) {
2915 $SESSION->wantsurl = qualified_me();
2917 if ($changeurl = $userauth->change_password_url()) {
2918 //use plugin custom url
2919 redirect($changeurl);
2920 } else {
2921 //use moodle internal method
2922 if (empty($CFG->loginhttps)) {
2923 redirect($CFG->wwwroot .'/login/change_password.php');
2924 } else {
2925 $wwwroot = str_replace('http:','https:', $CFG->wwwroot);
2926 redirect($wwwroot .'/login/change_password.php');
2929 } else {
2930 print_error('nopasswordchangeforced', 'auth');
2934 // Check that the user account is properly set up
2935 if (user_not_fully_set_up($USER)) {
2936 if ($preventredirect) {
2937 throw new require_login_exception('User not fully set-up');
2939 if ($setwantsurltome) {
2940 $SESSION->wantsurl = qualified_me();
2942 redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&amp;course='. SITEID);
2945 // Make sure the USER has a sesskey set up. Used for CSRF protection.
2946 sesskey();
2948 // Do not bother admins with any formalities
2949 if (is_siteadmin()) {
2950 //set accesstime or the user will appear offline which messes up messaging
2951 user_accesstime_log($course->id);
2952 return;
2955 // Check that the user has agreed to a site policy if there is one - do not test in case of admins
2956 if (!$USER->policyagreed and !is_siteadmin()) {
2957 if (!empty($CFG->sitepolicy) and !isguestuser()) {
2958 if ($preventredirect) {
2959 throw new require_login_exception('Policy not agreed');
2961 if ($setwantsurltome) {
2962 $SESSION->wantsurl = qualified_me();
2964 redirect($CFG->wwwroot .'/user/policy.php');
2965 } else if (!empty($CFG->sitepolicyguest) and isguestuser()) {
2966 if ($preventredirect) {
2967 throw new require_login_exception('Policy not agreed');
2969 if ($setwantsurltome) {
2970 $SESSION->wantsurl = qualified_me();
2972 redirect($CFG->wwwroot .'/user/policy.php');
2976 // Fetch the system context, the course context, and prefetch its child contexts
2977 $sysctx = context_system::instance();
2978 $coursecontext = context_course::instance($course->id, MUST_EXIST);
2979 if ($cm) {
2980 $cmcontext = context_module::instance($cm->id, MUST_EXIST);
2981 } else {
2982 $cmcontext = null;
2985 // If the site is currently under maintenance, then print a message
2986 if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:config', $sysctx)) {
2987 if ($preventredirect) {
2988 throw new require_login_exception('Maintenance in progress');
2991 print_maintenance_message();
2994 // make sure the course itself is not hidden
2995 if ($course->id == SITEID) {
2996 // frontpage can not be hidden
2997 } else {
2998 if (is_role_switched($course->id)) {
2999 // when switching roles ignore the hidden flag - user had to be in course to do the switch
3000 } else {
3001 if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
3002 // originally there was also test of parent category visibility,
3003 // BUT is was very slow in complex queries involving "my courses"
3004 // now it is also possible to simply hide all courses user is not enrolled in :-)
3005 if ($preventredirect) {
3006 throw new require_login_exception('Course is hidden');
3008 // We need to override the navigation URL as the course won't have
3009 // been added to the navigation and thus the navigation will mess up
3010 // when trying to find it.
3011 navigation_node::override_active_url(new moodle_url('/'));
3012 notice(get_string('coursehidden'), $CFG->wwwroot .'/');
3017 // is the user enrolled?
3018 if ($course->id == SITEID) {
3019 // everybody is enrolled on the frontpage
3021 } else {
3022 if (session_is_loggedinas()) {
3023 // Make sure the REAL person can access this course first
3024 $realuser = session_get_realuser();
3025 if (!is_enrolled($coursecontext, $realuser->id, '', true) and !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) {
3026 if ($preventredirect) {
3027 throw new require_login_exception('Invalid course login-as access');
3029 echo $OUTPUT->header();
3030 notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/');
3034 $access = false;
3036 if (is_role_switched($course->id)) {
3037 // ok, user had to be inside this course before the switch
3038 $access = true;
3040 } else if (is_viewing($coursecontext, $USER)) {
3041 // ok, no need to mess with enrol
3042 $access = true;
3044 } else {
3045 if (isset($USER->enrol['enrolled'][$course->id])) {
3046 if ($USER->enrol['enrolled'][$course->id] > time()) {
3047 $access = true;
3048 if (isset($USER->enrol['tempguest'][$course->id])) {
3049 unset($USER->enrol['tempguest'][$course->id]);
3050 remove_temp_course_roles($coursecontext);
3052 } else {
3053 //expired
3054 unset($USER->enrol['enrolled'][$course->id]);
3057 if (isset($USER->enrol['tempguest'][$course->id])) {
3058 if ($USER->enrol['tempguest'][$course->id] == 0) {
3059 $access = true;
3060 } else if ($USER->enrol['tempguest'][$course->id] > time()) {
3061 $access = true;
3062 } else {
3063 //expired
3064 unset($USER->enrol['tempguest'][$course->id]);
3065 remove_temp_course_roles($coursecontext);
3069 if ($access) {
3070 // cache ok
3071 } else {
3072 $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id);
3073 if ($until !== false) {
3074 // active participants may always access, a timestamp in the future, 0 (always) or false.
3075 if ($until == 0) {
3076 $until = ENROL_MAX_TIMESTAMP;
3078 $USER->enrol['enrolled'][$course->id] = $until;
3079 $access = true;
3081 } else {
3082 $instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder, id ASC');
3083 $enrols = enrol_get_plugins(true);
3084 // first ask all enabled enrol instances in course if they want to auto enrol user
3085 foreach($instances as $instance) {
3086 if (!isset($enrols[$instance->enrol])) {
3087 continue;
3089 // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false.
3090 $until = $enrols[$instance->enrol]->try_autoenrol($instance);
3091 if ($until !== false) {
3092 if ($until == 0) {
3093 $until = ENROL_MAX_TIMESTAMP;
3095 $USER->enrol['enrolled'][$course->id] = $until;
3096 $access = true;
3097 break;
3100 // if not enrolled yet try to gain temporary guest access
3101 if (!$access) {
3102 foreach($instances as $instance) {
3103 if (!isset($enrols[$instance->enrol])) {
3104 continue;
3106 // Get a duration for the guest access, a timestamp in the future or false.
3107 $until = $enrols[$instance->enrol]->try_guestaccess($instance);
3108 if ($until !== false and $until > time()) {
3109 $USER->enrol['tempguest'][$course->id] = $until;
3110 $access = true;
3111 break;
3119 if (!$access) {
3120 if ($preventredirect) {
3121 throw new require_login_exception('Not enrolled');
3123 if ($setwantsurltome) {
3124 $SESSION->wantsurl = qualified_me();
3126 redirect($CFG->wwwroot .'/enrol/index.php?id='. $course->id);
3130 // Check visibility of activity to current user; includes visible flag, groupmembersonly,
3131 // conditional availability, etc
3132 if ($cm && !$cm->uservisible) {
3133 if ($preventredirect) {
3134 throw new require_login_exception('Activity is hidden');
3136 if ($course->id != SITEID) {
3137 $url = new moodle_url('/course/view.php', array('id'=>$course->id));
3138 } else {
3139 $url = new moodle_url('/');
3141 redirect($url, get_string('activityiscurrentlyhidden'));
3144 // Finally access granted, update lastaccess times
3145 user_accesstime_log($course->id);
3150 * This function just makes sure a user is logged out.
3152 * @package core_access
3154 function require_logout() {
3155 global $USER;
3157 $params = $USER;
3159 if (isloggedin()) {
3160 add_to_log(SITEID, "user", "logout", "view.php?id=$USER->id&course=".SITEID, $USER->id, 0, $USER->id);
3162 $authsequence = get_enabled_auth_plugins(); // auths, in sequence
3163 foreach($authsequence as $authname) {
3164 $authplugin = get_auth_plugin($authname);
3165 $authplugin->prelogout_hook();
3169 events_trigger('user_logout', $params);
3170 session_get_instance()->terminate_current();
3171 unset($params);
3175 * Weaker version of require_login()
3177 * This is a weaker version of {@link require_login()} which only requires login
3178 * when called from within a course rather than the site page, unless
3179 * the forcelogin option is turned on.
3180 * @see require_login()
3182 * @package core_access
3183 * @category access
3185 * @param mixed $courseorid The course object or id in question
3186 * @param bool $autologinguest Allow autologin guests if that is wanted
3187 * @param object $cm Course activity module if known
3188 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
3189 * true. Used to avoid (=false) some scripts (file.php...) to set that variable,
3190 * in order to keep redirects working properly. MDL-14495
3191 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
3192 * @return void
3194 function require_course_login($courseorid, $autologinguest = true, $cm = NULL, $setwantsurltome = true, $preventredirect = false) {
3195 global $CFG, $PAGE, $SITE;
3196 $issite = (is_object($courseorid) and $courseorid->id == SITEID)
3197 or (!is_object($courseorid) and $courseorid == SITEID);
3198 if ($issite && !empty($cm) && !($cm instanceof cm_info)) {
3199 // note: nearly all pages call get_fast_modinfo anyway and it does not make any
3200 // db queries so this is not really a performance concern, however it is obviously
3201 // better if you use get_fast_modinfo to get the cm before calling this.
3202 if (is_object($courseorid)) {
3203 $course = $courseorid;
3204 } else {
3205 $course = clone($SITE);
3207 $modinfo = get_fast_modinfo($course);
3208 $cm = $modinfo->get_cm($cm->id);
3210 if (!empty($CFG->forcelogin)) {
3211 // login required for both SITE and courses
3212 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3214 } else if ($issite && !empty($cm) and !$cm->uservisible) {
3215 // always login for hidden activities
3216 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3218 } else if ($issite) {
3219 //login for SITE not required
3220 if ($cm and empty($cm->visible)) {
3221 // hidden activities are not accessible without login
3222 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3223 } else if ($cm and !empty($CFG->enablegroupmembersonly) and $cm->groupmembersonly) {
3224 // not-logged-in users do not have any group membership
3225 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3226 } else {
3227 // We still need to instatiate PAGE vars properly so that things
3228 // that rely on it like navigation function correctly.
3229 if (!empty($courseorid)) {
3230 if (is_object($courseorid)) {
3231 $course = $courseorid;
3232 } else {
3233 $course = clone($SITE);
3235 if ($cm) {
3236 if ($cm->course != $course->id) {
3237 throw new coding_exception('course and cm parameters in require_course_login() call do not match!!');
3239 $PAGE->set_cm($cm, $course);
3240 $PAGE->set_pagelayout('incourse');
3241 } else {
3242 $PAGE->set_course($course);
3244 } else {
3245 // If $PAGE->course, and hence $PAGE->context, have not already been set
3246 // up properly, set them up now.
3247 $PAGE->set_course($PAGE->course);
3249 //TODO: verify conditional activities here
3250 user_accesstime_log(SITEID);
3251 return;
3254 } else {
3255 // course login always required
3256 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3261 * Require key login. Function terminates with error if key not found or incorrect.
3263 * @global object
3264 * @global object
3265 * @global object
3266 * @global object
3267 * @uses NO_MOODLE_COOKIES
3268 * @uses PARAM_ALPHANUM
3269 * @param string $script unique script identifier
3270 * @param int $instance optional instance id
3271 * @return int Instance ID
3273 function require_user_key_login($script, $instance=null) {
3274 global $USER, $SESSION, $CFG, $DB;
3276 if (!NO_MOODLE_COOKIES) {
3277 print_error('sessioncookiesdisable');
3280 /// extra safety
3281 @session_write_close();
3283 $keyvalue = required_param('key', PARAM_ALPHANUM);
3285 if (!$key = $DB->get_record('user_private_key', array('script'=>$script, 'value'=>$keyvalue, 'instance'=>$instance))) {
3286 print_error('invalidkey');
3289 if (!empty($key->validuntil) and $key->validuntil < time()) {
3290 print_error('expiredkey');
3293 if ($key->iprestriction) {
3294 $remoteaddr = getremoteaddr(null);
3295 if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) {
3296 print_error('ipmismatch');
3300 if (!$user = $DB->get_record('user', array('id'=>$key->userid))) {
3301 print_error('invaliduserid');
3304 /// emulate normal session
3305 enrol_check_plugins($user);
3306 session_set_user($user);
3308 /// note we are not using normal login
3309 if (!defined('USER_KEY_LOGIN')) {
3310 define('USER_KEY_LOGIN', true);
3313 /// return instance id - it might be empty
3314 return $key->instance;
3318 * Creates a new private user access key.
3320 * @global object
3321 * @param string $script unique target identifier
3322 * @param int $userid
3323 * @param int $instance optional instance id
3324 * @param string $iprestriction optional ip restricted access
3325 * @param timestamp $validuntil key valid only until given data
3326 * @return string access key value
3328 function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3329 global $DB;
3331 $key = new stdClass();
3332 $key->script = $script;
3333 $key->userid = $userid;
3334 $key->instance = $instance;
3335 $key->iprestriction = $iprestriction;
3336 $key->validuntil = $validuntil;
3337 $key->timecreated = time();
3339 $key->value = md5($userid.'_'.time().random_string(40)); // something long and unique
3340 while ($DB->record_exists('user_private_key', array('value'=>$key->value))) {
3341 // must be unique
3342 $key->value = md5($userid.'_'.time().random_string(40));
3344 $DB->insert_record('user_private_key', $key);
3345 return $key->value;
3349 * Delete the user's new private user access keys for a particular script.
3351 * @global object
3352 * @param string $script unique target identifier
3353 * @param int $userid
3354 * @return void
3356 function delete_user_key($script,$userid) {
3357 global $DB;
3358 $DB->delete_records('user_private_key', array('script'=>$script, 'userid'=>$userid));
3362 * Gets a private user access key (and creates one if one doesn't exist).
3364 * @global object
3365 * @param string $script unique target identifier
3366 * @param int $userid
3367 * @param int $instance optional instance id
3368 * @param string $iprestriction optional ip restricted access
3369 * @param timestamp $validuntil key valid only until given data
3370 * @return string access key value
3372 function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3373 global $DB;
3375 if ($key = $DB->get_record('user_private_key', array('script'=>$script, 'userid'=>$userid,
3376 'instance'=>$instance, 'iprestriction'=>$iprestriction,
3377 'validuntil'=>$validuntil))) {
3378 return $key->value;
3379 } else {
3380 return create_user_key($script, $userid, $instance, $iprestriction, $validuntil);
3386 * Modify the user table by setting the currently logged in user's
3387 * last login to now.
3389 * @global object
3390 * @global object
3391 * @return bool Always returns true
3393 function update_user_login_times() {
3394 global $USER, $DB;
3396 if (isguestuser()) {
3397 // Do not update guest access times/ips for performance.
3398 return true;
3401 $now = time();
3403 $user = new stdClass();
3404 $user->id = $USER->id;
3406 // Make sure all users that logged in have some firstaccess.
3407 if ($USER->firstaccess == 0) {
3408 $USER->firstaccess = $user->firstaccess = $now;
3411 // Store the previous current as lastlogin.
3412 $USER->lastlogin = $user->lastlogin = $USER->currentlogin;
3414 $USER->currentlogin = $user->currentlogin = $now;
3416 // Function user_accesstime_log() may not update immediately, better do it here.
3417 $USER->lastaccess = $user->lastaccess = $now;
3418 $USER->lastip = $user->lastip = getremoteaddr();
3420 $DB->update_record('user', $user);
3421 return true;
3425 * Determines if a user has completed setting up their account.
3427 * @param user $user A {@link $USER} object to test for the existence of a valid name and email
3428 * @return bool
3430 function user_not_fully_set_up($user) {
3431 if (isguestuser($user)) {
3432 return false;
3434 return (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user));
3438 * Check whether the user has exceeded the bounce threshold
3440 * @global object
3441 * @global object
3442 * @param user $user A {@link $USER} object
3443 * @return bool true=>User has exceeded bounce threshold
3445 function over_bounce_threshold($user) {
3446 global $CFG, $DB;
3448 if (empty($CFG->handlebounces)) {
3449 return false;
3452 if (empty($user->id)) { /// No real (DB) user, nothing to do here.
3453 return false;
3456 // set sensible defaults
3457 if (empty($CFG->minbounces)) {
3458 $CFG->minbounces = 10;
3460 if (empty($CFG->bounceratio)) {
3461 $CFG->bounceratio = .20;
3463 $bouncecount = 0;
3464 $sendcount = 0;
3465 if ($bounce = $DB->get_record('user_preferences', array ('userid'=>$user->id, 'name'=>'email_bounce_count'))) {
3466 $bouncecount = $bounce->value;
3468 if ($send = $DB->get_record('user_preferences', array('userid'=>$user->id, 'name'=>'email_send_count'))) {
3469 $sendcount = $send->value;
3471 return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio);
3475 * Used to increment or reset email sent count
3477 * @global object
3478 * @param user $user object containing an id
3479 * @param bool $reset will reset the count to 0
3480 * @return void
3482 function set_send_count($user,$reset=false) {
3483 global $DB;
3485 if (empty($user->id)) { /// No real (DB) user, nothing to do here.
3486 return;
3489 if ($pref = $DB->get_record('user_preferences', array('userid'=>$user->id, 'name'=>'email_send_count'))) {
3490 $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3491 $DB->update_record('user_preferences', $pref);
3493 else if (!empty($reset)) { // if it's not there and we're resetting, don't bother.
3494 // make a new one
3495 $pref = new stdClass();
3496 $pref->name = 'email_send_count';
3497 $pref->value = 1;
3498 $pref->userid = $user->id;
3499 $DB->insert_record('user_preferences', $pref, false);
3504 * Increment or reset user's email bounce count
3506 * @global object
3507 * @param user $user object containing an id
3508 * @param bool $reset will reset the count to 0
3510 function set_bounce_count($user,$reset=false) {
3511 global $DB;
3513 if ($pref = $DB->get_record('user_preferences', array('userid'=>$user->id, 'name'=>'email_bounce_count'))) {
3514 $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3515 $DB->update_record('user_preferences', $pref);
3517 else if (!empty($reset)) { // if it's not there and we're resetting, don't bother.
3518 // make a new one
3519 $pref = new stdClass();
3520 $pref->name = 'email_bounce_count';
3521 $pref->value = 1;
3522 $pref->userid = $user->id;
3523 $DB->insert_record('user_preferences', $pref, false);
3528 * Determines if the currently logged in user is in editing mode.
3529 * Note: originally this function had $userid parameter - it was not usable anyway
3531 * @deprecated since Moodle 2.0 - use $PAGE->user_is_editing() instead.
3532 * @todo Deprecated function remove when ready
3534 * @global object
3535 * @uses DEBUG_DEVELOPER
3536 * @return bool
3538 function isediting() {
3539 global $PAGE;
3540 debugging('call to deprecated function isediting(). Please use $PAGE->user_is_editing() instead', DEBUG_DEVELOPER);
3541 return $PAGE->user_is_editing();
3545 * Determines if the logged in user is currently moving an activity
3547 * @global object
3548 * @param int $courseid The id of the course being tested
3549 * @return bool
3551 function ismoving($courseid) {
3552 global $USER;
3554 if (!empty($USER->activitycopy)) {
3555 return ($USER->activitycopycourse == $courseid);
3557 return false;
3561 * Returns a persons full name
3563 * Given an object containing firstname and lastname
3564 * values, this function returns a string with the
3565 * full name of the person.
3566 * The result may depend on system settings
3567 * or language. 'override' will force both names
3568 * to be used even if system settings specify one.
3570 * @global object
3571 * @global object
3572 * @param object $user A {@link $USER} object to get full name of
3573 * @param bool $override If true then the name will be first name followed by last name rather than adhering to fullnamedisplay setting.
3574 * @return string
3576 function fullname($user, $override=false) {
3577 global $CFG, $SESSION;
3579 if (!isset($user->firstname) and !isset($user->lastname)) {
3580 return '';
3583 if (!$override) {
3584 if (!empty($CFG->forcefirstname)) {
3585 $user->firstname = $CFG->forcefirstname;
3587 if (!empty($CFG->forcelastname)) {
3588 $user->lastname = $CFG->forcelastname;
3592 if (!empty($SESSION->fullnamedisplay)) {
3593 $CFG->fullnamedisplay = $SESSION->fullnamedisplay;
3596 if (!isset($CFG->fullnamedisplay) or $CFG->fullnamedisplay === 'firstname lastname') {
3597 return $user->firstname .' '. $user->lastname;
3599 } else if ($CFG->fullnamedisplay == 'lastname firstname') {
3600 return $user->lastname .' '. $user->firstname;
3602 } else if ($CFG->fullnamedisplay == 'firstname') {
3603 if ($override) {
3604 return get_string('fullnamedisplay', '', $user);
3605 } else {
3606 return $user->firstname;
3610 return get_string('fullnamedisplay', '', $user);
3614 * Checks if current user is shown any extra fields when listing users.
3615 * @param object $context Context
3616 * @param array $already Array of fields that we're going to show anyway
3617 * so don't bother listing them
3618 * @return array Array of field names from user table, not including anything
3619 * listed in $already
3621 function get_extra_user_fields($context, $already = array()) {
3622 global $CFG;
3624 // Only users with permission get the extra fields
3625 if (!has_capability('moodle/site:viewuseridentity', $context)) {
3626 return array();
3629 // Split showuseridentity on comma
3630 if (empty($CFG->showuseridentity)) {
3631 // Explode gives wrong result with empty string
3632 $extra = array();
3633 } else {
3634 $extra = explode(',', $CFG->showuseridentity);
3636 $renumber = false;
3637 foreach ($extra as $key => $field) {
3638 if (in_array($field, $already)) {
3639 unset($extra[$key]);
3640 $renumber = true;
3643 if ($renumber) {
3644 // For consistency, if entries are removed from array, renumber it
3645 // so they are numbered as you would expect
3646 $extra = array_merge($extra);
3648 return $extra;
3652 * If the current user is to be shown extra user fields when listing or
3653 * selecting users, returns a string suitable for including in an SQL select
3654 * clause to retrieve those fields.
3655 * @param object $context Context
3656 * @param string $alias Alias of user table, e.g. 'u' (default none)
3657 * @param string $prefix Prefix for field names using AS, e.g. 'u_' (default none)
3658 * @param array $already Array of fields that we're going to include anyway
3659 * so don't list them (default none)
3660 * @return string Partial SQL select clause, beginning with comma, for example
3661 * ',u.idnumber,u.department' unless it is blank
3663 function get_extra_user_fields_sql($context, $alias='', $prefix='',
3664 $already = array()) {
3665 $fields = get_extra_user_fields($context, $already);
3666 $result = '';
3667 // Add punctuation for alias
3668 if ($alias !== '') {
3669 $alias .= '.';
3671 foreach ($fields as $field) {
3672 $result .= ', ' . $alias . $field;
3673 if ($prefix) {
3674 $result .= ' AS ' . $prefix . $field;
3677 return $result;
3681 * Returns the display name of a field in the user table. Works for most fields
3682 * that are commonly displayed to users.
3683 * @param string $field Field name, e.g. 'phone1'
3684 * @return string Text description taken from language file, e.g. 'Phone number'
3686 function get_user_field_name($field) {
3687 // Some fields have language strings which are not the same as field name
3688 switch ($field) {
3689 case 'phone1' : return get_string('phone');
3690 case 'url' : return get_string('webpage');
3691 case 'icq' : return get_string('icqnumber');
3692 case 'skype' : return get_string('skypeid');
3693 case 'aim' : return get_string('aimid');
3694 case 'yahoo' : return get_string('yahooid');
3695 case 'msn' : return get_string('msnid');
3697 // Otherwise just use the same lang string
3698 return get_string($field);
3702 * Returns whether a given authentication plugin exists.
3704 * @global object
3705 * @param string $auth Form of authentication to check for. Defaults to the
3706 * global setting in {@link $CFG}.
3707 * @return boolean Whether the plugin is available.
3709 function exists_auth_plugin($auth) {
3710 global $CFG;
3712 if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) {
3713 return is_readable("{$CFG->dirroot}/auth/$auth/auth.php");
3715 return false;
3719 * Checks if a given plugin is in the list of enabled authentication plugins.
3721 * @param string $auth Authentication plugin.
3722 * @return boolean Whether the plugin is enabled.
3724 function is_enabled_auth($auth) {
3725 if (empty($auth)) {
3726 return false;
3729 $enabled = get_enabled_auth_plugins();
3731 return in_array($auth, $enabled);
3735 * Returns an authentication plugin instance.
3737 * @global object
3738 * @param string $auth name of authentication plugin
3739 * @return auth_plugin_base An instance of the required authentication plugin.
3741 function get_auth_plugin($auth) {
3742 global $CFG;
3744 // check the plugin exists first
3745 if (! exists_auth_plugin($auth)) {
3746 print_error('authpluginnotfound', 'debug', '', $auth);
3749 // return auth plugin instance
3750 require_once "{$CFG->dirroot}/auth/$auth/auth.php";
3751 $class = "auth_plugin_$auth";
3752 return new $class;
3756 * Returns array of active auth plugins.
3758 * @param bool $fix fix $CFG->auth if needed
3759 * @return array
3761 function get_enabled_auth_plugins($fix=false) {
3762 global $CFG;
3764 $default = array('manual', 'nologin');
3766 if (empty($CFG->auth)) {
3767 $auths = array();
3768 } else {
3769 $auths = explode(',', $CFG->auth);
3772 if ($fix) {
3773 $auths = array_unique($auths);
3774 foreach($auths as $k=>$authname) {
3775 if (!exists_auth_plugin($authname) or in_array($authname, $default)) {
3776 unset($auths[$k]);
3779 $newconfig = implode(',', $auths);
3780 if (!isset($CFG->auth) or $newconfig != $CFG->auth) {
3781 set_config('auth', $newconfig);
3785 return (array_merge($default, $auths));
3789 * Returns true if an internal authentication method is being used.
3790 * if method not specified then, global default is assumed
3792 * @param string $auth Form of authentication required
3793 * @return bool
3795 function is_internal_auth($auth) {
3796 $authplugin = get_auth_plugin($auth); // throws error if bad $auth
3797 return $authplugin->is_internal();
3801 * Returns true if the user is a 'restored' one
3803 * Used in the login process to inform the user
3804 * and allow him/her to reset the password
3806 * @uses $CFG
3807 * @uses $DB
3808 * @param string $username username to be checked
3809 * @return bool
3811 function is_restored_user($username) {
3812 global $CFG, $DB;
3814 return $DB->record_exists('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id, 'password'=>'restored'));
3818 * Returns an array of user fields
3820 * @return array User field/column names
3822 function get_user_fieldnames() {
3823 global $DB;
3825 $fieldarray = $DB->get_columns('user');
3826 unset($fieldarray['id']);
3827 $fieldarray = array_keys($fieldarray);
3829 return $fieldarray;
3833 * Creates a bare-bones user record
3835 * @todo Outline auth types and provide code example
3837 * @param string $username New user's username to add to record
3838 * @param string $password New user's password to add to record
3839 * @param string $auth Form of authentication required
3840 * @return stdClass A complete user object
3842 function create_user_record($username, $password, $auth = 'manual') {
3843 global $CFG, $DB;
3845 //just in case check text case
3846 $username = trim(textlib::strtolower($username));
3848 $authplugin = get_auth_plugin($auth);
3850 $newuser = new stdClass();
3852 if ($newinfo = $authplugin->get_userinfo($username)) {
3853 $newinfo = truncate_userinfo($newinfo);
3854 foreach ($newinfo as $key => $value){
3855 $newuser->$key = $value;
3859 if (!empty($newuser->email)) {
3860 if (email_is_not_allowed($newuser->email)) {
3861 unset($newuser->email);
3865 if (!isset($newuser->city)) {
3866 $newuser->city = '';
3869 $newuser->auth = $auth;
3870 $newuser->username = $username;
3872 // fix for MDL-8480
3873 // user CFG lang for user if $newuser->lang is empty
3874 // or $user->lang is not an installed language
3875 if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) {
3876 $newuser->lang = $CFG->lang;
3878 $newuser->confirmed = 1;
3879 $newuser->lastip = getremoteaddr();
3880 $newuser->timecreated = time();
3881 $newuser->timemodified = $newuser->timecreated;
3882 $newuser->mnethostid = $CFG->mnet_localhost_id;
3884 $newuser->id = $DB->insert_record('user', $newuser);
3885 $user = get_complete_user_data('id', $newuser->id);
3886 if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})){
3887 set_user_preference('auth_forcepasswordchange', 1, $user);
3889 // Set the password.
3890 update_internal_user_password($user, $password);
3892 // fetch full user record for the event, the complete user data contains too much info
3893 // and we want to be consistent with other places that trigger this event
3894 events_trigger('user_created', $DB->get_record('user', array('id'=>$user->id)));
3896 return $user;
3900 * Will update a local user record from an external source.
3901 * (MNET users can not be updated using this method!)
3903 * @param string $username user's username to update the record
3904 * @return stdClass A complete user object
3906 function update_user_record($username) {
3907 global $DB, $CFG;
3909 $username = trim(textlib::strtolower($username)); /// just in case check text case
3911 $oldinfo = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id), '*', MUST_EXIST);
3912 $newuser = array();
3913 $userauth = get_auth_plugin($oldinfo->auth);
3915 if ($newinfo = $userauth->get_userinfo($username)) {
3916 $newinfo = truncate_userinfo($newinfo);
3917 foreach ($newinfo as $key => $value){
3918 $key = strtolower($key);
3919 if (!property_exists($oldinfo, $key) or $key === 'username' or $key === 'id'
3920 or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') {
3921 // unknown or must not be changed
3922 continue;
3924 $confval = $userauth->config->{'field_updatelocal_' . $key};
3925 $lockval = $userauth->config->{'field_lock_' . $key};
3926 if (empty($confval) || empty($lockval)) {
3927 continue;
3929 if ($confval === 'onlogin') {
3930 // MDL-4207 Don't overwrite modified user profile values with
3931 // empty LDAP values when 'unlocked if empty' is set. The purpose
3932 // of the setting 'unlocked if empty' is to allow the user to fill
3933 // in a value for the selected field _if LDAP is giving
3934 // nothing_ for this field. Thus it makes sense to let this value
3935 // stand in until LDAP is giving a value for this field.
3936 if (!(empty($value) && $lockval === 'unlockedifempty')) {
3937 if ((string)$oldinfo->$key !== (string)$value) {
3938 $newuser[$key] = (string)$value;
3943 if ($newuser) {
3944 $newuser['id'] = $oldinfo->id;
3945 $newuser['timemodified'] = time();
3946 $DB->update_record('user', $newuser);
3947 // fetch full user record for the event, the complete user data contains too much info
3948 // and we want to be consistent with other places that trigger this event
3949 events_trigger('user_updated', $DB->get_record('user', array('id'=>$oldinfo->id)));
3953 return get_complete_user_data('id', $oldinfo->id);
3957 * Will truncate userinfo as it comes from auth_get_userinfo (from external auth)
3958 * which may have large fields
3960 * @todo Add vartype handling to ensure $info is an array
3962 * @param array $info Array of user properties to truncate if needed
3963 * @return array The now truncated information that was passed in
3965 function truncate_userinfo($info) {
3966 // define the limits
3967 $limit = array(
3968 'username' => 100,
3969 'idnumber' => 255,
3970 'firstname' => 100,
3971 'lastname' => 100,
3972 'email' => 100,
3973 'icq' => 15,
3974 'phone1' => 20,
3975 'phone2' => 20,
3976 'institution' => 40,
3977 'department' => 30,
3978 'address' => 70,
3979 'city' => 120,
3980 'country' => 2,
3981 'url' => 255,
3984 // apply where needed
3985 foreach (array_keys($info) as $key) {
3986 if (!empty($limit[$key])) {
3987 $info[$key] = trim(textlib::substr($info[$key],0, $limit[$key]));
3991 return $info;
3995 * Marks user deleted in internal user database and notifies the auth plugin.
3996 * Also unenrols user from all roles and does other cleanup.
3998 * Any plugin that needs to purge user data should register the 'user_deleted' event.
4000 * @param stdClass $user full user object before delete
4001 * @return boolean success
4002 * @throws coding_exception if invalid $user parameter detected
4004 function delete_user(stdClass $user) {
4005 global $CFG, $DB;
4006 require_once($CFG->libdir.'/grouplib.php');
4007 require_once($CFG->libdir.'/gradelib.php');
4008 require_once($CFG->dirroot.'/message/lib.php');
4009 require_once($CFG->dirroot.'/tag/lib.php');
4011 // Make sure nobody sends bogus record type as parameter.
4012 if (!property_exists($user, 'id') or !property_exists($user, 'username')) {
4013 throw new coding_exception('Invalid $user parameter in delete_user() detected');
4016 // Better not trust the parameter and fetch the latest info,
4017 // this will be very expensive anyway.
4018 if (!$user = $DB->get_record('user', array('id'=>$user->id))) {
4019 debugging('Attempt to delete unknown user account.');
4020 return false;
4023 // There must be always exactly one guest record,
4024 // originally the guest account was identified by username only,
4025 // now we use $CFG->siteguest for performance reasons.
4026 if ($user->username === 'guest' or isguestuser($user)) {
4027 debugging('Guest user account can not be deleted.');
4028 return false;
4031 // Admin can be theoretically from different auth plugin,
4032 // but we want to prevent deletion of internal accoutns only,
4033 // if anything goes wrong ppl may force somebody to be admin via
4034 // config.php setting $CFG->siteadmins.
4035 if ($user->auth === 'manual' and is_siteadmin($user)) {
4036 debugging('Local administrator accounts can not be deleted.');
4037 return false;
4040 // delete all grades - backup is kept in grade_grades_history table
4041 grade_user_delete($user->id);
4043 //move unread messages from this user to read
4044 message_move_userfrom_unread2read($user->id);
4046 // TODO: remove from cohorts using standard API here
4048 // remove user tags
4049 tag_set('user', $user->id, array());
4051 // unconditionally unenrol from all courses
4052 enrol_user_delete($user);
4054 // unenrol from all roles in all contexts
4055 role_unassign_all(array('userid'=>$user->id)); // this might be slow but it is really needed - modules might do some extra cleanup!
4057 //now do a brute force cleanup
4059 // remove from all cohorts
4060 $DB->delete_records('cohort_members', array('userid'=>$user->id));
4062 // remove from all groups
4063 $DB->delete_records('groups_members', array('userid'=>$user->id));
4065 // brute force unenrol from all courses
4066 $DB->delete_records('user_enrolments', array('userid'=>$user->id));
4068 // purge user preferences
4069 $DB->delete_records('user_preferences', array('userid'=>$user->id));
4071 // purge user extra profile info
4072 $DB->delete_records('user_info_data', array('userid'=>$user->id));
4074 // last course access not necessary either
4075 $DB->delete_records('user_lastaccess', array('userid'=>$user->id));
4077 // remove all user tokens
4078 $DB->delete_records('external_tokens', array('userid'=>$user->id));
4080 // unauthorise the user for all services
4081 $DB->delete_records('external_services_users', array('userid'=>$user->id));
4083 // Remove users private keys.
4084 $DB->delete_records('user_private_key', array('userid' => $user->id));
4086 // force logout - may fail if file based sessions used, sorry
4087 session_kill_user($user->id);
4089 // now do a final accesslib cleanup - removes all role assignments in user context and context itself
4090 delete_context(CONTEXT_USER, $user->id);
4092 // workaround for bulk deletes of users with the same email address
4093 $delname = "$user->email.".time();
4094 while ($DB->record_exists('user', array('username'=>$delname))) { // no need to use mnethostid here
4095 $delname++;
4098 // mark internal user record as "deleted"
4099 $updateuser = new stdClass();
4100 $updateuser->id = $user->id;
4101 $updateuser->deleted = 1;
4102 $updateuser->username = $delname; // Remember it just in case
4103 $updateuser->email = md5($user->username);// Store hash of username, useful importing/restoring users
4104 $updateuser->idnumber = ''; // Clear this field to free it up
4105 $updateuser->picture = 0;
4106 $updateuser->timemodified = time();
4108 $DB->update_record('user', $updateuser);
4109 // Add this action to log
4110 add_to_log(SITEID, 'user', 'delete', "view.php?id=$user->id", $user->firstname.' '.$user->lastname);
4113 // We will update the user's timemodified, as it will be passed to the user_deleted event, which
4114 // should know about this updated property persisted to the user's table.
4115 $user->timemodified = $updateuser->timemodified;
4117 // notify auth plugin - do not block the delete even when plugin fails
4118 $authplugin = get_auth_plugin($user->auth);
4119 $authplugin->user_delete($user);
4121 // any plugin that needs to cleanup should register this event
4122 events_trigger('user_deleted', $user);
4124 return true;
4128 * Retrieve the guest user object
4130 * @global object
4131 * @global object
4132 * @return user A {@link $USER} object
4134 function guest_user() {
4135 global $CFG, $DB;
4137 if ($newuser = $DB->get_record('user', array('id'=>$CFG->siteguest))) {
4138 $newuser->confirmed = 1;
4139 $newuser->lang = $CFG->lang;
4140 $newuser->lastip = getremoteaddr();
4143 return $newuser;
4147 * Authenticates a user against the chosen authentication mechanism
4149 * Given a username and password, this function looks them
4150 * up using the currently selected authentication mechanism,
4151 * and if the authentication is successful, it returns a
4152 * valid $user object from the 'user' table.
4154 * Uses auth_ functions from the currently active auth module
4156 * After authenticate_user_login() returns success, you will need to
4157 * log that the user has logged in, and call complete_user_login() to set
4158 * the session up.
4160 * Note: this function works only with non-mnet accounts!
4162 * @param string $username User's username
4163 * @param string $password User's password
4164 * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO
4165 * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists)
4166 * @return stdClass|false A {@link $USER} object or false if error
4168 function authenticate_user_login($username, $password, $ignorelockout=false, &$failurereason=null) {
4169 global $CFG, $DB;
4170 require_once("$CFG->libdir/authlib.php");
4172 $authsenabled = get_enabled_auth_plugins();
4174 if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) {
4175 $auth = empty($user->auth) ? 'manual' : $user->auth; // use manual if auth not set
4176 if (!empty($user->suspended)) {
4177 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4178 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4179 $failurereason = AUTH_LOGIN_SUSPENDED;
4180 return false;
4182 if ($auth=='nologin' or !is_enabled_auth($auth)) {
4183 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4184 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Disabled Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4185 $failurereason = AUTH_LOGIN_SUSPENDED; // Legacy way to suspend user.
4186 return false;
4188 $auths = array($auth);
4190 } else {
4191 // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user().
4192 if ($DB->get_field('user', 'id', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id, 'deleted'=>1))) {
4193 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Deleted Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4194 $failurereason = AUTH_LOGIN_NOUSER;
4195 return false;
4198 // Do not try to authenticate non-existent accounts when user creation is not disabled.
4199 if (!empty($CFG->authpreventaccountcreation)) {
4200 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4201 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Unknown user, can not create new accounts: $username ".$_SERVER['HTTP_USER_AGENT']);
4202 $failurereason = AUTH_LOGIN_NOUSER;
4203 return false;
4206 // User does not exist
4207 $auths = $authsenabled;
4208 $user = new stdClass();
4209 $user->id = 0;
4212 if ($ignorelockout) {
4213 // Some other mechanism protects against brute force password guessing,
4214 // for example login form might include reCAPTCHA or this function
4215 // is called from a SSO script.
4217 } else if ($user->id) {
4218 // Verify login lockout after other ways that may prevent user login.
4219 if (login_is_lockedout($user)) {
4220 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4221 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Login lockout: $username ".$_SERVER['HTTP_USER_AGENT']);
4222 $failurereason = AUTH_LOGIN_LOCKOUT;
4223 return false;
4226 } else {
4227 // We can not lockout non-existing accounts.
4230 foreach ($auths as $auth) {
4231 $authplugin = get_auth_plugin($auth);
4233 // on auth fail fall through to the next plugin
4234 if (!$authplugin->user_login($username, $password)) {
4235 continue;
4238 // successful authentication
4239 if ($user->id) { // User already exists in database
4240 if (empty($user->auth)) { // For some reason auth isn't set yet
4241 $DB->set_field('user', 'auth', $auth, array('username'=>$username));
4242 $user->auth = $auth;
4245 // If the existing hash is using an out-of-date algorithm (or the
4246 // legacy md5 algorithm), then we should update to the current
4247 // hash algorithm while we have access to the user's password.
4248 update_internal_user_password($user, $password);
4250 if ($authplugin->is_synchronised_with_external()) { // update user record from external DB
4251 $user = update_user_record($username);
4253 } else {
4254 // Create account, we verified above that user creation is allowed.
4255 $user = create_user_record($username, $password, $auth);
4258 $authplugin->sync_roles($user);
4260 foreach ($authsenabled as $hau) {
4261 $hauth = get_auth_plugin($hau);
4262 $hauth->user_authenticated_hook($user, $username, $password);
4265 if (empty($user->id)) {
4266 $failurereason = AUTH_LOGIN_NOUSER;
4267 return false;
4270 if (!empty($user->suspended)) {
4271 // just in case some auth plugin suspended account
4272 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4273 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4274 $failurereason = AUTH_LOGIN_SUSPENDED;
4275 return false;
4278 login_attempt_valid($user);
4279 $failurereason = AUTH_LOGIN_OK;
4280 return $user;
4283 // failed if all the plugins have failed
4284 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4285 if (debugging('', DEBUG_ALL)) {
4286 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Failed Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4289 if ($user->id) {
4290 login_attempt_failed($user);
4291 $failurereason = AUTH_LOGIN_FAILED;
4292 } else {
4293 $failurereason = AUTH_LOGIN_NOUSER;
4296 return false;
4300 * Call to complete the user login process after authenticate_user_login()
4301 * has succeeded. It will setup the $USER variable and other required bits
4302 * and pieces.
4304 * NOTE:
4305 * - It will NOT log anything -- up to the caller to decide what to log.
4306 * - this function does not set any cookies any more!
4308 * @param object $user
4309 * @return object A {@link $USER} object - BC only, do not use
4311 function complete_user_login($user) {
4312 global $CFG, $USER;
4314 // regenerate session id and delete old session,
4315 // this helps prevent session fixation attacks from the same domain
4316 session_regenerate_id(true);
4318 // let enrol plugins deal with new enrolments if necessary
4319 enrol_check_plugins($user);
4321 // check enrolments, load caps and setup $USER object
4322 session_set_user($user);
4324 // reload preferences from DB
4325 unset($USER->preference);
4326 check_user_preferences_loaded($USER);
4328 // update login times
4329 update_user_login_times();
4331 // extra session prefs init
4332 set_login_session_preferences();
4334 if (isguestuser()) {
4335 // no need to continue when user is THE guest
4336 return $USER;
4339 /// Select password change url
4340 $userauth = get_auth_plugin($USER->auth);
4342 /// check whether the user should be changing password
4343 if (get_user_preferences('auth_forcepasswordchange', false)){
4344 if ($userauth->can_change_password()) {
4345 if ($changeurl = $userauth->change_password_url()) {
4346 redirect($changeurl);
4347 } else {
4348 redirect($CFG->httpswwwroot.'/login/change_password.php');
4350 } else {
4351 print_error('nopasswordchangeforced', 'auth');
4354 return $USER;
4358 * Check a password hash to see if it was hashed using the
4359 * legacy hash algorithm (md5).
4361 * @param string $password String to check.
4362 * @return boolean True if the $password matches the format of an md5 sum.
4364 function password_is_legacy_hash($password) {
4365 return (bool) preg_match('/^[0-9a-f]{32}$/', $password);
4369 * Checks whether the password compatibility library will work with the current
4370 * version of PHP. This cannot be done using PHP version numbers since the fix
4371 * has been backported to earlier versions in some distributions.
4373 * See https://github.com/ircmaxell/password_compat/issues/10 for
4374 * more details.
4376 * @return bool True if the library is NOT supported.
4378 function password_compat_not_supported() {
4380 $hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG';
4382 // Create a one off application cache to store bcrypt support status as
4383 // the support status doesn't change and crypt() is slow.
4384 $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'password_compat');
4386 if (!$bcryptsupport = $cache->get('bcryptsupport')) {
4387 $test = crypt('password', $hash);
4388 // Cache string instead of boolean to avoid MDL-37472.
4389 if ($test == $hash) {
4390 $bcryptsupport = 'supported';
4391 } else {
4392 $bcryptsupport = 'not supported';
4394 $cache->set('bcryptsupport', $bcryptsupport);
4397 // Return true if bcrypt *not* supported.
4398 return ($bcryptsupport !== 'supported');
4402 * Compare password against hash stored in user object to determine if it is valid.
4404 * If necessary it also updates the stored hash to the current format.
4406 * @param stdClass $user (Password property may be updated).
4407 * @param string $password Plain text password.
4408 * @return bool True if password is valid.
4410 function validate_internal_user_password($user, $password) {
4411 global $CFG;
4412 require_once($CFG->libdir.'/password_compat/lib/password.php');
4414 if ($user->password === AUTH_PASSWORD_NOT_CACHED) {
4415 // Internal password is not used at all, it can not validate.
4416 return false;
4419 // If hash isn't a legacy (md5) hash, validate using the library function.
4420 if (!password_is_legacy_hash($user->password)) {
4421 return password_verify($password, $user->password);
4424 // Otherwise we need to check for a legacy (md5) hash instead. If the hash
4425 // is valid we can then update it to the new algorithm.
4427 $sitesalt = isset($CFG->passwordsaltmain) ? $CFG->passwordsaltmain : '';
4428 $validated = false;
4430 if ($user->password === md5($password.$sitesalt)
4431 or $user->password === md5($password)
4432 or $user->password === md5(addslashes($password).$sitesalt)
4433 or $user->password === md5(addslashes($password))) {
4434 // note: we are intentionally using the addslashes() here because we
4435 // need to accept old password hashes of passwords with magic quotes
4436 $validated = true;
4438 } else {
4439 for ($i=1; $i<=20; $i++) { //20 alternative salts should be enough, right?
4440 $alt = 'passwordsaltalt'.$i;
4441 if (!empty($CFG->$alt)) {
4442 if ($user->password === md5($password.$CFG->$alt) or $user->password === md5(addslashes($password).$CFG->$alt)) {
4443 $validated = true;
4444 break;
4450 if ($validated) {
4451 // If the password matches the existing md5 hash, update to the
4452 // current hash algorithm while we have access to the user's password.
4453 update_internal_user_password($user, $password);
4456 return $validated;
4460 * Calculate hash for a plain text password.
4462 * @param string $password Plain text password to be hashed.
4463 * @param bool $fasthash If true, use a low cost factor when generating the hash
4464 * This is much faster to generate but makes the hash
4465 * less secure. It is used when lots of hashes need to
4466 * be generated quickly.
4467 * @return string The hashed password.
4469 * @throws moodle_exception If a problem occurs while generating the hash.
4471 function hash_internal_user_password($password, $fasthash = false) {
4472 global $CFG;
4473 require_once($CFG->libdir.'/password_compat/lib/password.php');
4475 // Use the legacy hashing algorithm (md5) if PHP is not new enough
4476 // to support bcrypt properly
4477 if (password_compat_not_supported()) {
4478 if (isset($CFG->passwordsaltmain)) {
4479 return md5($password.$CFG->passwordsaltmain);
4480 } else {
4481 return md5($password);
4485 // Set the cost factor to 4 for fast hashing, otherwise use default cost.
4486 $options = ($fasthash) ? array('cost' => 4) : array();
4488 $generatedhash = password_hash($password, PASSWORD_DEFAULT, $options);
4490 if ($generatedhash === false || $generatedhash === null) {
4491 throw new moodle_exception('Failed to generate password hash.');
4494 return $generatedhash;
4498 * Update password hash in user object (if necessary).
4500 * The password is updated if:
4501 * 1. The password has changed (the hash of $user->password is different
4502 * to the hash of $password).
4503 * 2. The existing hash is using an out-of-date algorithm (or the legacy
4504 * md5 algorithm).
4506 * Updating the password will modify the $user object and the database
4507 * record to use the current hashing algorithm.
4509 * @param stdClass $user User object (password property may be updated).
4510 * @param string $password Plain text password.
4511 * @return bool Always returns true.
4513 function update_internal_user_password($user, $password) {
4514 global $CFG, $DB;
4515 require_once($CFG->libdir.'/password_compat/lib/password.php');
4517 // Use the legacy hashing algorithm (md5) if PHP doesn't support
4518 // bcrypt properly.
4519 $legacyhash = password_compat_not_supported();
4521 // Figure out what the hashed password should be.
4522 $authplugin = get_auth_plugin($user->auth);
4523 if ($authplugin->prevent_local_passwords()) {
4524 $hashedpassword = AUTH_PASSWORD_NOT_CACHED;
4525 } else {
4526 $hashedpassword = hash_internal_user_password($password);
4529 if ($legacyhash) {
4530 $passwordchanged = ($user->password !== $hashedpassword);
4531 $algorithmchanged = false;
4532 } else {
4533 // If verification fails then it means the password has changed.
4534 $passwordchanged = !password_verify($password, $user->password);
4535 $algorithmchanged = password_needs_rehash($user->password, PASSWORD_DEFAULT);
4538 if ($passwordchanged || $algorithmchanged) {
4539 $DB->set_field('user', 'password', $hashedpassword, array('id'=>$user->id));
4540 $user->password = $hashedpassword;
4543 return true;
4547 * Get a complete user record, which includes all the info
4548 * in the user record.
4550 * Intended for setting as $USER session variable
4552 * @param string $field The user field to be checked for a given value.
4553 * @param string $value The value to match for $field.
4554 * @param int $mnethostid
4555 * @return mixed False, or A {@link $USER} object.
4557 function get_complete_user_data($field, $value, $mnethostid = null) {
4558 global $CFG, $DB;
4560 if (!$field || !$value) {
4561 return false;
4564 /// Build the WHERE clause for an SQL query
4565 $params = array('fieldval'=>$value);
4566 $constraints = "$field = :fieldval AND deleted <> 1";
4568 // If we are loading user data based on anything other than id,
4569 // we must also restrict our search based on mnet host.
4570 if ($field != 'id') {
4571 if (empty($mnethostid)) {
4572 // if empty, we restrict to local users
4573 $mnethostid = $CFG->mnet_localhost_id;
4576 if (!empty($mnethostid)) {
4577 $params['mnethostid'] = $mnethostid;
4578 $constraints .= " AND mnethostid = :mnethostid";
4581 /// Get all the basic user data
4583 if (! $user = $DB->get_record_select('user', $constraints, $params)) {
4584 return false;
4587 /// Get various settings and preferences
4589 // preload preference cache
4590 check_user_preferences_loaded($user);
4592 // load course enrolment related stuff
4593 $user->lastcourseaccess = array(); // during last session
4594 $user->currentcourseaccess = array(); // during current session
4595 if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid'=>$user->id))) {
4596 foreach ($lastaccesses as $lastaccess) {
4597 $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess;
4601 $sql = "SELECT g.id, g.courseid
4602 FROM {groups} g, {groups_members} gm
4603 WHERE gm.groupid=g.id AND gm.userid=?";
4605 // this is a special hack to speedup calendar display
4606 $user->groupmember = array();
4607 if (!isguestuser($user)) {
4608 if ($groups = $DB->get_records_sql($sql, array($user->id))) {
4609 foreach ($groups as $group) {
4610 if (!array_key_exists($group->courseid, $user->groupmember)) {
4611 $user->groupmember[$group->courseid] = array();
4613 $user->groupmember[$group->courseid][$group->id] = $group->id;
4618 /// Add the custom profile fields to the user record
4619 $user->profile = array();
4620 if (!isguestuser($user)) {
4621 require_once($CFG->dirroot.'/user/profile/lib.php');
4622 profile_load_custom_fields($user);
4625 /// Rewrite some variables if necessary
4626 if (!empty($user->description)) {
4627 $user->description = true; // No need to cart all of it around
4629 if (isguestuser($user)) {
4630 $user->lang = $CFG->lang; // Guest language always same as site
4631 $user->firstname = get_string('guestuser'); // Name always in current language
4632 $user->lastname = ' ';
4635 return $user;
4639 * Validate a password against the configured password policy
4641 * @global object
4642 * @param string $password the password to be checked against the password policy
4643 * @param string $errmsg the error message to display when the password doesn't comply with the policy.
4644 * @return bool true if the password is valid according to the policy. false otherwise.
4646 function check_password_policy($password, &$errmsg) {
4647 global $CFG;
4649 if (empty($CFG->passwordpolicy)) {
4650 return true;
4653 $errmsg = '';
4654 if (textlib::strlen($password) < $CFG->minpasswordlength) {
4655 $errmsg .= '<div>'. get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength) .'</div>';
4658 if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) {
4659 $errmsg .= '<div>'. get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits) .'</div>';
4662 if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) {
4663 $errmsg .= '<div>'. get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower) .'</div>';
4666 if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) {
4667 $errmsg .= '<div>'. get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper) .'</div>';
4670 if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) {
4671 $errmsg .= '<div>'. get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum) .'</div>';
4673 if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) {
4674 $errmsg .= '<div>'. get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars) .'</div>';
4677 if ($errmsg == '') {
4678 return true;
4679 } else {
4680 return false;
4686 * When logging in, this function is run to set certain preferences
4687 * for the current SESSION
4689 * @global object
4690 * @global object
4692 function set_login_session_preferences() {
4693 global $SESSION, $CFG;
4695 $SESSION->justloggedin = true;
4697 unset($SESSION->lang);
4702 * Delete a course, including all related data from the database,
4703 * and any associated files.
4705 * @global object
4706 * @global object
4707 * @param mixed $courseorid The id of the course or course object to delete.
4708 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4709 * @return bool true if all the removals succeeded. false if there were any failures. If this
4710 * method returns false, some of the removals will probably have succeeded, and others
4711 * failed, but you have no way of knowing which.
4713 function delete_course($courseorid, $showfeedback = true) {
4714 global $DB;
4716 if (is_object($courseorid)) {
4717 $courseid = $courseorid->id;
4718 $course = $courseorid;
4719 } else {
4720 $courseid = $courseorid;
4721 if (!$course = $DB->get_record('course', array('id'=>$courseid))) {
4722 return false;
4725 $context = context_course::instance($courseid);
4727 // frontpage course can not be deleted!!
4728 if ($courseid == SITEID) {
4729 return false;
4732 // make the course completely empty
4733 remove_course_contents($courseid, $showfeedback);
4735 // delete the course and related context instance
4736 delete_context(CONTEXT_COURSE, $courseid);
4738 // We will update the course's timemodified, as it will be passed to the course_deleted event,
4739 // which should know about this updated property, as this event is meant to pass the full course record
4740 $course->timemodified = time();
4742 $DB->delete_records("course", array("id" => $courseid));
4743 $DB->delete_records("course_format_options", array("courseid" => $courseid));
4745 //trigger events
4746 $course->context = $context; // you can not fetch context in the event because it was already deleted
4747 events_trigger('course_deleted', $course);
4749 return true;
4753 * Clear a course out completely, deleting all content
4754 * but don't delete the course itself.
4755 * This function does not verify any permissions.
4757 * Please note this function also deletes all user enrolments,
4758 * enrolment instances and role assignments by default.
4760 * $options:
4761 * - 'keep_roles_and_enrolments' - false by default
4762 * - 'keep_groups_and_groupings' - false by default
4764 * @param int $courseid The id of the course that is being deleted
4765 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4766 * @param array $options extra options
4767 * @return bool true if all the removals succeeded. false if there were any failures. If this
4768 * method returns false, some of the removals will probably have succeeded, and others
4769 * failed, but you have no way of knowing which.
4771 function remove_course_contents($courseid, $showfeedback = true, array $options = null) {
4772 global $CFG, $DB, $OUTPUT;
4773 require_once($CFG->libdir.'/badgeslib.php');
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 // Handle course badges.
4783 badges_handle_course_deletion($courseid);
4785 // NOTE: these concatenated strings are suboptimal, but it is just extra info...
4786 $strdeleted = get_string('deleted').' - ';
4788 // Some crazy wishlist of stuff we should skip during purging of course content
4789 $options = (array)$options;
4791 $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST);
4792 $coursecontext = context_course::instance($courseid);
4793 $fs = get_file_storage();
4795 // Delete course completion information, this has to be done before grades and enrols
4796 $cc = new completion_info($course);
4797 $cc->clear_criteria();
4798 if ($showfeedback) {
4799 echo $OUTPUT->notification($strdeleted.get_string('completion', 'completion'), 'notifysuccess');
4802 // Remove all data from gradebook - this needs to be done before course modules
4803 // because while deleting this information, the system may need to reference
4804 // the course modules that own the grades.
4805 remove_course_grades($courseid, $showfeedback);
4806 remove_grade_letters($coursecontext, $showfeedback);
4808 // Delete course blocks in any all child contexts,
4809 // they may depend on modules so delete them first
4810 $childcontexts = $coursecontext->get_child_contexts(); // returns all subcontexts since 2.2
4811 foreach ($childcontexts as $childcontext) {
4812 blocks_delete_all_for_context($childcontext->id);
4814 unset($childcontexts);
4815 blocks_delete_all_for_context($coursecontext->id);
4816 if ($showfeedback) {
4817 echo $OUTPUT->notification($strdeleted.get_string('type_block_plural', 'plugin'), 'notifysuccess');
4820 // Delete every instance of every module,
4821 // this has to be done before deleting of course level stuff
4822 $locations = get_plugin_list('mod');
4823 foreach ($locations as $modname=>$moddir) {
4824 if ($modname === 'NEWMODULE') {
4825 continue;
4827 if ($module = $DB->get_record('modules', array('name'=>$modname))) {
4828 include_once("$moddir/lib.php"); // Shows php warning only if plugin defective
4829 $moddelete = $modname .'_delete_instance'; // Delete everything connected to an instance
4830 $moddeletecourse = $modname .'_delete_course'; // Delete other stray stuff (uncommon)
4832 if ($instances = $DB->get_records($modname, array('course'=>$course->id))) {
4833 foreach ($instances as $instance) {
4834 if ($cm = get_coursemodule_from_instance($modname, $instance->id, $course->id)) {
4835 /// Delete activity context questions and question categories
4836 question_delete_activity($cm, $showfeedback);
4838 if (function_exists($moddelete)) {
4839 // This purges all module data in related tables, extra user prefs, settings, etc.
4840 $moddelete($instance->id);
4841 } else {
4842 // NOTE: we should not allow installation of modules with missing delete support!
4843 debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!");
4844 $DB->delete_records($modname, array('id'=>$instance->id));
4847 if ($cm) {
4848 // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition
4849 context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4850 $DB->delete_records('course_modules', array('id'=>$cm->id));
4854 if (function_exists($moddeletecourse)) {
4855 // Execute ptional course cleanup callback
4856 $moddeletecourse($course, $showfeedback);
4858 if ($instances and $showfeedback) {
4859 echo $OUTPUT->notification($strdeleted.get_string('pluginname', $modname), 'notifysuccess');
4861 } else {
4862 // Ooops, this module is not properly installed, force-delete it in the next block
4866 // We have tried to delete everything the nice way - now let's force-delete any remaining module data
4868 // Remove all data from availability and completion tables that is associated
4869 // with course-modules belonging to this course. Note this is done even if the
4870 // features are not enabled now, in case they were enabled previously.
4871 $DB->delete_records_select('course_modules_completion',
4872 'coursemoduleid IN (SELECT id from {course_modules} WHERE course=?)',
4873 array($courseid));
4874 $DB->delete_records_select('course_modules_availability',
4875 'coursemoduleid IN (SELECT id from {course_modules} WHERE course=?)',
4876 array($courseid));
4877 $DB->delete_records_select('course_modules_avail_fields',
4878 'coursemoduleid IN (SELECT id from {course_modules} WHERE course=?)',
4879 array($courseid));
4881 // Remove course-module data.
4882 $cms = $DB->get_records('course_modules', array('course'=>$course->id));
4883 foreach ($cms as $cm) {
4884 if ($module = $DB->get_record('modules', array('id'=>$cm->module))) {
4885 try {
4886 $DB->delete_records($module->name, array('id'=>$cm->instance));
4887 } catch (Exception $e) {
4888 // Ignore weird or missing table problems
4891 context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4892 $DB->delete_records('course_modules', array('id'=>$cm->id));
4895 if ($showfeedback) {
4896 echo $OUTPUT->notification($strdeleted.get_string('type_mod_plural', 'plugin'), 'notifysuccess');
4899 // Cleanup the rest of plugins
4900 $cleanuplugintypes = array('report', 'coursereport', 'format');
4901 foreach ($cleanuplugintypes as $type) {
4902 $plugins = get_plugin_list_with_function($type, 'delete_course', 'lib.php');
4903 foreach ($plugins as $plugin=>$pluginfunction) {
4904 $pluginfunction($course->id, $showfeedback);
4906 if ($showfeedback) {
4907 echo $OUTPUT->notification($strdeleted.get_string('type_'.$type.'_plural', 'plugin'), 'notifysuccess');
4911 // Delete questions and question categories
4912 question_delete_course($course, $showfeedback);
4913 if ($showfeedback) {
4914 echo $OUTPUT->notification($strdeleted.get_string('questions', 'question'), 'notifysuccess');
4917 // Make sure there are no subcontexts left - all valid blocks and modules should be already gone
4918 $childcontexts = $coursecontext->get_child_contexts(); // returns all subcontexts since 2.2
4919 foreach ($childcontexts as $childcontext) {
4920 $childcontext->delete();
4922 unset($childcontexts);
4924 // Remove all roles and enrolments by default
4925 if (empty($options['keep_roles_and_enrolments'])) {
4926 // this hack is used in restore when deleting contents of existing course
4927 role_unassign_all(array('contextid'=>$coursecontext->id, 'component'=>''), true);
4928 enrol_course_delete($course);
4929 if ($showfeedback) {
4930 echo $OUTPUT->notification($strdeleted.get_string('type_enrol_plural', 'plugin'), 'notifysuccess');
4934 // Delete any groups, removing members and grouping/course links first.
4935 if (empty($options['keep_groups_and_groupings'])) {
4936 groups_delete_groupings($course->id, $showfeedback);
4937 groups_delete_groups($course->id, $showfeedback);
4940 // filters be gone!
4941 filter_delete_all_for_context($coursecontext->id);
4943 // die comments!
4944 comment::delete_comments($coursecontext->id);
4946 // ratings are history too
4947 $delopt = new stdclass();
4948 $delopt->contextid = $coursecontext->id;
4949 $rm = new rating_manager();
4950 $rm->delete_ratings($delopt);
4952 // Delete course tags
4953 coursetag_delete_course_tags($course->id, $showfeedback);
4955 // Delete calendar events
4956 $DB->delete_records('event', array('courseid'=>$course->id));
4957 $fs->delete_area_files($coursecontext->id, 'calendar');
4959 // Delete all related records in other core tables that may have a courseid
4960 // This array stores the tables that need to be cleared, as
4961 // table_name => column_name that contains the course id.
4962 $tablestoclear = array(
4963 'log' => 'course', // Course logs (NOTE: this might be changed in the future)
4964 'backup_courses' => 'courseid', // Scheduled backup stuff
4965 'user_lastaccess' => 'courseid', // User access info
4967 foreach ($tablestoclear as $table => $col) {
4968 $DB->delete_records($table, array($col=>$course->id));
4971 // delete all course backup files
4972 $fs->delete_area_files($coursecontext->id, 'backup');
4974 // cleanup course record - remove links to deleted stuff
4975 $oldcourse = new stdClass();
4976 $oldcourse->id = $course->id;
4977 $oldcourse->summary = '';
4978 $oldcourse->modinfo = NULL;
4979 $oldcourse->legacyfiles = 0;
4980 $oldcourse->enablecompletion = 0;
4981 if (!empty($options['keep_groups_and_groupings'])) {
4982 $oldcourse->defaultgroupingid = 0;
4984 $DB->update_record('course', $oldcourse);
4986 // Delete course sections and availability options.
4987 $DB->delete_records_select('course_sections_availability',
4988 'coursesectionid IN (SELECT id from {course_sections} WHERE course=?)',
4989 array($course->id));
4990 $DB->delete_records_select('course_sections_avail_fields',
4991 'coursesectionid IN (SELECT id from {course_sections} WHERE course=?)',
4992 array($course->id));
4993 $DB->delete_records('course_sections', array('course'=>$course->id));
4995 // delete legacy, section and any other course files
4996 $fs->delete_area_files($coursecontext->id, 'course'); // files from summary and section
4998 // Delete all remaining stuff linked to context such as files, comments, ratings, etc.
4999 if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) {
5000 // Easy, do not delete the context itself...
5001 $coursecontext->delete_content();
5003 } else {
5004 // Hack alert!!!!
5005 // We can not drop all context stuff because it would bork enrolments and roles,
5006 // there might be also files used by enrol plugins...
5009 // Delete legacy files - just in case some files are still left there after conversion to new file api,
5010 // also some non-standard unsupported plugins may try to store something there
5011 fulldelete($CFG->dataroot.'/'.$course->id);
5013 // Finally trigger the event
5014 $course->context = $coursecontext; // you can not access context in cron event later after course is deleted
5015 $course->options = $options; // not empty if we used any crazy hack
5016 events_trigger('course_content_removed', $course);
5018 return true;
5022 * Change dates in module - used from course reset.
5024 * @global object
5025 * @global object
5026 * @param string $modname forum, assignment, etc
5027 * @param array $fields array of date fields from mod table
5028 * @param int $timeshift time difference
5029 * @param int $courseid
5030 * @return bool success
5032 function shift_course_mod_dates($modname, $fields, $timeshift, $courseid) {
5033 global $CFG, $DB;
5034 include_once($CFG->dirroot.'/mod/'.$modname.'/lib.php');
5036 $return = true;
5037 foreach ($fields as $field) {
5038 $updatesql = "UPDATE {".$modname."}
5039 SET $field = $field + ?
5040 WHERE course=? AND $field<>0";
5041 $return = $DB->execute($updatesql, array($timeshift, $courseid)) && $return;
5044 $refreshfunction = $modname.'_refresh_events';
5045 if (function_exists($refreshfunction)) {
5046 $refreshfunction($courseid);
5049 return $return;
5053 * This function will empty a course of user data.
5054 * It will retain the activities and the structure of the course.
5056 * @param object $data an object containing all the settings including courseid (without magic quotes)
5057 * @return array status array of array component, item, error
5059 function reset_course_userdata($data) {
5060 global $CFG, $USER, $DB;
5061 require_once($CFG->libdir.'/gradelib.php');
5062 require_once($CFG->libdir.'/completionlib.php');
5063 require_once($CFG->dirroot.'/group/lib.php');
5065 $data->courseid = $data->id;
5066 $context = context_course::instance($data->courseid);
5068 // calculate the time shift of dates
5069 if (!empty($data->reset_start_date)) {
5070 // time part of course startdate should be zero
5071 $data->timeshift = $data->reset_start_date - usergetmidnight($data->reset_start_date_old);
5072 } else {
5073 $data->timeshift = 0;
5076 // result array: component, item, error
5077 $status = array();
5079 // start the resetting
5080 $componentstr = get_string('general');
5082 // move the course start time
5083 if (!empty($data->reset_start_date) and $data->timeshift) {
5084 // change course start data
5085 $DB->set_field('course', 'startdate', $data->reset_start_date, array('id'=>$data->courseid));
5086 // update all course and group events - do not move activity events
5087 $updatesql = "UPDATE {event}
5088 SET timestart = timestart + ?
5089 WHERE courseid=? AND instance=0";
5090 $DB->execute($updatesql, array($data->timeshift, $data->courseid));
5092 $status[] = array('component'=>$componentstr, 'item'=>get_string('datechanged'), 'error'=>false);
5095 if (!empty($data->reset_logs)) {
5096 $DB->delete_records('log', array('course'=>$data->courseid));
5097 $status[] = array('component'=>$componentstr, 'item'=>get_string('deletelogs'), 'error'=>false);
5100 if (!empty($data->reset_events)) {
5101 $DB->delete_records('event', array('courseid'=>$data->courseid));
5102 $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteevents', 'calendar'), 'error'=>false);
5105 if (!empty($data->reset_notes)) {
5106 require_once($CFG->dirroot.'/notes/lib.php');
5107 note_delete_all($data->courseid);
5108 $status[] = array('component'=>$componentstr, 'item'=>get_string('deletenotes', 'notes'), 'error'=>false);
5111 if (!empty($data->delete_blog_associations)) {
5112 require_once($CFG->dirroot.'/blog/lib.php');
5113 blog_remove_associations_for_course($data->courseid);
5114 $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteblogassociations', 'blog'), 'error'=>false);
5117 if (!empty($data->reset_completion)) {
5118 // Delete course and activity completion information.
5119 $course = $DB->get_record('course', array('id'=>$data->courseid));
5120 $cc = new completion_info($course);
5121 $cc->delete_all_completion_data();
5122 $status[] = array('component' => $componentstr,
5123 'item' => get_string('deletecompletiondata', 'completion'), 'error' => false);
5126 $componentstr = get_string('roles');
5128 if (!empty($data->reset_roles_overrides)) {
5129 $children = get_child_contexts($context);
5130 foreach ($children as $child) {
5131 $DB->delete_records('role_capabilities', array('contextid'=>$child->id));
5133 $DB->delete_records('role_capabilities', array('contextid'=>$context->id));
5134 //force refresh for logged in users
5135 mark_context_dirty($context->path);
5136 $status[] = array('component'=>$componentstr, 'item'=>get_string('deletecourseoverrides', 'role'), 'error'=>false);
5139 if (!empty($data->reset_roles_local)) {
5140 $children = get_child_contexts($context);
5141 foreach ($children as $child) {
5142 role_unassign_all(array('contextid'=>$child->id));
5144 //force refresh for logged in users
5145 mark_context_dirty($context->path);
5146 $status[] = array('component'=>$componentstr, 'item'=>get_string('deletelocalroles', 'role'), 'error'=>false);
5149 // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc.
5150 $data->unenrolled = array();
5151 if (!empty($data->unenrol_users)) {
5152 $plugins = enrol_get_plugins(true);
5153 $instances = enrol_get_instances($data->courseid, true);
5154 foreach ($instances as $key=>$instance) {
5155 if (!isset($plugins[$instance->enrol])) {
5156 unset($instances[$key]);
5157 continue;
5161 foreach($data->unenrol_users as $withroleid) {
5162 if ($withroleid) {
5163 $sql = "SELECT ue.*
5164 FROM {user_enrolments} ue
5165 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5166 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5167 JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)";
5168 $params = array('courseid'=>$data->courseid, 'roleid'=>$withroleid, 'courselevel'=>CONTEXT_COURSE);
5170 } else {
5171 // without any role assigned at course context
5172 $sql = "SELECT ue.*
5173 FROM {user_enrolments} ue
5174 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5175 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5176 LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid)
5177 WHERE ra.id IS NULL";
5178 $params = array('courseid'=>$data->courseid, 'courselevel'=>CONTEXT_COURSE);
5181 $rs = $DB->get_recordset_sql($sql, $params);
5182 foreach ($rs as $ue) {
5183 if (!isset($instances[$ue->enrolid])) {
5184 continue;
5186 $instance = $instances[$ue->enrolid];
5187 $plugin = $plugins[$instance->enrol];
5188 if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) {
5189 continue;
5192 $plugin->unenrol_user($instance, $ue->userid);
5193 $data->unenrolled[$ue->userid] = $ue->userid;
5195 $rs->close();
5198 if (!empty($data->unenrolled)) {
5199 $status[] = array('component'=>$componentstr, 'item'=>get_string('unenrol', 'enrol').' ('.count($data->unenrolled).')', 'error'=>false);
5203 $componentstr = get_string('groups');
5205 // remove all group members
5206 if (!empty($data->reset_groups_members)) {
5207 groups_delete_group_members($data->courseid);
5208 $status[] = array('component'=>$componentstr, 'item'=>get_string('removegroupsmembers', 'group'), 'error'=>false);
5211 // remove all groups
5212 if (!empty($data->reset_groups_remove)) {
5213 groups_delete_groups($data->courseid, false);
5214 $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteallgroups', 'group'), 'error'=>false);
5217 // remove all grouping members
5218 if (!empty($data->reset_groupings_members)) {
5219 groups_delete_groupings_groups($data->courseid, false);
5220 $status[] = array('component'=>$componentstr, 'item'=>get_string('removegroupingsmembers', 'group'), 'error'=>false);
5223 // remove all groupings
5224 if (!empty($data->reset_groupings_remove)) {
5225 groups_delete_groupings($data->courseid, false);
5226 $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteallgroupings', 'group'), 'error'=>false);
5229 // Look in every instance of every module for data to delete
5230 $unsupported_mods = array();
5231 if ($allmods = $DB->get_records('modules') ) {
5232 foreach ($allmods as $mod) {
5233 $modname = $mod->name;
5234 $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php';
5235 $moddeleteuserdata = $modname.'_reset_userdata'; // Function to delete user data
5236 if (file_exists($modfile)) {
5237 if (!$DB->count_records($modname, array('course'=>$data->courseid))) {
5238 continue; // Skip mods with no instances
5240 include_once($modfile);
5241 if (function_exists($moddeleteuserdata)) {
5242 $modstatus = $moddeleteuserdata($data);
5243 if (is_array($modstatus)) {
5244 $status = array_merge($status, $modstatus);
5245 } else {
5246 debugging('Module '.$modname.' returned incorrect staus - must be an array!');
5248 } else {
5249 $unsupported_mods[] = $mod;
5251 } else {
5252 debugging('Missing lib.php in '.$modname.' module!');
5257 // mention unsupported mods
5258 if (!empty($unsupported_mods)) {
5259 foreach($unsupported_mods as $mod) {
5260 $status[] = array('component'=>get_string('modulenameplural', $mod->name), 'item'=>'', 'error'=>get_string('resetnotimplemented'));
5265 $componentstr = get_string('gradebook', 'grades');
5266 // reset gradebook
5267 if (!empty($data->reset_gradebook_items)) {
5268 remove_course_grades($data->courseid, false);
5269 grade_grab_course_grades($data->courseid);
5270 grade_regrade_final_grades($data->courseid);
5271 $status[] = array('component'=>$componentstr, 'item'=>get_string('removeallcourseitems', 'grades'), 'error'=>false);
5273 } else if (!empty($data->reset_gradebook_grades)) {
5274 grade_course_reset($data->courseid);
5275 $status[] = array('component'=>$componentstr, 'item'=>get_string('removeallcoursegrades', 'grades'), 'error'=>false);
5277 // reset comments
5278 if (!empty($data->reset_comments)) {
5279 require_once($CFG->dirroot.'/comment/lib.php');
5280 comment::reset_course_page_comments($context);
5283 return $status;
5287 * Generate an email processing address
5289 * @param int $modid
5290 * @param string $modargs
5291 * @return string Returns email processing address
5293 function generate_email_processing_address($modid,$modargs) {
5294 global $CFG;
5296 $header = $CFG->mailprefix . substr(base64_encode(pack('C',$modid)),0,2).$modargs;
5297 return $header . substr(md5($header.get_site_identifier()),0,16).'@'.$CFG->maildomain;
5303 * @todo Finish documenting this function
5305 * @global object
5306 * @param string $modargs
5307 * @param string $body Currently unused
5309 function moodle_process_email($modargs,$body) {
5310 global $DB;
5312 // the first char should be an unencoded letter. We'll take this as an action
5313 switch ($modargs{0}) {
5314 case 'B': { // bounce
5315 list(,$userid) = unpack('V',base64_decode(substr($modargs,1,8)));
5316 if ($user = $DB->get_record("user", array('id'=>$userid), "id,email")) {
5317 // check the half md5 of their email
5318 $md5check = substr(md5($user->email),0,16);
5319 if ($md5check == substr($modargs, -16)) {
5320 set_bounce_count($user);
5322 // else maybe they've already changed it?
5325 break;
5326 // maybe more later?
5330 /// CORRESPONDENCE ////////////////////////////////////////////////
5333 * Get mailer instance, enable buffering, flush buffer or disable buffering.
5335 * @param string $action 'get', 'buffer', 'close' or 'flush'
5336 * @return moodle_phpmailer|null mailer instance if 'get' used or nothing
5338 function get_mailer($action='get') {
5339 global $CFG;
5341 static $mailer = null;
5342 static $counter = 0;
5344 if (!isset($CFG->smtpmaxbulk)) {
5345 $CFG->smtpmaxbulk = 1;
5348 if ($action == 'get') {
5349 $prevkeepalive = false;
5351 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5352 if ($counter < $CFG->smtpmaxbulk and !$mailer->IsError()) {
5353 $counter++;
5354 // reset the mailer
5355 $mailer->Priority = 3;
5356 $mailer->CharSet = 'UTF-8'; // our default
5357 $mailer->ContentType = "text/plain";
5358 $mailer->Encoding = "8bit";
5359 $mailer->From = "root@localhost";
5360 $mailer->FromName = "Root User";
5361 $mailer->Sender = "";
5362 $mailer->Subject = "";
5363 $mailer->Body = "";
5364 $mailer->AltBody = "";
5365 $mailer->ConfirmReadingTo = "";
5367 $mailer->ClearAllRecipients();
5368 $mailer->ClearReplyTos();
5369 $mailer->ClearAttachments();
5370 $mailer->ClearCustomHeaders();
5371 return $mailer;
5374 $prevkeepalive = $mailer->SMTPKeepAlive;
5375 get_mailer('flush');
5378 include_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php');
5379 $mailer = new moodle_phpmailer();
5381 $counter = 1;
5383 $mailer->Version = 'Moodle '.$CFG->version; // mailer version
5384 $mailer->PluginDir = $CFG->libdir.'/phpmailer/'; // plugin directory (eg smtp plugin)
5385 $mailer->CharSet = 'UTF-8';
5387 // some MTAs may do double conversion of LF if CRLF used, CRLF is required line ending in RFC 822bis
5388 if (isset($CFG->mailnewline) and $CFG->mailnewline == 'CRLF') {
5389 $mailer->LE = "\r\n";
5390 } else {
5391 $mailer->LE = "\n";
5394 if ($CFG->smtphosts == 'qmail') {
5395 $mailer->IsQmail(); // use Qmail system
5397 } else if (empty($CFG->smtphosts)) {
5398 $mailer->IsMail(); // use PHP mail() = sendmail
5400 } else {
5401 $mailer->IsSMTP(); // use SMTP directly
5402 if (!empty($CFG->debugsmtp)) {
5403 $mailer->SMTPDebug = true;
5405 $mailer->Host = $CFG->smtphosts; // specify main and backup servers
5406 $mailer->SMTPSecure = $CFG->smtpsecure; // specify secure connection protocol
5407 $mailer->SMTPKeepAlive = $prevkeepalive; // use previous keepalive
5409 if ($CFG->smtpuser) { // Use SMTP authentication
5410 $mailer->SMTPAuth = true;
5411 $mailer->Username = $CFG->smtpuser;
5412 $mailer->Password = $CFG->smtppass;
5416 return $mailer;
5419 $nothing = null;
5421 // keep smtp session open after sending
5422 if ($action == 'buffer') {
5423 if (!empty($CFG->smtpmaxbulk)) {
5424 get_mailer('flush');
5425 $m = get_mailer();
5426 if ($m->Mailer == 'smtp') {
5427 $m->SMTPKeepAlive = true;
5430 return $nothing;
5433 // close smtp session, but continue buffering
5434 if ($action == 'flush') {
5435 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5436 if (!empty($mailer->SMTPDebug)) {
5437 echo '<pre>'."\n";
5439 $mailer->SmtpClose();
5440 if (!empty($mailer->SMTPDebug)) {
5441 echo '</pre>';
5444 return $nothing;
5447 // close smtp session, do not buffer anymore
5448 if ($action == 'close') {
5449 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5450 get_mailer('flush');
5451 $mailer->SMTPKeepAlive = false;
5453 $mailer = null; // better force new instance
5454 return $nothing;
5459 * Send an email to a specified user
5461 * @global object
5462 * @global string
5463 * @global string IdentityProvider(IDP) URL user hits to jump to mnet peer.
5464 * @uses SITEID
5465 * @param stdClass $user A {@link $USER} object
5466 * @param stdClass $from A {@link $USER} object
5467 * @param string $subject plain text subject line of the email
5468 * @param string $messagetext plain text version of the message
5469 * @param string $messagehtml complete html version of the message (optional)
5470 * @param string $attachment a file on the filesystem, relative to $CFG->dataroot
5471 * @param string $attachname the name of the file (extension indicates MIME)
5472 * @param bool $usetrueaddress determines whether $from email address should
5473 * be sent out. Will be overruled by user profile setting for maildisplay
5474 * @param string $replyto Email address to reply to
5475 * @param string $replytoname Name of reply to recipient
5476 * @param int $wordwrapwidth custom word wrap width, default 79
5477 * @return bool Returns true if mail was sent OK and false if there was an error.
5479 function email_to_user($user, $from, $subject, $messagetext, $messagehtml='', $attachment='', $attachname='', $usetrueaddress=true, $replyto='', $replytoname='', $wordwrapwidth=79) {
5481 global $CFG;
5483 if (empty($user) || empty($user->email)) {
5484 $nulluser = 'User is null or has no email';
5485 error_log($nulluser);
5486 if (CLI_SCRIPT) {
5487 mtrace('Error: lib/moodlelib.php email_to_user(): '.$nulluser);
5489 return false;
5492 if (!empty($user->deleted)) {
5493 // do not mail deleted users
5494 $userdeleted = 'User is deleted';
5495 error_log($userdeleted);
5496 if (CLI_SCRIPT) {
5497 mtrace('Error: lib/moodlelib.php email_to_user(): '.$userdeleted);
5499 return false;
5502 if (!empty($CFG->noemailever)) {
5503 // hidden setting for development sites, set in config.php if needed
5504 $noemail = 'Not sending email due to noemailever config setting';
5505 error_log($noemail);
5506 if (CLI_SCRIPT) {
5507 mtrace('Error: lib/moodlelib.php email_to_user(): '.$noemail);
5509 return true;
5512 if (!empty($CFG->divertallemailsto)) {
5513 $subject = "[DIVERTED {$user->email}] $subject";
5514 $user = clone($user);
5515 $user->email = $CFG->divertallemailsto;
5518 // skip mail to suspended users
5519 if ((isset($user->auth) && $user->auth=='nologin') or (isset($user->suspended) && $user->suspended)) {
5520 return true;
5523 if (!validate_email($user->email)) {
5524 // we can not send emails to invalid addresses - it might create security issue or confuse the mailer
5525 $invalidemail = "User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending.";
5526 error_log($invalidemail);
5527 if (CLI_SCRIPT) {
5528 mtrace('Error: lib/moodlelib.php email_to_user(): '.$invalidemail);
5530 return false;
5533 if (over_bounce_threshold($user)) {
5534 $bouncemsg = "User $user->id (".fullname($user).") is over bounce threshold! Not sending.";
5535 error_log($bouncemsg);
5536 if (CLI_SCRIPT) {
5537 mtrace('Error: lib/moodlelib.php email_to_user(): '.$bouncemsg);
5539 return false;
5542 // If the user is a remote mnet user, parse the email text for URL to the
5543 // wwwroot and modify the url to direct the user's browser to login at their
5544 // home site (identity provider - idp) before hitting the link itself
5545 if (is_mnet_remote_user($user)) {
5546 require_once($CFG->dirroot.'/mnet/lib.php');
5548 $jumpurl = mnet_get_idp_jump_url($user);
5549 $callback = partial('mnet_sso_apply_indirection', $jumpurl);
5551 $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%",
5552 $callback,
5553 $messagetext);
5554 $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%",
5555 $callback,
5556 $messagehtml);
5558 $mail = get_mailer();
5560 if (!empty($mail->SMTPDebug)) {
5561 echo '<pre>' . "\n";
5564 $temprecipients = array();
5565 $tempreplyto = array();
5567 $supportuser = generate_email_supportuser();
5569 // make up an email address for handling bounces
5570 if (!empty($CFG->handlebounces)) {
5571 $modargs = 'B'.base64_encode(pack('V',$user->id)).substr(md5($user->email),0,16);
5572 $mail->Sender = generate_email_processing_address(0,$modargs);
5573 } else {
5574 $mail->Sender = $supportuser->email;
5577 if (is_string($from)) { // So we can pass whatever we want if there is need
5578 $mail->From = $CFG->noreplyaddress;
5579 $mail->FromName = $from;
5580 } else if ($usetrueaddress and $from->maildisplay) {
5581 $mail->From = $from->email;
5582 $mail->FromName = fullname($from);
5583 } else {
5584 $mail->From = $CFG->noreplyaddress;
5585 $mail->FromName = fullname($from);
5586 if (empty($replyto)) {
5587 $tempreplyto[] = array($CFG->noreplyaddress, get_string('noreplyname'));
5591 if (!empty($replyto)) {
5592 $tempreplyto[] = array($replyto, $replytoname);
5595 $mail->Subject = substr($subject, 0, 900);
5597 $temprecipients[] = array($user->email, fullname($user));
5599 $mail->WordWrap = $wordwrapwidth; // set word wrap
5601 if (!empty($from->customheaders)) { // Add custom headers
5602 if (is_array($from->customheaders)) {
5603 foreach ($from->customheaders as $customheader) {
5604 $mail->AddCustomHeader($customheader);
5606 } else {
5607 $mail->AddCustomHeader($from->customheaders);
5611 if (!empty($from->priority)) {
5612 $mail->Priority = $from->priority;
5615 if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) { // Don't ever send HTML to users who don't want it
5616 $mail->IsHTML(true);
5617 $mail->Encoding = 'quoted-printable'; // Encoding to use
5618 $mail->Body = $messagehtml;
5619 $mail->AltBody = "\n$messagetext\n";
5620 } else {
5621 $mail->IsHTML(false);
5622 $mail->Body = "\n$messagetext\n";
5625 if ($attachment && $attachname) {
5626 if (preg_match( "~\\.\\.~" ,$attachment )) { // Security check for ".." in dir path
5627 $temprecipients[] = array($supportuser->email, fullname($supportuser, true));
5628 $mail->AddStringAttachment('Error in attachment. User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain');
5629 } else {
5630 require_once($CFG->libdir.'/filelib.php');
5631 $mimetype = mimeinfo('type', $attachname);
5632 $mail->AddAttachment($CFG->dataroot .'/'. $attachment, $attachname, 'base64', $mimetype);
5636 // Check if the email should be sent in an other charset then the default UTF-8
5637 if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) {
5639 // use the defined site mail charset or eventually the one preferred by the recipient
5640 $charset = $CFG->sitemailcharset;
5641 if (!empty($CFG->allowusermailcharset)) {
5642 if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) {
5643 $charset = $useremailcharset;
5647 // convert all the necessary strings if the charset is supported
5648 $charsets = get_list_of_charsets();
5649 unset($charsets['UTF-8']);
5650 if (in_array($charset, $charsets)) {
5651 $mail->CharSet = $charset;
5652 $mail->FromName = textlib::convert($mail->FromName, 'utf-8', strtolower($charset));
5653 $mail->Subject = textlib::convert($mail->Subject, 'utf-8', strtolower($charset));
5654 $mail->Body = textlib::convert($mail->Body, 'utf-8', strtolower($charset));
5655 $mail->AltBody = textlib::convert($mail->AltBody, 'utf-8', strtolower($charset));
5657 foreach ($temprecipients as $key => $values) {
5658 $temprecipients[$key][1] = textlib::convert($values[1], 'utf-8', strtolower($charset));
5660 foreach ($tempreplyto as $key => $values) {
5661 $tempreplyto[$key][1] = textlib::convert($values[1], 'utf-8', strtolower($charset));
5666 foreach ($temprecipients as $values) {
5667 $mail->AddAddress($values[0], $values[1]);
5669 foreach ($tempreplyto as $values) {
5670 $mail->AddReplyTo($values[0], $values[1]);
5673 if ($mail->Send()) {
5674 set_send_count($user);
5675 if (!empty($mail->SMTPDebug)) {
5676 echo '</pre>';
5678 return true;
5679 } else {
5680 add_to_log(SITEID, 'library', 'mailer', qualified_me(), 'ERROR: '. $mail->ErrorInfo);
5681 if (CLI_SCRIPT) {
5682 mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo);
5684 if (!empty($mail->SMTPDebug)) {
5685 echo '</pre>';
5687 return false;
5692 * Generate a signoff for emails based on support settings
5694 * @global object
5695 * @return string
5697 function generate_email_signoff() {
5698 global $CFG;
5700 $signoff = "\n";
5701 if (!empty($CFG->supportname)) {
5702 $signoff .= $CFG->supportname."\n";
5704 if (!empty($CFG->supportemail)) {
5705 $signoff .= $CFG->supportemail."\n";
5707 if (!empty($CFG->supportpage)) {
5708 $signoff .= $CFG->supportpage."\n";
5710 return $signoff;
5714 * Generate a fake user for emails based on support settings
5715 * @global object
5716 * @return object user info
5718 function generate_email_supportuser() {
5719 global $CFG;
5721 static $supportuser;
5723 if (!empty($supportuser)) {
5724 return $supportuser;
5727 $supportuser = new stdClass();
5728 $supportuser->email = $CFG->supportemail ? $CFG->supportemail : $CFG->noreplyaddress;
5729 $supportuser->firstname = $CFG->supportname ? $CFG->supportname : get_string('noreplyname');
5730 $supportuser->lastname = '';
5731 $supportuser->maildisplay = true;
5733 return $supportuser;
5738 * Sets specified user's password and send the new password to the user via email.
5740 * @global object
5741 * @global object
5742 * @param user $user A {@link $USER} object
5743 * @param boolean $fasthash If true, use a low cost factor when generating the hash for speed.
5744 * @return boolean|string Returns "true" if mail was sent OK and "false" if there was an error
5746 function setnew_password_and_mail($user, $fasthash = false) {
5747 global $CFG, $DB;
5749 // we try to send the mail in language the user understands,
5750 // unfortunately the filter_string() does not support alternative langs yet
5751 // so multilang will not work properly for site->fullname
5752 $lang = empty($user->lang) ? $CFG->lang : $user->lang;
5754 $site = get_site();
5756 $supportuser = generate_email_supportuser();
5758 $newpassword = generate_password();
5760 $hashedpassword = hash_internal_user_password($newpassword, $fasthash);
5761 $DB->set_field('user', 'password', $hashedpassword, array('id'=>$user->id));
5763 $a = new stdClass();
5764 $a->firstname = fullname($user, true);
5765 $a->sitename = format_string($site->fullname);
5766 $a->username = $user->username;
5767 $a->newpassword = $newpassword;
5768 $a->link = $CFG->wwwroot .'/login/';
5769 $a->signoff = generate_email_signoff();
5771 $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang);
5773 $subject = format_string($site->fullname) .': '. (string)new lang_string('newusernewpasswordsubj', '', $a, $lang);
5775 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5776 return email_to_user($user, $supportuser, $subject, $message);
5781 * Resets specified user's password and send the new password to the user via email.
5783 * @param stdClass $user A {@link $USER} object
5784 * @return bool Returns true if mail was sent OK and false if there was an error.
5786 function reset_password_and_mail($user) {
5787 global $CFG;
5789 $site = get_site();
5790 $supportuser = generate_email_supportuser();
5792 $userauth = get_auth_plugin($user->auth);
5793 if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) {
5794 trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth.");
5795 return false;
5798 $newpassword = generate_password();
5800 if (!$userauth->user_update_password($user, $newpassword)) {
5801 print_error("cannotsetpassword");
5804 $a = new stdClass();
5805 $a->firstname = $user->firstname;
5806 $a->lastname = $user->lastname;
5807 $a->sitename = format_string($site->fullname);
5808 $a->username = $user->username;
5809 $a->newpassword = $newpassword;
5810 $a->link = $CFG->httpswwwroot .'/login/change_password.php';
5811 $a->signoff = generate_email_signoff();
5813 $message = get_string('newpasswordtext', '', $a);
5815 $subject = format_string($site->fullname) .': '. get_string('changedpassword');
5817 unset_user_preference('create_password', $user); // prevent cron from generating the password
5819 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5820 return email_to_user($user, $supportuser, $subject, $message);
5825 * Send email to specified user with confirmation text and activation link.
5827 * @global object
5828 * @param user $user A {@link $USER} object
5829 * @return bool Returns true if mail was sent OK and false if there was an error.
5831 function send_confirmation_email($user) {
5832 global $CFG;
5834 $site = get_site();
5835 $supportuser = generate_email_supportuser();
5837 $data = new stdClass();
5838 $data->firstname = fullname($user);
5839 $data->sitename = format_string($site->fullname);
5840 $data->admin = generate_email_signoff();
5842 $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname));
5844 $username = urlencode($user->username);
5845 $username = str_replace('.', '%2E', $username); // prevent problems with trailing dots
5846 $data->link = $CFG->wwwroot .'/login/confirm.php?data='. $user->secret .'/'. $username;
5847 $message = get_string('emailconfirmation', '', $data);
5848 $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true);
5850 $user->mailformat = 1; // Always send HTML version as well
5852 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5853 return email_to_user($user, $supportuser, $subject, $message, $messagehtml);
5858 * send_password_change_confirmation_email.
5860 * @global object
5861 * @param user $user A {@link $USER} object
5862 * @return bool Returns true if mail was sent OK and false if there was an error.
5864 function send_password_change_confirmation_email($user) {
5865 global $CFG;
5867 $site = get_site();
5868 $supportuser = generate_email_supportuser();
5870 $data = new stdClass();
5871 $data->firstname = $user->firstname;
5872 $data->lastname = $user->lastname;
5873 $data->sitename = format_string($site->fullname);
5874 $data->link = $CFG->httpswwwroot .'/login/forgot_password.php?p='. $user->secret .'&s='. urlencode($user->username);
5875 $data->admin = generate_email_signoff();
5877 $message = get_string('emailpasswordconfirmation', '', $data);
5878 $subject = get_string('emailpasswordconfirmationsubject', '', format_string($site->fullname));
5880 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5881 return email_to_user($user, $supportuser, $subject, $message);
5886 * send_password_change_info.
5888 * @global object
5889 * @param user $user A {@link $USER} object
5890 * @return bool Returns true if mail was sent OK and false if there was an error.
5892 function send_password_change_info($user) {
5893 global $CFG;
5895 $site = get_site();
5896 $supportuser = generate_email_supportuser();
5897 $systemcontext = context_system::instance();
5899 $data = new stdClass();
5900 $data->firstname = $user->firstname;
5901 $data->lastname = $user->lastname;
5902 $data->sitename = format_string($site->fullname);
5903 $data->admin = generate_email_signoff();
5905 $userauth = get_auth_plugin($user->auth);
5907 if (!is_enabled_auth($user->auth) or $user->auth == 'nologin') {
5908 $message = get_string('emailpasswordchangeinfodisabled', '', $data);
5909 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
5910 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5911 return email_to_user($user, $supportuser, $subject, $message);
5914 if ($userauth->can_change_password() and $userauth->change_password_url()) {
5915 // we have some external url for password changing
5916 $data->link .= $userauth->change_password_url();
5918 } else {
5919 //no way to change password, sorry
5920 $data->link = '';
5923 if (!empty($data->link) and has_capability('moodle/user:changeownpassword', $systemcontext, $user->id)) {
5924 $message = get_string('emailpasswordchangeinfo', '', $data);
5925 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
5926 } else {
5927 $message = get_string('emailpasswordchangeinfofail', '', $data);
5928 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
5931 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5932 return email_to_user($user, $supportuser, $subject, $message);
5937 * Check that an email is allowed. It returns an error message if there
5938 * was a problem.
5940 * @global object
5941 * @param string $email Content of email
5942 * @return string|false
5944 function email_is_not_allowed($email) {
5945 global $CFG;
5947 if (!empty($CFG->allowemailaddresses)) {
5948 $allowed = explode(' ', $CFG->allowemailaddresses);
5949 foreach ($allowed as $allowedpattern) {
5950 $allowedpattern = trim($allowedpattern);
5951 if (!$allowedpattern) {
5952 continue;
5954 if (strpos($allowedpattern, '.') === 0) {
5955 if (strpos(strrev($email), strrev($allowedpattern)) === 0) {
5956 // subdomains are in a form ".example.com" - matches "xxx@anything.example.com"
5957 return false;
5960 } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) { // Match! (bug 5250)
5961 return false;
5964 return get_string('emailonlyallowed', '', $CFG->allowemailaddresses);
5966 } else if (!empty($CFG->denyemailaddresses)) {
5967 $denied = explode(' ', $CFG->denyemailaddresses);
5968 foreach ($denied as $deniedpattern) {
5969 $deniedpattern = trim($deniedpattern);
5970 if (!$deniedpattern) {
5971 continue;
5973 if (strpos($deniedpattern, '.') === 0) {
5974 if (strpos(strrev($email), strrev($deniedpattern)) === 0) {
5975 // subdomains are in a form ".example.com" - matches "xxx@anything.example.com"
5976 return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
5979 } else if (strpos(strrev($email), strrev('@'.$deniedpattern)) === 0) { // Match! (bug 5250)
5980 return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
5985 return false;
5988 /// FILE HANDLING /////////////////////////////////////////////
5991 * Returns local file storage instance
5993 * @return file_storage
5995 function get_file_storage() {
5996 global $CFG;
5998 static $fs = null;
6000 if ($fs) {
6001 return $fs;
6004 require_once("$CFG->libdir/filelib.php");
6006 if (isset($CFG->filedir)) {
6007 $filedir = $CFG->filedir;
6008 } else {
6009 $filedir = $CFG->dataroot.'/filedir';
6012 if (isset($CFG->trashdir)) {
6013 $trashdirdir = $CFG->trashdir;
6014 } else {
6015 $trashdirdir = $CFG->dataroot.'/trashdir';
6018 $fs = new file_storage($filedir, $trashdirdir, "$CFG->tempdir/filestorage", $CFG->directorypermissions, $CFG->filepermissions);
6020 return $fs;
6024 * Returns local file storage instance
6026 * @return file_browser
6028 function get_file_browser() {
6029 global $CFG;
6031 static $fb = null;
6033 if ($fb) {
6034 return $fb;
6037 require_once("$CFG->libdir/filelib.php");
6039 $fb = new file_browser();
6041 return $fb;
6045 * Returns file packer
6047 * @param string $mimetype default application/zip
6048 * @return file_packer
6050 function get_file_packer($mimetype='application/zip') {
6051 global $CFG;
6053 static $fp = array();
6055 if (isset($fp[$mimetype])) {
6056 return $fp[$mimetype];
6059 switch ($mimetype) {
6060 case 'application/zip':
6061 case 'application/vnd.moodle.backup':
6062 case 'application/vnd.moodle.profiling':
6063 $classname = 'zip_packer';
6064 break;
6065 case 'application/x-tar':
6066 // $classname = 'tar_packer';
6067 // break;
6068 default:
6069 return false;
6072 require_once("$CFG->libdir/filestorage/$classname.php");
6073 $fp[$mimetype] = new $classname();
6075 return $fp[$mimetype];
6079 * Returns current name of file on disk if it exists.
6081 * @param string $newfile File to be verified
6082 * @return string Current name of file on disk if true
6084 function valid_uploaded_file($newfile) {
6085 if (empty($newfile)) {
6086 return '';
6088 if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) {
6089 return $newfile['tmp_name'];
6090 } else {
6091 return '';
6096 * Returns the maximum size for uploading files.
6098 * There are seven possible upload limits:
6099 * 1. in Apache using LimitRequestBody (no way of checking or changing this)
6100 * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
6101 * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
6102 * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
6103 * 5. by the Moodle admin in $CFG->maxbytes
6104 * 6. by the teacher in the current course $course->maxbytes
6105 * 7. by the teacher for the current module, eg $assignment->maxbytes
6107 * These last two are passed to this function as arguments (in bytes).
6108 * Anything defined as 0 is ignored.
6109 * The smallest of all the non-zero numbers is returned.
6111 * @todo Finish documenting this function
6113 * @param int $sizebytes Set maximum size
6114 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6115 * @param int $modulebytes Current module ->maxbytes (in bytes)
6116 * @return int The maximum size for uploading files.
6118 function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0) {
6120 if (! $filesize = ini_get('upload_max_filesize')) {
6121 $filesize = '5M';
6123 $minimumsize = get_real_size($filesize);
6125 if ($postsize = ini_get('post_max_size')) {
6126 $postsize = get_real_size($postsize);
6127 if ($postsize < $minimumsize) {
6128 $minimumsize = $postsize;
6132 if (($sitebytes > 0) and ($sitebytes < $minimumsize)) {
6133 $minimumsize = $sitebytes;
6136 if (($coursebytes > 0) and ($coursebytes < $minimumsize)) {
6137 $minimumsize = $coursebytes;
6140 if (($modulebytes > 0) and ($modulebytes < $minimumsize)) {
6141 $minimumsize = $modulebytes;
6144 return $minimumsize;
6148 * Returns the maximum size for uploading files for the current user
6150 * This function takes in account @see:get_max_upload_file_size() the user's capabilities
6152 * @param context $context The context in which to check user capabilities
6153 * @param int $sizebytes Set maximum size
6154 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6155 * @param int $modulebytes Current module ->maxbytes (in bytes)
6156 * @param stdClass The user
6157 * @return int The maximum size for uploading files.
6159 function get_user_max_upload_file_size($context, $sitebytes=0, $coursebytes=0, $modulebytes=0, $user=null) {
6160 global $USER;
6162 if (empty($user)) {
6163 $user = $USER;
6166 if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) {
6167 return USER_CAN_IGNORE_FILE_SIZE_LIMITS;
6170 return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes);
6174 * Returns an array of possible sizes in local language
6176 * Related to {@link get_max_upload_file_size()} - this function returns an
6177 * array of possible sizes in an array, translated to the
6178 * local language.
6180 * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes.
6182 * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)"
6183 * with the value set to 0. This option will be the first in the list.
6185 * @global object
6186 * @uses SORT_NUMERIC
6187 * @param int $sizebytes Set maximum size
6188 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6189 * @param int $modulebytes Current module ->maxbytes (in bytes)
6190 * @param int|array $custombytes custom upload size/s which will be added to list,
6191 * Only value/s smaller then maxsize will be added to list.
6192 * @return array
6194 function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null) {
6195 global $CFG;
6197 if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) {
6198 return array();
6201 if ($sitebytes == 0) {
6202 // Will get the minimum of upload_max_filesize or post_max_size.
6203 $sitebytes = get_max_upload_file_size();
6206 $filesize = array();
6207 $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152,
6208 5242880, 10485760, 20971520, 52428800, 104857600);
6210 // If custombytes is given and is valid then add it to the list.
6211 if (is_number($custombytes) and $custombytes > 0) {
6212 $custombytes = (int)$custombytes;
6213 if (!in_array($custombytes, $sizelist)) {
6214 $sizelist[] = $custombytes;
6216 } else if (is_array($custombytes)) {
6217 $sizelist = array_unique(array_merge($sizelist, $custombytes));
6220 // Allow maxbytes to be selected if it falls outside the above boundaries
6221 if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) {
6222 // note: get_real_size() is used in order to prevent problems with invalid values
6223 $sizelist[] = get_real_size($CFG->maxbytes);
6226 foreach ($sizelist as $sizebytes) {
6227 if ($sizebytes < $maxsize && $sizebytes > 0) {
6228 $filesize[(string)intval($sizebytes)] = display_size($sizebytes);
6232 $limitlevel = '';
6233 $displaysize = '';
6234 if ($modulebytes &&
6235 (($modulebytes < $coursebytes || $coursebytes == 0) &&
6236 ($modulebytes < $sitebytes || $sitebytes == 0))) {
6237 $limitlevel = get_string('activity', 'core');
6238 $displaysize = display_size($modulebytes);
6239 $filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list.
6241 } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) {
6242 $limitlevel = get_string('course', 'core');
6243 $displaysize = display_size($coursebytes);
6244 $filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list.
6246 } else if ($sitebytes) {
6247 $limitlevel = get_string('site', 'core');
6248 $displaysize = display_size($sitebytes);
6249 $filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list.
6252 krsort($filesize, SORT_NUMERIC);
6253 if ($limitlevel) {
6254 $params = (object) array('contextname'=>$limitlevel, 'displaysize'=>$displaysize);
6255 $filesize = array('0'=>get_string('uploadlimitwithsize', 'core', $params)) + $filesize;
6258 return $filesize;
6262 * Returns an array with all the filenames in all subdirectories, relative to the given rootdir.
6264 * If excludefiles is defined, then that file/directory is ignored
6265 * If getdirs is true, then (sub)directories are included in the output
6266 * If getfiles is true, then files are included in the output
6267 * (at least one of these must be true!)
6269 * @todo Finish documenting this function. Add examples of $excludefile usage.
6271 * @param string $rootdir A given root directory to start from
6272 * @param string|array $excludefile If defined then the specified file/directory is ignored
6273 * @param bool $descend If true then subdirectories are recursed as well
6274 * @param bool $getdirs If true then (sub)directories are included in the output
6275 * @param bool $getfiles If true then files are included in the output
6276 * @return array An array with all the filenames in
6277 * all subdirectories, relative to the given rootdir
6279 function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) {
6281 $dirs = array();
6283 if (!$getdirs and !$getfiles) { // Nothing to show
6284 return $dirs;
6287 if (!is_dir($rootdir)) { // Must be a directory
6288 return $dirs;
6291 if (!$dir = opendir($rootdir)) { // Can't open it for some reason
6292 return $dirs;
6295 if (!is_array($excludefiles)) {
6296 $excludefiles = array($excludefiles);
6299 while (false !== ($file = readdir($dir))) {
6300 $firstchar = substr($file, 0, 1);
6301 if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) {
6302 continue;
6304 $fullfile = $rootdir .'/'. $file;
6305 if (filetype($fullfile) == 'dir') {
6306 if ($getdirs) {
6307 $dirs[] = $file;
6309 if ($descend) {
6310 $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles);
6311 foreach ($subdirs as $subdir) {
6312 $dirs[] = $file .'/'. $subdir;
6315 } else if ($getfiles) {
6316 $dirs[] = $file;
6319 closedir($dir);
6321 asort($dirs);
6323 return $dirs;
6328 * Adds up all the files in a directory and works out the size.
6330 * @todo Finish documenting this function
6332 * @param string $rootdir The directory to start from
6333 * @param string $excludefile A file to exclude when summing directory size
6334 * @return int The summed size of all files and subfiles within the root directory
6336 function get_directory_size($rootdir, $excludefile='') {
6337 global $CFG;
6339 // do it this way if we can, it's much faster
6340 if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) {
6341 $command = trim($CFG->pathtodu).' -sk '.escapeshellarg($rootdir);
6342 $output = null;
6343 $return = null;
6344 exec($command,$output,$return);
6345 if (is_array($output)) {
6346 return get_real_size(intval($output[0]).'k'); // we told it to return k.
6350 if (!is_dir($rootdir)) { // Must be a directory
6351 return 0;
6354 if (!$dir = @opendir($rootdir)) { // Can't open it for some reason
6355 return 0;
6358 $size = 0;
6360 while (false !== ($file = readdir($dir))) {
6361 $firstchar = substr($file, 0, 1);
6362 if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) {
6363 continue;
6365 $fullfile = $rootdir .'/'. $file;
6366 if (filetype($fullfile) == 'dir') {
6367 $size += get_directory_size($fullfile, $excludefile);
6368 } else {
6369 $size += filesize($fullfile);
6372 closedir($dir);
6374 return $size;
6378 * Converts bytes into display form
6380 * @todo Finish documenting this function. Verify return type.
6382 * @staticvar string $gb Localized string for size in gigabytes
6383 * @staticvar string $mb Localized string for size in megabytes
6384 * @staticvar string $kb Localized string for size in kilobytes
6385 * @staticvar string $b Localized string for size in bytes
6386 * @param int $size The size to convert to human readable form
6387 * @return string
6389 function display_size($size) {
6391 static $gb, $mb, $kb, $b;
6393 if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) {
6394 return get_string('unlimited');
6397 if (empty($gb)) {
6398 $gb = get_string('sizegb');
6399 $mb = get_string('sizemb');
6400 $kb = get_string('sizekb');
6401 $b = get_string('sizeb');
6404 if ($size >= 1073741824) {
6405 $size = round($size / 1073741824 * 10) / 10 . $gb;
6406 } else if ($size >= 1048576) {
6407 $size = round($size / 1048576 * 10) / 10 . $mb;
6408 } else if ($size >= 1024) {
6409 $size = round($size / 1024 * 10) / 10 . $kb;
6410 } else {
6411 $size = intval($size) .' '. $b; // file sizes over 2GB can not work in 32bit PHP anyway
6413 return $size;
6417 * Cleans a given filename by removing suspicious or troublesome characters
6418 * @see clean_param()
6420 * @uses PARAM_FILE
6421 * @param string $string file name
6422 * @return string cleaned file name
6424 function clean_filename($string) {
6425 return clean_param($string, PARAM_FILE);
6429 /// STRING TRANSLATION ////////////////////////////////////////
6432 * Returns the code for the current language
6434 * @category string
6435 * @return string
6437 function current_language() {
6438 global $CFG, $USER, $SESSION, $COURSE;
6440 if (!empty($COURSE->id) and $COURSE->id != SITEID and !empty($COURSE->lang)) { // Course language can override all other settings for this page
6441 $return = $COURSE->lang;
6443 } else if (!empty($SESSION->lang)) { // Session language can override other settings
6444 $return = $SESSION->lang;
6446 } else if (!empty($USER->lang)) {
6447 $return = $USER->lang;
6449 } else if (isset($CFG->lang)) {
6450 $return = $CFG->lang;
6452 } else {
6453 $return = 'en';
6456 $return = str_replace('_utf8', '', $return); // Just in case this slipped in from somewhere by accident
6458 return $return;
6462 * Returns parent language of current active language if defined
6464 * @category string
6465 * @uses COURSE
6466 * @uses SESSION
6467 * @param string $lang null means current language
6468 * @return string
6470 function get_parent_language($lang=null) {
6471 global $COURSE, $SESSION;
6473 //let's hack around the current language
6474 if (!empty($lang)) {
6475 $old_course_lang = empty($COURSE->lang) ? '' : $COURSE->lang;
6476 $old_session_lang = empty($SESSION->lang) ? '' : $SESSION->lang;
6477 $COURSE->lang = '';
6478 $SESSION->lang = $lang;
6481 $parentlang = get_string('parentlanguage', 'langconfig');
6482 if ($parentlang === 'en') {
6483 $parentlang = '';
6486 //let's hack around the current language
6487 if (!empty($lang)) {
6488 $COURSE->lang = $old_course_lang;
6489 $SESSION->lang = $old_session_lang;
6492 return $parentlang;
6496 * Returns current string_manager instance.
6498 * The param $forcereload is needed for CLI installer only where the string_manager instance
6499 * must be replaced during the install.php script life time.
6501 * @category string
6502 * @param bool $forcereload shall the singleton be released and new instance created instead?
6503 * @return string_manager
6505 function get_string_manager($forcereload=false) {
6506 global $CFG;
6508 static $singleton = null;
6510 if ($forcereload) {
6511 $singleton = null;
6513 if ($singleton === null) {
6514 if (empty($CFG->early_install_lang)) {
6516 if (empty($CFG->langlist)) {
6517 $translist = array();
6518 } else {
6519 $translist = explode(',', $CFG->langlist);
6522 if (empty($CFG->langmenucachefile)) {
6523 $langmenucache = $CFG->cachedir . '/languages';
6524 } else {
6525 $langmenucache = $CFG->langmenucachefile;
6528 $singleton = new core_string_manager($CFG->langotherroot, $CFG->langlocalroot,
6529 !empty($CFG->langstringcache), $translist, $langmenucache);
6531 } else {
6532 $singleton = new install_string_manager();
6536 return $singleton;
6541 * Interface for string manager
6543 * Interface describing class which is responsible for getting
6544 * of localised strings from language packs.
6546 * @package core
6547 * @copyright 2010 Petr Skoda (http://skodak.org)
6548 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6550 interface string_manager {
6552 * Get String returns a requested string
6554 * @param string $identifier The identifier of the string to search for
6555 * @param string $component The module the string is associated with
6556 * @param string|object|array $a An object, string or number that can be used
6557 * within translation strings
6558 * @param string $lang moodle translation language, NULL means use current
6559 * @return string The String !
6561 public function get_string($identifier, $component = '', $a = NULL, $lang = NULL);
6564 * Does the string actually exist?
6566 * get_string() is throwing debug warnings, sometimes we do not want them
6567 * or we want to display better explanation of the problem.
6569 * Use with care!
6571 * @param string $identifier The identifier of the string to search for
6572 * @param string $component The module the string is associated with
6573 * @return boot true if exists
6575 public function string_exists($identifier, $component);
6578 * Returns a localised list of all country names, sorted by country keys.
6579 * @param bool $returnall return all or just enabled
6580 * @param string $lang moodle translation language, NULL means use current
6581 * @return array two-letter country code => translated name.
6583 public function get_list_of_countries($returnall = false, $lang = NULL);
6586 * Returns a localised list of languages, sorted by code keys.
6588 * @param string $lang moodle translation language, NULL means use current
6589 * @param string $standard language list standard
6590 * iso6392: three-letter language code (ISO 639-2/T) => translated name.
6591 * @return array language code => translated name
6593 public function get_list_of_languages($lang = NULL, $standard = 'iso6392');
6596 * Checks if the translation exists for the language
6598 * @param string $lang moodle translation language code
6599 * @param bool $includeall include also disabled translations
6600 * @return bool true if exists
6602 public function translation_exists($lang, $includeall = true);
6605 * Returns localised list of installed translations
6606 * @param bool $returnall return all or just enabled
6607 * @return array moodle translation code => localised translation name
6609 public function get_list_of_translations($returnall = false);
6612 * Returns localised list of currencies.
6614 * @param string $lang moodle translation language, NULL means use current
6615 * @return array currency code => localised currency name
6617 public function get_list_of_currencies($lang = NULL);
6620 * Load all strings for one component
6621 * @param string $component The module the string is associated with
6622 * @param string $lang
6623 * @param bool $disablecache Do not use caches, force fetching the strings from sources
6624 * @param bool $disablelocal Do not use customized strings in xx_local language packs
6625 * @return array of all string for given component and lang
6627 public function load_component_strings($component, $lang, $disablecache=false, $disablelocal=false);
6630 * Invalidates all caches, should the implementation use any
6631 * @param bool $phpunitreset true means called from our PHPUnit integration test reset
6633 public function reset_caches($phpunitreset = false);
6636 * Returns string revision counter, this is incremented after any
6637 * string cache reset.
6638 * @return int lang string revision counter, -1 if unknown
6640 public function get_revision();
6645 * Standard string_manager implementation
6647 * Implements string_manager with getting and printing localised strings
6649 * @package core
6650 * @category string
6651 * @copyright 2010 Petr Skoda (http://skodak.org)
6652 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6654 class core_string_manager implements string_manager {
6655 /** @var string location of all packs except 'en' */
6656 protected $otherroot;
6657 /** @var string location of all lang pack local modifications */
6658 protected $localroot;
6659 /** @var cache lang string cache - it will be optimised more later */
6660 protected $cache;
6661 /** @var int get_string() counter */
6662 protected $countgetstring = 0;
6663 /** @var bool use disk cache */
6664 protected $usecache;
6665 /** @var array limit list of translations */
6666 protected $translist;
6667 /** @var string location of a file that caches the list of available translations */
6668 protected $menucache;
6671 * Create new instance of string manager
6673 * @param string $otherroot location of downlaoded lang packs - usually $CFG->dataroot/lang
6674 * @param string $localroot usually the same as $otherroot
6675 * @param bool $usecache use disk cache
6676 * @param array $translist limit list of visible translations
6677 * @param string $menucache the location of a file that caches the list of available translations
6679 public function __construct($otherroot, $localroot, $usecache, $translist, $menucache) {
6680 $this->otherroot = $otherroot;
6681 $this->localroot = $localroot;
6682 $this->usecache = $usecache;
6683 $this->translist = $translist;
6684 $this->menucache = $menucache;
6686 if ($this->usecache) {
6687 // We can use a proper cache, establish the cache using the 'String cache' definition.
6688 $this->cache = cache::make('core', 'string');
6689 } else {
6690 // We only want a cache for the length of the request, create a static cache.
6691 $options = array(
6692 'simplekeys' => true,
6693 'simpledata' => true
6695 $this->cache = cache::make_from_params(cache_store::MODE_REQUEST, 'core', 'string', array(), $options);
6700 * Returns list of all explicit parent languages for the given language.
6702 * English (en) is considered as the top implicit parent of all language packs
6703 * and is not included in the returned list. The language itself is appended to the
6704 * end of the list. The method is aware of circular dependency risk.
6706 * @see self::populate_parent_languages()
6707 * @param string $lang the code of the language
6708 * @return array all explicit parent languages with the lang itself appended
6710 public function get_language_dependencies($lang) {
6711 return $this->populate_parent_languages($lang);
6715 * Load all strings for one component
6717 * @param string $component The module the string is associated with
6718 * @param string $lang
6719 * @param bool $disablecache Do not use caches, force fetching the strings from sources
6720 * @param bool $disablelocal Do not use customized strings in xx_local language packs
6721 * @return array of all string for given component and lang
6723 public function load_component_strings($component, $lang, $disablecache=false, $disablelocal=false) {
6724 global $CFG;
6726 list($plugintype, $pluginname) = normalize_component($component);
6727 if ($plugintype == 'core' and is_null($pluginname)) {
6728 $component = 'core';
6729 } else {
6730 $component = $plugintype . '_' . $pluginname;
6733 $cachekey = $lang.'_'.$component;
6735 if (!$disablecache and !$disablelocal) {
6736 $string = $this->cache->get($cachekey);
6737 if ($string) {
6738 return $string;
6742 // no cache found - let us merge all possible sources of the strings
6743 if ($plugintype === 'core') {
6744 $file = $pluginname;
6745 if ($file === null) {
6746 $file = 'moodle';
6748 $string = array();
6749 // first load english pack
6750 if (!file_exists("$CFG->dirroot/lang/en/$file.php")) {
6751 return array();
6753 include("$CFG->dirroot/lang/en/$file.php");
6754 $originalkeys = array_keys($string);
6755 $originalkeys = array_flip($originalkeys);
6757 // and then corresponding local if present and allowed
6758 if (!$disablelocal and file_exists("$this->localroot/en_local/$file.php")) {
6759 include("$this->localroot/en_local/$file.php");
6761 // now loop through all langs in correct order
6762 $deps = $this->get_language_dependencies($lang);
6763 foreach ($deps as $dep) {
6764 // the main lang string location
6765 if (file_exists("$this->otherroot/$dep/$file.php")) {
6766 include("$this->otherroot/$dep/$file.php");
6768 if (!$disablelocal and file_exists("$this->localroot/{$dep}_local/$file.php")) {
6769 include("$this->localroot/{$dep}_local/$file.php");
6773 } else {
6774 if (!$location = get_plugin_directory($plugintype, $pluginname) or !is_dir($location)) {
6775 return array();
6777 if ($plugintype === 'mod') {
6778 // bloody mod hack
6779 $file = $pluginname;
6780 } else {
6781 $file = $plugintype . '_' . $pluginname;
6783 $string = array();
6784 // first load English pack
6785 if (!file_exists("$location/lang/en/$file.php")) {
6786 //English pack does not exist, so do not try to load anything else
6787 return array();
6789 include("$location/lang/en/$file.php");
6790 $originalkeys = array_keys($string);
6791 $originalkeys = array_flip($originalkeys);
6792 // and then corresponding local english if present
6793 if (!$disablelocal and file_exists("$this->localroot/en_local/$file.php")) {
6794 include("$this->localroot/en_local/$file.php");
6797 // now loop through all langs in correct order
6798 $deps = $this->get_language_dependencies($lang);
6799 foreach ($deps as $dep) {
6800 // legacy location - used by contrib only
6801 if (file_exists("$location/lang/$dep/$file.php")) {
6802 include("$location/lang/$dep/$file.php");
6804 // the main lang string location
6805 if (file_exists("$this->otherroot/$dep/$file.php")) {
6806 include("$this->otherroot/$dep/$file.php");
6808 // local customisations
6809 if (!$disablelocal and file_exists("$this->localroot/{$dep}_local/$file.php")) {
6810 include("$this->localroot/{$dep}_local/$file.php");
6815 // we do not want any extra strings from other languages - everything must be in en lang pack
6816 $string = array_intersect_key($string, $originalkeys);
6818 if (!$disablelocal) {
6819 // now we have a list of strings from all possible sources. put it into both in-memory and on-disk
6820 // caches so we do not need to do all this merging and dependencies resolving again
6821 $this->cache->set($cachekey, $string);
6823 return $string;
6827 * Does the string actually exist?
6829 * get_string() is throwing debug warnings, sometimes we do not want them
6830 * or we want to display better explanation of the problem.
6831 * Note: Use with care!
6833 * @param string $identifier The identifier of the string to search for
6834 * @param string $component The module the string is associated with
6835 * @return boot true if exists
6837 public function string_exists($identifier, $component) {
6838 $lang = current_language();
6839 $string = $this->load_component_strings($component, $lang);
6840 return isset($string[$identifier]);
6844 * Get String returns a requested string
6846 * @param string $identifier The identifier of the string to search for
6847 * @param string $component The module the string is associated with
6848 * @param string|object|array $a An object, string or number that can be used
6849 * within translation strings
6850 * @param string $lang moodle translation language, NULL means use current
6851 * @return string The String !
6853 public function get_string($identifier, $component = '', $a = NULL, $lang = NULL) {
6854 $this->countgetstring++;
6855 // there are very many uses of these time formating strings without the 'langconfig' component,
6856 // it would not be reasonable to expect that all of them would be converted during 2.0 migration
6857 static $langconfigstrs = array(
6858 'strftimedate' => 1,
6859 'strftimedatefullshort' => 1,
6860 'strftimedateshort' => 1,
6861 'strftimedatetime' => 1,
6862 'strftimedatetimeshort' => 1,
6863 'strftimedaydate' => 1,
6864 'strftimedaydatetime' => 1,
6865 'strftimedayshort' => 1,
6866 'strftimedaytime' => 1,
6867 'strftimemonthyear' => 1,
6868 'strftimerecent' => 1,
6869 'strftimerecentfull' => 1,
6870 'strftimetime' => 1);
6872 if (empty($component)) {
6873 if (isset($langconfigstrs[$identifier])) {
6874 $component = 'langconfig';
6875 } else {
6876 $component = 'moodle';
6880 if ($lang === NULL) {
6881 $lang = current_language();
6884 $string = $this->load_component_strings($component, $lang);
6886 if (!isset($string[$identifier])) {
6887 if ($component === 'pix' or $component === 'core_pix') {
6888 // this component contains only alt tags for emoticons,
6889 // not all of them are supposed to be defined
6890 return '';
6892 if ($identifier === 'parentlanguage' and ($component === 'langconfig' or $component === 'core_langconfig')) {
6893 // parentlanguage is a special string, undefined means use English if not defined
6894 return 'en';
6896 if ($this->usecache) {
6897 // maybe the on-disk cache is dirty - let the last attempt be to find the string in original sources,
6898 // do NOT write the results to disk cache because it may end up in race conditions see MDL-31904
6899 $this->usecache = false;
6900 $string = $this->load_component_strings($component, $lang, true);
6901 $this->usecache = true;
6903 if (!isset($string[$identifier])) {
6904 // the string is still missing - should be fixed by developer
6905 list($plugintype, $pluginname) = normalize_component($component);
6906 if ($plugintype == 'core') {
6907 $file = "lang/en/{$component}.php";
6908 } else if ($plugintype == 'mod') {
6909 $file = "mod/{$pluginname}/lang/en/{$pluginname}.php";
6910 } else {
6911 $path = get_plugin_directory($plugintype, $pluginname);
6912 $file = "{$path}/lang/en/{$plugintype}_{$pluginname}.php";
6914 debugging("Invalid get_string() identifier: '{$identifier}' or component '{$component}'. " .
6915 "Perhaps you are missing \$string['{$identifier}'] = ''; in {$file}?", DEBUG_DEVELOPER);
6916 return "[[$identifier]]";
6920 $string = $string[$identifier];
6922 if ($a !== NULL) {
6923 // Process array's and objects (except lang_strings)
6924 if (is_array($a) or (is_object($a) && !($a instanceof lang_string))) {
6925 $a = (array)$a;
6926 $search = array();
6927 $replace = array();
6928 foreach ($a as $key=>$value) {
6929 if (is_int($key)) {
6930 // we do not support numeric keys - sorry!
6931 continue;
6933 if (is_array($value) or (is_object($value) && !($value instanceof lang_string))) {
6934 // we support just string or lang_string as value
6935 continue;
6937 $search[] = '{$a->'.$key.'}';
6938 $replace[] = (string)$value;
6940 if ($search) {
6941 $string = str_replace($search, $replace, $string);
6943 } else {
6944 $string = str_replace('{$a}', (string)$a, $string);
6948 return $string;
6952 * Returns information about the string_manager performance
6954 * @return array
6956 public function get_performance_summary() {
6957 return array(array(
6958 'langcountgetstring' => $this->countgetstring,
6959 ), array(
6960 'langcountgetstring' => 'get_string calls',
6965 * Returns a localised list of all country names, sorted by localised name.
6967 * @param bool $returnall return all or just enabled
6968 * @param string $lang moodle translation language, NULL means use current
6969 * @return array two-letter country code => translated name.
6971 public function get_list_of_countries($returnall = false, $lang = NULL) {
6972 global $CFG;
6974 if ($lang === NULL) {
6975 $lang = current_language();
6978 $countries = $this->load_component_strings('core_countries', $lang);
6979 collatorlib::asort($countries);
6980 if (!$returnall and !empty($CFG->allcountrycodes)) {
6981 $enabled = explode(',', $CFG->allcountrycodes);
6982 $return = array();
6983 foreach ($enabled as $c) {
6984 if (isset($countries[$c])) {
6985 $return[$c] = $countries[$c];
6988 return $return;
6991 return $countries;
6995 * Returns a localised list of languages, sorted by code keys.
6997 * @param string $lang moodle translation language, NULL means use current
6998 * @param string $standard language list standard
6999 * - iso6392: three-letter language code (ISO 639-2/T) => translated name
7000 * - iso6391: two-letter langauge code (ISO 639-1) => translated name
7001 * @return array language code => translated name
7003 public function get_list_of_languages($lang = NULL, $standard = 'iso6391') {
7004 if ($lang === NULL) {
7005 $lang = current_language();
7008 if ($standard === 'iso6392') {
7009 $langs = $this->load_component_strings('core_iso6392', $lang);
7010 ksort($langs);
7011 return $langs;
7013 } else if ($standard === 'iso6391') {
7014 $langs2 = $this->load_component_strings('core_iso6392', $lang);
7015 static $mapping = array('aar' => 'aa', 'abk' => 'ab', 'afr' => 'af', 'aka' => 'ak', 'sqi' => 'sq', 'amh' => 'am', 'ara' => 'ar', 'arg' => 'an', 'hye' => 'hy',
7016 'asm' => 'as', 'ava' => 'av', 'ave' => 'ae', 'aym' => 'ay', 'aze' => 'az', 'bak' => 'ba', 'bam' => 'bm', 'eus' => 'eu', 'bel' => 'be', 'ben' => 'bn', 'bih' => 'bh',
7017 'bis' => 'bi', 'bos' => 'bs', 'bre' => 'br', 'bul' => 'bg', 'mya' => 'my', 'cat' => 'ca', 'cha' => 'ch', 'che' => 'ce', 'zho' => 'zh', 'chu' => 'cu', 'chv' => 'cv',
7018 'cor' => 'kw', 'cos' => 'co', 'cre' => 'cr', 'ces' => 'cs', 'dan' => 'da', 'div' => 'dv', 'nld' => 'nl', 'dzo' => 'dz', 'eng' => 'en', 'epo' => 'eo', 'est' => 'et',
7019 'ewe' => 'ee', 'fao' => 'fo', 'fij' => 'fj', 'fin' => 'fi', 'fra' => 'fr', 'fry' => 'fy', 'ful' => 'ff', 'kat' => 'ka', 'deu' => 'de', 'gla' => 'gd', 'gle' => 'ga',
7020 'glg' => 'gl', 'glv' => 'gv', 'ell' => 'el', 'grn' => 'gn', 'guj' => 'gu', 'hat' => 'ht', 'hau' => 'ha', 'heb' => 'he', 'her' => 'hz', 'hin' => 'hi', 'hmo' => 'ho',
7021 'hrv' => 'hr', 'hun' => 'hu', 'ibo' => 'ig', 'isl' => 'is', 'ido' => 'io', 'iii' => 'ii', 'iku' => 'iu', 'ile' => 'ie', 'ina' => 'ia', 'ind' => 'id', 'ipk' => 'ik',
7022 'ita' => 'it', 'jav' => 'jv', 'jpn' => 'ja', 'kal' => 'kl', 'kan' => 'kn', 'kas' => 'ks', 'kau' => 'kr', 'kaz' => 'kk', 'khm' => 'km', 'kik' => 'ki', 'kin' => 'rw',
7023 'kir' => 'ky', 'kom' => 'kv', 'kon' => 'kg', 'kor' => 'ko', 'kua' => 'kj', 'kur' => 'ku', 'lao' => 'lo', 'lat' => 'la', 'lav' => 'lv', 'lim' => 'li', 'lin' => 'ln',
7024 'lit' => 'lt', 'ltz' => 'lb', 'lub' => 'lu', 'lug' => 'lg', 'mkd' => 'mk', 'mah' => 'mh', 'mal' => 'ml', 'mri' => 'mi', 'mar' => 'mr', 'msa' => 'ms', 'mlg' => 'mg',
7025 'mlt' => 'mt', 'mon' => 'mn', 'nau' => 'na', 'nav' => 'nv', 'nbl' => 'nr', 'nde' => 'nd', 'ndo' => 'ng', 'nep' => 'ne', 'nno' => 'nn', 'nob' => 'nb', 'nor' => 'no',
7026 'nya' => 'ny', 'oci' => 'oc', 'oji' => 'oj', 'ori' => 'or', 'orm' => 'om', 'oss' => 'os', 'pan' => 'pa', 'fas' => 'fa', 'pli' => 'pi', 'pol' => 'pl', 'por' => 'pt',
7027 'pus' => 'ps', 'que' => 'qu', 'roh' => 'rm', 'ron' => 'ro', 'run' => 'rn', 'rus' => 'ru', 'sag' => 'sg', 'san' => 'sa', 'sin' => 'si', 'slk' => 'sk', 'slv' => 'sl',
7028 'sme' => 'se', 'smo' => 'sm', 'sna' => 'sn', 'snd' => 'sd', 'som' => 'so', 'sot' => 'st', 'spa' => 'es', 'srd' => 'sc', 'srp' => 'sr', 'ssw' => 'ss', 'sun' => 'su',
7029 'swa' => 'sw', 'swe' => 'sv', 'tah' => 'ty', 'tam' => 'ta', 'tat' => 'tt', 'tel' => 'te', 'tgk' => 'tg', 'tgl' => 'tl', 'tha' => 'th', 'bod' => 'bo', 'tir' => 'ti',
7030 'ton' => 'to', 'tsn' => 'tn', 'tso' => 'ts', 'tuk' => 'tk', 'tur' => 'tr', 'twi' => 'tw', 'uig' => 'ug', 'ukr' => 'uk', 'urd' => 'ur', 'uzb' => 'uz', 'ven' => 've',
7031 'vie' => 'vi', 'vol' => 'vo', 'cym' => 'cy', 'wln' => 'wa', 'wol' => 'wo', 'xho' => 'xh', 'yid' => 'yi', 'yor' => 'yo', 'zha' => 'za', 'zul' => 'zu');
7032 $langs1 = array();
7033 foreach ($mapping as $c2=>$c1) {
7034 $langs1[$c1] = $langs2[$c2];
7036 ksort($langs1);
7037 return $langs1;
7039 } else {
7040 debugging('Unsupported $standard parameter in get_list_of_languages() method: '.$standard);
7043 return array();
7047 * Checks if the translation exists for the language
7049 * @param string $lang moodle translation language code
7050 * @param bool $includeall include also disabled translations
7051 * @return bool true if exists
7053 public function translation_exists($lang, $includeall = true) {
7055 if (strpos($lang, '_local') !== false) {
7056 // _local packs are not real translations
7057 return false;
7059 if (!$includeall and !empty($this->translist)) {
7060 if (!in_array($lang, $this->translist)) {
7061 return false;
7064 if ($lang === 'en') {
7065 // part of distribution
7066 return true;
7068 return file_exists("$this->otherroot/$lang/langconfig.php");
7072 * Returns localised list of installed translations
7074 * @param bool $returnall return all or just enabled
7075 * @return array moodle translation code => localised translation name
7077 public function get_list_of_translations($returnall = false) {
7078 global $CFG;
7080 $languages = array();
7082 if (!empty($CFG->langcache) and is_readable($this->menucache)) {
7083 // try to re-use the cached list of all available languages
7084 $cachedlist = json_decode(file_get_contents($this->menucache), true);
7086 if (is_array($cachedlist) and !empty($cachedlist)) {
7087 // the cache file is restored correctly
7089 if (!$returnall and !empty($this->translist)) {
7090 // return just enabled translations
7091 foreach ($cachedlist as $langcode => $langname) {
7092 if (in_array($langcode, $this->translist)) {
7093 $languages[$langcode] = $langname;
7096 return $languages;
7098 } else {
7099 // return all translations
7100 return $cachedlist;
7105 // the cached list of languages is not available, let us populate the list
7107 if (!$returnall and !empty($this->translist)) {
7108 // return only some translations
7109 foreach ($this->translist as $lang) {
7110 $lang = trim($lang); //Just trim spaces to be a bit more permissive
7111 if (strstr($lang, '_local') !== false) {
7112 continue;
7114 if (strstr($lang, '_utf8') !== false) {
7115 continue;
7117 if ($lang !== 'en' and !file_exists("$this->otherroot/$lang/langconfig.php")) {
7118 // some broken or missing lang - can not switch to it anyway
7119 continue;
7121 $string = $this->load_component_strings('langconfig', $lang);
7122 if (!empty($string['thislanguage'])) {
7123 $languages[$lang] = $string['thislanguage'].' ('. $lang .')';
7125 unset($string);
7128 } else {
7129 // return all languages available in system
7130 $langdirs = get_list_of_plugins('', '', $this->otherroot);
7132 $langdirs = array_merge($langdirs, array("$CFG->dirroot/lang/en"=>'en'));
7133 // Sort all
7135 // Loop through all langs and get info
7136 foreach ($langdirs as $lang) {
7137 if (strstr($lang, '_local') !== false) {
7138 continue;
7140 if (strstr($lang, '_utf8') !== false) {
7141 continue;
7143 $string = $this->load_component_strings('langconfig', $lang);
7144 if (!empty($string['thislanguage'])) {
7145 $languages[$lang] = $string['thislanguage'].' ('. $lang .')';
7147 unset($string);
7150 if (!empty($CFG->langcache) and !empty($this->menucache)) {
7151 // cache the list so that it can be used next time
7152 collatorlib::asort($languages);
7153 check_dir_exists(dirname($this->menucache), true, true);
7154 file_put_contents($this->menucache, json_encode($languages));
7158 collatorlib::asort($languages);
7160 return $languages;
7164 * Returns localised list of currencies.
7166 * @param string $lang moodle translation language, NULL means use current
7167 * @return array currency code => localised currency name
7169 public function get_list_of_currencies($lang = NULL) {
7170 if ($lang === NULL) {
7171 $lang = current_language();
7174 $currencies = $this->load_component_strings('core_currencies', $lang);
7175 asort($currencies);
7177 return $currencies;
7181 * Clears both in-memory and on-disk caches
7182 * @param bool $phpunitreset true means called from our PHPUnit integration test reset
7184 public function reset_caches($phpunitreset = false) {
7185 global $CFG;
7186 require_once("$CFG->libdir/filelib.php");
7188 // clear the on-disk disk with aggregated string files
7189 $this->cache->purge();
7191 if (!$phpunitreset) {
7192 // Increment the revision counter.
7193 $langrev = get_config('core', 'langrev');
7194 $next = time();
7195 if ($langrev !== false and $next <= $langrev and $langrev - $next < 60*60) {
7196 // This resolves problems when reset is requested repeatedly within 1s,
7197 // the < 1h condition prevents accidental switching to future dates
7198 // because we might not recover from it.
7199 $next = $langrev+1;
7201 set_config('langrev', $next);
7204 // clear the cache containing the list of available translations
7205 // and re-populate it again
7206 fulldelete($this->menucache);
7207 $this->get_list_of_translations(true);
7211 * Returns string revision counter, this is incremented after any
7212 * string cache reset.
7213 * @return int lang string revision counter, -1 if unknown
7215 public function get_revision() {
7216 global $CFG;
7217 if (isset($CFG->langrev)) {
7218 return (int)$CFG->langrev;
7219 } else {
7220 return -1;
7224 /// End of external API ////////////////////////////////////////////////////
7227 * Helper method that recursively loads all parents of the given language.
7229 * @see self::get_language_dependencies()
7230 * @param string $lang language code
7231 * @param array $stack list of parent languages already populated in previous recursive calls
7232 * @return array list of all parents of the given language with the $lang itself added as the last element
7234 protected function populate_parent_languages($lang, array $stack = array()) {
7236 // English does not have a parent language.
7237 if ($lang === 'en') {
7238 return $stack;
7241 // Prevent circular dependency (and thence the infinitive recursion loop).
7242 if (in_array($lang, $stack)) {
7243 return $stack;
7246 // Load language configuration and look for the explicit parent language.
7247 if (!file_exists("$this->otherroot/$lang/langconfig.php")) {
7248 return $stack;
7250 $string = array();
7251 include("$this->otherroot/$lang/langconfig.php");
7253 if (empty($string['parentlanguage']) or $string['parentlanguage'] === 'en') {
7254 unset($string);
7255 return array_merge(array($lang), $stack);
7257 } else {
7258 $parentlang = $string['parentlanguage'];
7259 unset($string);
7260 return $this->populate_parent_languages($parentlang, array_merge(array($lang), $stack));
7267 * Fetches minimum strings for installation
7269 * Minimalistic string fetching implementation
7270 * that is used in installer before we fetch the wanted
7271 * language pack from moodle.org lang download site.
7273 * @package core
7274 * @copyright 2010 Petr Skoda (http://skodak.org)
7275 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
7277 class install_string_manager implements string_manager {
7278 /** @var string location of pre-install packs for all langs */
7279 protected $installroot;
7282 * Crate new instance of install string manager
7284 public function __construct() {
7285 global $CFG;
7286 $this->installroot = "$CFG->dirroot/install/lang";
7290 * Load all strings for one component
7291 * @param string $component The module the string is associated with
7292 * @param string $lang
7293 * @param bool $disablecache Do not use caches, force fetching the strings from sources
7294 * @param bool $disablelocal Do not use customized strings in xx_local language packs
7295 * @return array of all string for given component and lang
7297 public function load_component_strings($component, $lang, $disablecache=false, $disablelocal=false) {
7298 // not needed in installer
7299 return array();
7303 * Does the string actually exist?
7305 * get_string() is throwing debug warnings, sometimes we do not want them
7306 * or we want to display better explanation of the problem.
7308 * Use with care!
7310 * @param string $identifier The identifier of the string to search for
7311 * @param string $component The module the string is associated with
7312 * @return boot true if exists
7314 public function string_exists($identifier, $component) {
7315 // simple old style hack ;)
7316 $str = get_string($identifier, $component);
7317 return (strpos($str, '[[') === false);
7321 * Get String returns a requested string
7323 * @param string $identifier The identifier of the string to search for
7324 * @param string $component The module the string is associated with
7325 * @param string|object|array $a An object, string or number that can be used
7326 * within translation strings
7327 * @param string $lang moodle translation language, NULL means use current
7328 * @return string The String !
7330 public function get_string($identifier, $component = '', $a = NULL, $lang = NULL) {
7331 if (!$component) {
7332 $component = 'moodle';
7335 if ($lang === NULL) {
7336 $lang = current_language();
7339 //get parent lang
7340 $parent = '';
7341 if ($lang !== 'en' and $identifier !== 'parentlanguage' and $component !== 'langconfig') {
7342 if (file_exists("$this->installroot/$lang/langconfig.php")) {
7343 $string = array();
7344 include("$this->installroot/$lang/langconfig.php");
7345 if (isset($string['parentlanguage'])) {
7346 $parent = $string['parentlanguage'];
7348 unset($string);
7352 // include en string first
7353 if (!file_exists("$this->installroot/en/$component.php")) {
7354 return "[[$identifier]]";
7356 $string = array();
7357 include("$this->installroot/en/$component.php");
7359 // now override en with parent if defined
7360 if ($parent and $parent !== 'en' and file_exists("$this->installroot/$parent/$component.php")) {
7361 include("$this->installroot/$parent/$component.php");
7364 // finally override with requested language
7365 if ($lang !== 'en' and file_exists("$this->installroot/$lang/$component.php")) {
7366 include("$this->installroot/$lang/$component.php");
7369 if (!isset($string[$identifier])) {
7370 return "[[$identifier]]";
7373 $string = $string[$identifier];
7375 if ($a !== NULL) {
7376 if (is_object($a) or is_array($a)) {
7377 $a = (array)$a;
7378 $search = array();
7379 $replace = array();
7380 foreach ($a as $key=>$value) {
7381 if (is_int($key)) {
7382 // we do not support numeric keys - sorry!
7383 continue;
7385 $search[] = '{$a->'.$key.'}';
7386 $replace[] = (string)$value;
7388 if ($search) {
7389 $string = str_replace($search, $replace, $string);
7391 } else {
7392 $string = str_replace('{$a}', (string)$a, $string);
7396 return $string;
7400 * Returns a localised list of all country names, sorted by country keys.
7402 * @param bool $returnall return all or just enabled
7403 * @param string $lang moodle translation language, NULL means use current
7404 * @return array two-letter country code => translated name.
7406 public function get_list_of_countries($returnall = false, $lang = NULL) {
7407 //not used in installer
7408 return array();
7412 * Returns a localised list of languages, sorted by code keys.
7414 * @param string $lang moodle translation language, NULL means use current
7415 * @param string $standard language list standard
7416 * iso6392: three-letter language code (ISO 639-2/T) => translated name.
7417 * @return array language code => translated name
7419 public function get_list_of_languages($lang = NULL, $standard = 'iso6392') {
7420 //not used in installer
7421 return array();
7425 * Checks if the translation exists for the language
7427 * @param string $lang moodle translation language code
7428 * @param bool $includeall include also disabled translations
7429 * @return bool true if exists
7431 public function translation_exists($lang, $includeall = true) {
7432 return file_exists($this->installroot.'/'.$lang.'/langconfig.php');
7436 * Returns localised list of installed translations
7437 * @param bool $returnall return all or just enabled
7438 * @return array moodle translation code => localised translation name
7440 public function get_list_of_translations($returnall = false) {
7441 // return all is ignored here - we need to know all langs in installer
7442 $languages = array();
7443 // Get raw list of lang directories
7444 $langdirs = get_list_of_plugins('install/lang');
7445 asort($langdirs);
7446 // Get some info from each lang
7447 foreach ($langdirs as $lang) {
7448 if (file_exists($this->installroot.'/'.$lang.'/langconfig.php')) {
7449 $string = array();
7450 include($this->installroot.'/'.$lang.'/langconfig.php');
7451 if (!empty($string['thislanguage'])) {
7452 $languages[$lang] = $string['thislanguage'].' ('.$lang.')';
7456 // Return array
7457 return $languages;
7461 * Returns localised list of currencies.
7463 * @param string $lang moodle translation language, NULL means use current
7464 * @return array currency code => localised currency name
7466 public function get_list_of_currencies($lang = NULL) {
7467 // not used in installer
7468 return array();
7472 * This implementation does not use any caches
7473 * @param bool $phpunitreset true means called from our PHPUnit integration test reset
7475 public function reset_caches($phpunitreset = false) {
7476 // Nothing to do.
7480 * Returns string revision counter, this is incremented after any
7481 * string cache reset.
7482 * @return int lang string revision counter, -1 if unknown
7484 public function get_revision() {
7485 return -1;
7491 * Returns a localized string.
7493 * Returns the translated string specified by $identifier as
7494 * for $module. Uses the same format files as STphp.
7495 * $a is an object, string or number that can be used
7496 * within translation strings
7498 * eg 'hello {$a->firstname} {$a->lastname}'
7499 * or 'hello {$a}'
7501 * If you would like to directly echo the localized string use
7502 * the function {@link print_string()}
7504 * Example usage of this function involves finding the string you would
7505 * like a local equivalent of and using its identifier and module information
7506 * to retrieve it.<br/>
7507 * If you open moodle/lang/en/moodle.php and look near line 278
7508 * you will find a string to prompt a user for their word for 'course'
7509 * <code>
7510 * $string['course'] = 'Course';
7511 * </code>
7512 * So if you want to display the string 'Course'
7513 * in any language that supports it on your site
7514 * you just need to use the identifier 'course'
7515 * <code>
7516 * $mystring = '<strong>'. get_string('course') .'</strong>';
7517 * or
7518 * </code>
7519 * If the string you want is in another file you'd take a slightly
7520 * different approach. Looking in moodle/lang/en/calendar.php you find
7521 * around line 75:
7522 * <code>
7523 * $string['typecourse'] = 'Course event';
7524 * </code>
7525 * If you want to display the string "Course event" in any language
7526 * supported you would use the identifier 'typecourse' and the module 'calendar'
7527 * (because it is in the file calendar.php):
7528 * <code>
7529 * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>';
7530 * </code>
7532 * As a last resort, should the identifier fail to map to a string
7533 * the returned string will be [[ $identifier ]]
7535 * In Moodle 2.3 there is a new argument to this function $lazyload.
7536 * Setting $lazyload to true causes get_string to return a lang_string object
7537 * rather than the string itself. The fetching of the string is then put off until
7538 * the string object is first used. The object can be used by calling it's out
7539 * method or by casting the object to a string, either directly e.g.
7540 * (string)$stringobject
7541 * or indirectly by using the string within another string or echoing it out e.g.
7542 * echo $stringobject
7543 * return "<p>{$stringobject}</p>";
7544 * It is worth noting that using $lazyload and attempting to use the string as an
7545 * array key will cause a fatal error as objects cannot be used as array keys.
7546 * But you should never do that anyway!
7547 * For more information {@see lang_string}
7549 * @category string
7550 * @param string $identifier The key identifier for the localized string
7551 * @param string $component The module where the key identifier is stored,
7552 * usually expressed as the filename in the language pack without the
7553 * .php on the end but can also be written as mod/forum or grade/export/xls.
7554 * If none is specified then moodle.php is used.
7555 * @param string|object|array $a An object, string or number that can be used
7556 * within translation strings
7557 * @param bool $lazyload If set to true a string object is returned instead of
7558 * the string itself. The string then isn't calculated until it is first used.
7559 * @return string The localized string.
7561 function get_string($identifier, $component = '', $a = NULL, $lazyload = false) {
7562 global $CFG;
7564 // If the lazy load argument has been supplied return a lang_string object
7565 // instead.
7566 // We need to make sure it is true (and a bool) as you will see below there
7567 // used to be a forth argument at one point.
7568 if ($lazyload === true) {
7569 return new lang_string($identifier, $component, $a);
7572 if (debugging('', DEBUG_DEVELOPER) && clean_param($identifier, PARAM_STRINGID) === '') {
7573 throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.');
7576 // There is now a forth argument again, this time it is a boolean however so
7577 // we can still check for the old extralocations parameter.
7578 if (!is_bool($lazyload) && !empty($lazyload)) {
7579 debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.');
7582 if (strpos($component, '/') !== false) {
7583 debugging('The module name you passed to get_string is the deprecated format ' .
7584 'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER);
7585 $componentpath = explode('/', $component);
7587 switch ($componentpath[0]) {
7588 case 'mod':
7589 $component = $componentpath[1];
7590 break;
7591 case 'blocks':
7592 case 'block':
7593 $component = 'block_'.$componentpath[1];
7594 break;
7595 case 'enrol':
7596 $component = 'enrol_'.$componentpath[1];
7597 break;
7598 case 'format':
7599 $component = 'format_'.$componentpath[1];
7600 break;
7601 case 'grade':
7602 $component = 'grade'.$componentpath[1].'_'.$componentpath[2];
7603 break;
7607 $result = get_string_manager()->get_string($identifier, $component, $a);
7609 // Debugging feature lets you display string identifier and component
7610 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
7611 $result .= ' {' . $identifier . '/' . $component . '}';
7613 return $result;
7617 * Converts an array of strings to their localized value.
7619 * @param array $array An array of strings
7620 * @param string $component The language module that these strings can be found in.
7621 * @return stdClass translated strings.
7623 function get_strings($array, $component = '') {
7624 $string = new stdClass;
7625 foreach ($array as $item) {
7626 $string->$item = get_string($item, $component);
7628 return $string;
7632 * Prints out a translated string.
7634 * Prints out a translated string using the return value from the {@link get_string()} function.
7636 * Example usage of this function when the string is in the moodle.php file:<br/>
7637 * <code>
7638 * echo '<strong>';
7639 * print_string('course');
7640 * echo '</strong>';
7641 * </code>
7643 * Example usage of this function when the string is not in the moodle.php file:<br/>
7644 * <code>
7645 * echo '<h1>';
7646 * print_string('typecourse', 'calendar');
7647 * echo '</h1>';
7648 * </code>
7650 * @category string
7651 * @param string $identifier The key identifier for the localized string
7652 * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used.
7653 * @param string|object|array $a An object, string or number that can be used within translation strings
7655 function print_string($identifier, $component = '', $a = NULL) {
7656 echo get_string($identifier, $component, $a);
7660 * Returns a list of charset codes
7662 * Returns a list of charset codes. It's hardcoded, so they should be added manually
7663 * (checking that such charset is supported by the texlib library!)
7665 * @return array And associative array with contents in the form of charset => charset
7667 function get_list_of_charsets() {
7669 $charsets = array(
7670 'EUC-JP' => 'EUC-JP',
7671 'ISO-2022-JP'=> 'ISO-2022-JP',
7672 'ISO-8859-1' => 'ISO-8859-1',
7673 'SHIFT-JIS' => 'SHIFT-JIS',
7674 'GB2312' => 'GB2312',
7675 'GB18030' => 'GB18030', // gb18030 not supported by typo and mbstring
7676 'UTF-8' => 'UTF-8');
7678 asort($charsets);
7680 return $charsets;
7684 * Returns a list of valid and compatible themes
7686 * @return array
7688 function get_list_of_themes() {
7689 global $CFG;
7691 $themes = array();
7693 if (!empty($CFG->themelist)) { // use admin's list of themes
7694 $themelist = explode(',', $CFG->themelist);
7695 } else {
7696 $themelist = array_keys(get_plugin_list("theme"));
7699 foreach ($themelist as $key => $themename) {
7700 $theme = theme_config::load($themename);
7701 $themes[$themename] = $theme;
7704 collatorlib::asort_objects_by_method($themes, 'get_theme_name');
7706 return $themes;
7710 * Returns a list of timezones in the current language
7712 * @global object
7713 * @global object
7714 * @return array
7716 function get_list_of_timezones() {
7717 global $CFG, $DB;
7719 static $timezones;
7721 if (!empty($timezones)) { // This function has been called recently
7722 return $timezones;
7725 $timezones = array();
7727 if ($rawtimezones = $DB->get_records_sql("SELECT MAX(id), name FROM {timezone} GROUP BY name")) {
7728 foreach($rawtimezones as $timezone) {
7729 if (!empty($timezone->name)) {
7730 if (get_string_manager()->string_exists(strtolower($timezone->name), 'timezones')) {
7731 $timezones[$timezone->name] = get_string(strtolower($timezone->name), 'timezones');
7732 } else {
7733 $timezones[$timezone->name] = $timezone->name;
7735 if (substr($timezones[$timezone->name], 0, 1) == '[') { // No translation found
7736 $timezones[$timezone->name] = $timezone->name;
7742 asort($timezones);
7744 for ($i = -13; $i <= 13; $i += .5) {
7745 $tzstring = 'UTC';
7746 if ($i < 0) {
7747 $timezones[sprintf("%.1f", $i)] = $tzstring . $i;
7748 } else if ($i > 0) {
7749 $timezones[sprintf("%.1f", $i)] = $tzstring . '+' . $i;
7750 } else {
7751 $timezones[sprintf("%.1f", $i)] = $tzstring;
7755 return $timezones;
7759 * Factory function for emoticon_manager
7761 * @return emoticon_manager singleton
7763 function get_emoticon_manager() {
7764 static $singleton = null;
7766 if (is_null($singleton)) {
7767 $singleton = new emoticon_manager();
7770 return $singleton;
7774 * Provides core support for plugins that have to deal with
7775 * emoticons (like HTML editor or emoticon filter).
7777 * Whenever this manager mentiones 'emoticon object', the following data
7778 * structure is expected: stdClass with properties text, imagename, imagecomponent,
7779 * altidentifier and altcomponent
7781 * @see admin_setting_emoticons
7783 class emoticon_manager {
7786 * Returns the currently enabled emoticons
7788 * @return array of emoticon objects
7790 public function get_emoticons() {
7791 global $CFG;
7793 if (empty($CFG->emoticons)) {
7794 return array();
7797 $emoticons = $this->decode_stored_config($CFG->emoticons);
7799 if (!is_array($emoticons)) {
7800 // something is wrong with the format of stored setting
7801 debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL);
7802 return array();
7805 return $emoticons;
7809 * Converts emoticon object into renderable pix_emoticon object
7811 * @param stdClass $emoticon emoticon object
7812 * @param array $attributes explicit HTML attributes to set
7813 * @return pix_emoticon
7815 public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array()) {
7816 $stringmanager = get_string_manager();
7817 if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) {
7818 $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent);
7819 } else {
7820 $alt = s($emoticon->text);
7822 return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes);
7826 * Encodes the array of emoticon objects into a string storable in config table
7828 * @see self::decode_stored_config()
7829 * @param array $emoticons array of emtocion objects
7830 * @return string
7832 public function encode_stored_config(array $emoticons) {
7833 return json_encode($emoticons);
7837 * Decodes the string into an array of emoticon objects
7839 * @see self::encode_stored_config()
7840 * @param string $encoded
7841 * @return string|null
7843 public function decode_stored_config($encoded) {
7844 $decoded = json_decode($encoded);
7845 if (!is_array($decoded)) {
7846 return null;
7848 return $decoded;
7852 * Returns default set of emoticons supported by Moodle
7854 * @return array of sdtClasses
7856 public function default_emoticons() {
7857 return array(
7858 $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'),
7859 $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'),
7860 $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'),
7861 $this->prepare_emoticon_object(";-)", 's/wink', 'wink'),
7862 $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'),
7863 $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'),
7864 $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'),
7865 $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'),
7866 $this->prepare_emoticon_object("B-)", 's/cool', 'cool'),
7867 $this->prepare_emoticon_object("^-)", 's/approve', 'approve'),
7868 $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'),
7869 $this->prepare_emoticon_object(":o)", 's/clown', 'clown'),
7870 $this->prepare_emoticon_object(":-(", 's/sad', 'sad'),
7871 $this->prepare_emoticon_object(":(", 's/sad', 'sad'),
7872 $this->prepare_emoticon_object("8-.", 's/shy', 'shy'),
7873 $this->prepare_emoticon_object(":-I", 's/blush', 'blush'),
7874 $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'),
7875 $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'),
7876 $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'),
7877 $this->prepare_emoticon_object("8-[", 's/angry', 'angry'),
7878 $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'),
7879 $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'),
7880 $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'),
7881 $this->prepare_emoticon_object("}-]", 's/evil', 'evil'),
7882 $this->prepare_emoticon_object("(h)", 's/heart', 'heart'),
7883 $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'),
7884 $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'),
7885 $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'),
7886 $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'),
7887 $this->prepare_emoticon_object("( )", 's/egg', 'egg'),
7892 * Helper method preparing the stdClass with the emoticon properties
7894 * @param string|array $text or array of strings
7895 * @param string $imagename to be used by {@see pix_emoticon}
7896 * @param string $altidentifier alternative string identifier, null for no alt
7897 * @param array $altcomponent where the alternative string is defined
7898 * @param string $imagecomponent to be used by {@see pix_emoticon}
7899 * @return stdClass
7901 protected function prepare_emoticon_object($text, $imagename, $altidentifier = null, $altcomponent = 'core_pix', $imagecomponent = 'core') {
7902 return (object)array(
7903 'text' => $text,
7904 'imagename' => $imagename,
7905 'imagecomponent' => $imagecomponent,
7906 'altidentifier' => $altidentifier,
7907 'altcomponent' => $altcomponent,
7912 /// ENCRYPTION ////////////////////////////////////////////////
7915 * rc4encrypt
7917 * Please note that in this version of moodle that the default for rc4encryption is
7918 * using the slightly more secure password key. There may be an issue when upgrading
7919 * from an older version of moodle.
7921 * @todo MDL-31836 Remove the old password key in version 2.4
7922 * Code also needs to be changed in sessionlib.php
7923 * @see get_moodle_cookie()
7924 * @see set_moodle_cookie()
7926 * @param string $data Data to encrypt.
7927 * @param bool $usesecurekey Lets us know if we are using the old or new secure password key.
7928 * @return string The now encrypted data.
7930 function rc4encrypt($data, $usesecurekey = true) {
7931 if (!$usesecurekey) {
7932 $passwordkey = 'nfgjeingjk';
7933 } else {
7934 $passwordkey = get_site_identifier();
7936 return endecrypt($passwordkey, $data, '');
7940 * rc4decrypt
7942 * Please note that in this version of moodle that the default for rc4encryption is
7943 * using the slightly more secure password key. There may be an issue when upgrading
7944 * from an older version of moodle.
7946 * @todo MDL-31836 Remove the old password key in version 2.4
7947 * Code also needs to be changed in sessionlib.php
7948 * @see get_moodle_cookie()
7949 * @see set_moodle_cookie()
7951 * @param string $data Data to decrypt.
7952 * @param bool $usesecurekey Lets us know if we are using the old or new secure password key.
7953 * @return string The now decrypted data.
7955 function rc4decrypt($data, $usesecurekey = true) {
7956 if (!$usesecurekey) {
7957 $passwordkey = 'nfgjeingjk';
7958 } else {
7959 $passwordkey = get_site_identifier();
7961 return endecrypt($passwordkey, $data, 'de');
7965 * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com]
7967 * @todo Finish documenting this function
7969 * @param string $pwd The password to use when encrypting or decrypting
7970 * @param string $data The data to be decrypted/encrypted
7971 * @param string $case Either 'de' for decrypt or '' for encrypt
7972 * @return string
7974 function endecrypt ($pwd, $data, $case) {
7976 if ($case == 'de') {
7977 $data = urldecode($data);
7980 $key[] = '';
7981 $box[] = '';
7982 $temp_swap = '';
7983 $pwd_length = 0;
7985 $pwd_length = strlen($pwd);
7987 for ($i = 0; $i <= 255; $i++) {
7988 $key[$i] = ord(substr($pwd, ($i % $pwd_length), 1));
7989 $box[$i] = $i;
7992 $x = 0;
7994 for ($i = 0; $i <= 255; $i++) {
7995 $x = ($x + $box[$i] + $key[$i]) % 256;
7996 $temp_swap = $box[$i];
7997 $box[$i] = $box[$x];
7998 $box[$x] = $temp_swap;
8001 $temp = '';
8002 $k = '';
8004 $cipherby = '';
8005 $cipher = '';
8007 $a = 0;
8008 $j = 0;
8010 for ($i = 0; $i < strlen($data); $i++) {
8011 $a = ($a + 1) % 256;
8012 $j = ($j + $box[$a]) % 256;
8013 $temp = $box[$a];
8014 $box[$a] = $box[$j];
8015 $box[$j] = $temp;
8016 $k = $box[(($box[$a] + $box[$j]) % 256)];
8017 $cipherby = ord(substr($data, $i, 1)) ^ $k;
8018 $cipher .= chr($cipherby);
8021 if ($case == 'de') {
8022 $cipher = urldecode(urlencode($cipher));
8023 } else {
8024 $cipher = urlencode($cipher);
8027 return $cipher;
8030 /// ENVIRONMENT CHECKING ////////////////////////////////////////////////////////////
8033 * Returns the exact absolute path to plugin directory.
8035 * @param string $plugintype type of plugin
8036 * @param string $name name of the plugin
8037 * @return string full path to plugin directory; NULL if not found
8039 function get_plugin_directory($plugintype, $name) {
8040 global $CFG;
8042 if ($plugintype === '') {
8043 $plugintype = 'mod';
8046 $types = get_plugin_types(true);
8047 if (!array_key_exists($plugintype, $types)) {
8048 return NULL;
8050 $name = clean_param($name, PARAM_SAFEDIR); // just in case ;-)
8052 if (!empty($CFG->themedir) and $plugintype === 'theme') {
8053 if (!is_dir($types['theme'] . '/' . $name)) {
8054 // ok, so the theme is supposed to be in the $CFG->themedir
8055 return $CFG->themedir . '/' . $name;
8059 return $types[$plugintype].'/'.$name;
8063 * Return exact absolute path to a plugin directory.
8065 * @param string $component name such as 'moodle', 'mod_forum'
8066 * @return string full path to component directory; NULL if not found
8068 function get_component_directory($component) {
8069 global $CFG;
8071 list($type, $plugin) = normalize_component($component);
8073 if ($type === 'core') {
8074 if ($plugin === NULL ) {
8075 $path = $CFG->libdir;
8076 } else {
8077 $subsystems = get_core_subsystems();
8078 if (isset($subsystems[$plugin])) {
8079 $path = $CFG->dirroot.'/'.$subsystems[$plugin];
8080 } else {
8081 $path = NULL;
8085 } else {
8086 $path = get_plugin_directory($type, $plugin);
8089 return $path;
8093 * Normalize the component name using the "frankenstyle" names.
8094 * @param string $component
8095 * @return array $type+$plugin elements
8097 function normalize_component($component) {
8098 if ($component === 'moodle' or $component === 'core') {
8099 $type = 'core';
8100 $plugin = NULL;
8102 } else if (strpos($component, '_') === false) {
8103 $subsystems = get_core_subsystems();
8104 if (array_key_exists($component, $subsystems)) {
8105 $type = 'core';
8106 $plugin = $component;
8107 } else {
8108 // everything else is a module
8109 $type = 'mod';
8110 $plugin = $component;
8113 } else {
8114 list($type, $plugin) = explode('_', $component, 2);
8115 $plugintypes = get_plugin_types(false);
8116 if ($type !== 'core' and !array_key_exists($type, $plugintypes)) {
8117 $type = 'mod';
8118 $plugin = $component;
8122 return array($type, $plugin);
8126 * List all core subsystems and their location
8128 * This is a whitelist of components that are part of the core and their
8129 * language strings are defined in /lang/en/<<subsystem>>.php. If a given
8130 * plugin is not listed here and it does not have proper plugintype prefix,
8131 * then it is considered as course activity module.
8133 * The location is dirroot relative path. NULL means there is no special
8134 * directory for this subsystem. If the location is set, the subsystem's
8135 * renderer.php is expected to be there.
8137 * @return array of (string)name => (string|null)location
8139 function get_core_subsystems() {
8140 global $CFG;
8142 static $info = null;
8144 if (!$info) {
8145 $info = array(
8146 'access' => NULL,
8147 'admin' => $CFG->admin,
8148 'auth' => 'auth',
8149 'backup' => 'backup/util/ui',
8150 'badges' => 'badges',
8151 'block' => 'blocks',
8152 'blog' => 'blog',
8153 'bulkusers' => NULL,
8154 'cache' => 'cache',
8155 'calendar' => 'calendar',
8156 'cohort' => 'cohort',
8157 'condition' => NULL,
8158 'completion' => NULL,
8159 'countries' => NULL,
8160 'course' => 'course',
8161 'currencies' => NULL,
8162 'dbtransfer' => NULL,
8163 'debug' => NULL,
8164 'dock' => NULL,
8165 'editor' => 'lib/editor',
8166 'edufields' => NULL,
8167 'enrol' => 'enrol',
8168 'error' => NULL,
8169 'filepicker' => NULL,
8170 'files' => 'files',
8171 'filters' => NULL,
8172 'fonts' => NULL,
8173 'form' => 'lib/form',
8174 'grades' => 'grade',
8175 'grading' => 'grade/grading',
8176 'group' => 'group',
8177 'help' => NULL,
8178 'hub' => NULL,
8179 'imscc' => NULL,
8180 'install' => NULL,
8181 'iso6392' => NULL,
8182 'langconfig' => NULL,
8183 'license' => NULL,
8184 'mathslib' => NULL,
8185 'media' => 'media',
8186 'message' => 'message',
8187 'mimetypes' => NULL,
8188 'mnet' => 'mnet',
8189 'moodle.org' => NULL, // the dot is nasty, watch out! should be renamed to moodleorg
8190 'my' => 'my',
8191 'notes' => 'notes',
8192 'pagetype' => NULL,
8193 'pix' => NULL,
8194 'plagiarism' => 'plagiarism',
8195 'plugin' => NULL,
8196 'portfolio' => 'portfolio',
8197 'publish' => 'course/publish',
8198 'question' => 'question',
8199 'rating' => 'rating',
8200 'register' => 'admin/registration', //TODO: this is wrong, unfortunately we would need to modify hub code to pass around the correct url
8201 'repository' => 'repository',
8202 'rss' => 'rss',
8203 'role' => $CFG->admin.'/role',
8204 'search' => 'search',
8205 'table' => NULL,
8206 'tag' => 'tag',
8207 'timezones' => NULL,
8208 'user' => 'user',
8209 'userkey' => NULL,
8210 'webservice' => 'webservice',
8214 return $info;
8218 * Lists all plugin types
8219 * @param bool $fullpaths false means relative paths from dirroot
8220 * @return array Array of strings - name=>location
8222 function get_plugin_types($fullpaths=true) {
8223 global $CFG;
8225 $cache = cache::make('core', 'plugintypes');
8227 if ($fullpaths) {
8228 // First confirm that dirroot and the stored dirroot match.
8229 if ($CFG->dirroot === $cache->get('dirroot')) {
8230 // They match we can use it.
8231 $cached = $cache->get(1);
8232 } else {
8233 // Oops they didn't match. The moodle directory has been moved on us.
8234 $cached = false;
8236 } else {
8237 $cached = $cache->get(0);
8240 if ($cached !== false) {
8241 return $cached;
8243 } else {
8244 $info = array('qtype' => 'question/type',
8245 'mod' => 'mod',
8246 'auth' => 'auth',
8247 'enrol' => 'enrol',
8248 'message' => 'message/output',
8249 'block' => 'blocks',
8250 'filter' => 'filter',
8251 'editor' => 'lib/editor',
8252 'format' => 'course/format',
8253 'profilefield' => 'user/profile/field',
8254 'report' => 'report',
8255 'coursereport' => 'course/report', // must be after system reports
8256 'gradeexport' => 'grade/export',
8257 'gradeimport' => 'grade/import',
8258 'gradereport' => 'grade/report',
8259 'gradingform' => 'grade/grading/form',
8260 'mnetservice' => 'mnet/service',
8261 'webservice' => 'webservice',
8262 'repository' => 'repository',
8263 'portfolio' => 'portfolio',
8264 'qbehaviour' => 'question/behaviour',
8265 'qformat' => 'question/format',
8266 'plagiarism' => 'plagiarism',
8267 'tool' => $CFG->admin.'/tool',
8268 'cachestore' => 'cache/stores',
8269 'cachelock' => 'cache/locks',
8270 'theme' => 'theme', // this is a bit hacky, themes may be in $CFG->themedir too
8273 $subpluginowners = array_merge(array_values(get_plugin_list('mod')),
8274 array_values(get_plugin_list('editor')));
8275 foreach ($subpluginowners as $ownerdir) {
8276 if (file_exists("$ownerdir/db/subplugins.php")) {
8277 $subplugins = array();
8278 include("$ownerdir/db/subplugins.php");
8279 foreach ($subplugins as $subtype=>$dir) {
8280 $info[$subtype] = $dir;
8285 // local is always last!
8286 $info['local'] = 'local';
8288 $fullinfo = array();
8289 foreach ($info as $type => $dir) {
8290 $fullinfo[$type] = $CFG->dirroot.'/'.$dir;
8293 $cache->set(0, $info);
8294 $cache->set(1, $fullinfo);
8295 // We cache the dirroot as well so that we can compare it when we
8296 // retrieve full info from the cache.
8297 $cache->set('dirroot', $CFG->dirroot);
8299 return ($fullpaths ? $fullinfo : $info);
8304 * This method validates a plug name. It is much faster than calling clean_param.
8305 * @param string $name a string that might be a plugin name.
8306 * @return bool if this string is a valid plugin name.
8308 function is_valid_plugin_name($name) {
8309 return (bool) preg_match('/^[a-z](?:[a-z0-9_](?!__))*[a-z0-9]$/', $name);
8313 * Simplified version of get_list_of_plugins()
8314 * @param string $plugintype type of plugin
8315 * @return array name=>fulllocation pairs of plugins of given type
8317 function get_plugin_list($plugintype) {
8318 global $CFG;
8320 // We use the dirroot as an identifier here because if it has changed the whole cache
8321 // can be considered invalid.
8322 $cache = cache::make('core', 'pluginlist', array('dirroot' => $CFG->dirroot));
8323 $cached = $cache->get($plugintype);
8324 if ($cached !== false) {
8325 return $cached;
8328 $ignored = array('CVS', '_vti_cnf', 'simpletest', 'db', 'yui', 'tests');
8329 if ($plugintype == 'auth') {
8330 // Historically we have had an auth plugin called 'db', so allow a special case.
8331 $key = array_search('db', $ignored);
8332 if ($key !== false) {
8333 unset($ignored[$key]);
8337 if ($plugintype === '') {
8338 $plugintype = 'mod';
8341 $fulldirs = array();
8343 if ($plugintype === 'mod') {
8344 // mod is an exception because we have to call this function from get_plugin_types()
8345 $fulldirs[] = $CFG->dirroot.'/mod';
8347 } else if ($plugintype === 'editor') {
8348 // Exception also needed for editor for same reason.
8349 $fulldirs[] = $CFG->dirroot . '/lib/editor';
8351 } else if ($plugintype === 'theme') {
8352 $fulldirs[] = $CFG->dirroot.'/theme';
8353 // themes are special because they may be stored also in separate directory
8354 if (!empty($CFG->themedir) and file_exists($CFG->themedir) and is_dir($CFG->themedir) ) {
8355 $fulldirs[] = $CFG->themedir;
8358 } else {
8359 $types = get_plugin_types(true);
8360 if (!array_key_exists($plugintype, $types)) {
8361 $cache->set($plugintype, array());
8362 return array();
8364 $fulldir = $types[$plugintype];
8365 if (!file_exists($fulldir)) {
8366 $cache->set($plugintype, array());
8367 return array();
8369 $fulldirs[] = $fulldir;
8371 $result = array();
8373 foreach ($fulldirs as $fulldir) {
8374 if (!is_dir($fulldir)) {
8375 continue;
8377 $items = new DirectoryIterator($fulldir);
8378 foreach ($items as $item) {
8379 if ($item->isDot() or !$item->isDir()) {
8380 continue;
8382 $pluginname = $item->getFilename();
8383 if (in_array($pluginname, $ignored)) {
8384 continue;
8386 if (!is_valid_plugin_name($pluginname)) {
8387 // Better ignore plugins with problematic names here.
8388 continue;
8390 $result[$pluginname] = $fulldir.'/'.$pluginname;
8391 unset($item);
8393 unset($items);
8396 //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!
8397 ksort($result);
8398 $cache->set($plugintype, $result);
8399 return $result;
8403 * Get a list of all the plugins of a given type that contain a particular file.
8404 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
8405 * @param string $file the name of file that must be present in the plugin.
8406 * (e.g. 'view.php', 'db/install.xml').
8407 * @param bool $include if true (default false), the file will be include_once-ed if found.
8408 * @return array with plugin name as keys (e.g. 'forum', 'courselist') and the path
8409 * to the file relative to dirroot as value (e.g. "$CFG->dirroot/mod/forum/view.php").
8411 function get_plugin_list_with_file($plugintype, $file, $include = false) {
8412 global $CFG; // Necessary in case it is referenced by include()d PHP scripts.
8414 $plugins = array();
8416 foreach(get_plugin_list($plugintype) as $plugin => $dir) {
8417 $path = $dir . '/' . $file;
8418 if (file_exists($path)) {
8419 if ($include) {
8420 include_once($path);
8422 $plugins[$plugin] = $path;
8426 return $plugins;
8430 * Get a list of all the plugins of a given type that define a certain API function
8431 * in a certain file. The plugin component names and function names are returned.
8433 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
8434 * @param string $function the part of the name of the function after the
8435 * frankenstyle prefix. e.g 'hook' if you are looking for functions with
8436 * names like report_courselist_hook.
8437 * @param string $file the name of file within the plugin that defines the
8438 * function. Defaults to lib.php.
8439 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
8440 * and the function names as values (e.g. 'report_courselist_hook', 'forum_hook').
8442 function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php') {
8443 $pluginfunctions = array();
8444 foreach (get_plugin_list_with_file($plugintype, $file, true) as $plugin => $notused) {
8445 $fullfunction = $plugintype . '_' . $plugin . '_' . $function;
8447 if (function_exists($fullfunction)) {
8448 // Function exists with standard name. Store, indexed by
8449 // frankenstyle name of plugin
8450 $pluginfunctions[$plugintype . '_' . $plugin] = $fullfunction;
8452 } else if ($plugintype === 'mod') {
8453 // For modules, we also allow plugin without full frankenstyle
8454 // but just starting with the module name
8455 $shortfunction = $plugin . '_' . $function;
8456 if (function_exists($shortfunction)) {
8457 $pluginfunctions[$plugintype . '_' . $plugin] = $shortfunction;
8461 return $pluginfunctions;
8465 * Get a list of all the plugins of a given type that define a certain class
8466 * in a certain file. The plugin component names and class names are returned.
8468 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
8469 * @param string $class the part of the name of the class after the
8470 * frankenstyle prefix. e.g 'thing' if you are looking for classes with
8471 * names like report_courselist_thing. If you are looking for classes with
8472 * the same name as the plugin name (e.g. qtype_multichoice) then pass ''.
8473 * @param string $file the name of file within the plugin that defines the class.
8474 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
8475 * and the class names as values (e.g. 'report_courselist_thing', 'qtype_multichoice').
8477 function get_plugin_list_with_class($plugintype, $class, $file) {
8478 if ($class) {
8479 $suffix = '_' . $class;
8480 } else {
8481 $suffix = '';
8484 $pluginclasses = array();
8485 foreach (get_plugin_list_with_file($plugintype, $file, true) as $plugin => $notused) {
8486 $classname = $plugintype . '_' . $plugin . $suffix;
8487 if (class_exists($classname)) {
8488 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
8492 return $pluginclasses;
8496 * Lists plugin-like directories within specified directory
8498 * This function was originally used for standard Moodle plugins, please use
8499 * new get_plugin_list() now.
8501 * This function is used for general directory listing and backwards compatility.
8503 * @param string $directory relative directory from root
8504 * @param string $exclude dir name to exclude from the list (defaults to none)
8505 * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot)
8506 * @return array Sorted array of directory names found under the requested parameters
8508 function get_list_of_plugins($directory='mod', $exclude='', $basedir='') {
8509 global $CFG;
8511 $plugins = array();
8513 if (empty($basedir)) {
8514 $basedir = $CFG->dirroot .'/'. $directory;
8516 } else {
8517 $basedir = $basedir .'/'. $directory;
8520 if (file_exists($basedir) && filetype($basedir) == 'dir') {
8521 if (!$dirhandle = opendir($basedir)) {
8522 debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER);
8523 return array();
8525 while (false !== ($dir = readdir($dirhandle))) {
8526 $firstchar = substr($dir, 0, 1);
8527 if ($firstchar === '.' or $dir === 'CVS' or $dir === '_vti_cnf' or $dir === 'simpletest' or $dir === 'yui' or $dir === 'phpunit' or $dir === $exclude) {
8528 continue;
8530 if (filetype($basedir .'/'. $dir) != 'dir') {
8531 continue;
8533 $plugins[] = $dir;
8535 closedir($dirhandle);
8537 if ($plugins) {
8538 asort($plugins);
8540 return $plugins;
8544 * Invoke plugin's callback functions
8546 * @param string $type plugin type e.g. 'mod'
8547 * @param string $name plugin name
8548 * @param string $feature feature name
8549 * @param string $action feature's action
8550 * @param array $params parameters of callback function, should be an array
8551 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
8552 * @return mixed
8554 * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743
8556 function plugin_callback($type, $name, $feature, $action, $params = null, $default = null) {
8557 return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default);
8561 * Invoke component's callback functions
8563 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
8564 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
8565 * @param array $params parameters of callback function
8566 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
8567 * @return mixed
8569 function component_callback($component, $function, array $params = array(), $default = null) {
8570 global $CFG; // this is needed for require_once() below
8572 $cleancomponent = clean_param($component, PARAM_COMPONENT);
8573 if (empty($cleancomponent)) {
8574 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
8576 $component = $cleancomponent;
8578 list($type, $name) = normalize_component($component);
8579 $component = $type . '_' . $name;
8581 $oldfunction = $name.'_'.$function;
8582 $function = $component.'_'.$function;
8584 $dir = get_component_directory($component);
8585 if (empty($dir)) {
8586 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
8589 // Load library and look for function
8590 if (file_exists($dir.'/lib.php')) {
8591 require_once($dir.'/lib.php');
8594 if (!function_exists($function) and function_exists($oldfunction)) {
8595 if ($type !== 'mod' and $type !== 'core') {
8596 debugging("Please use new function name $function instead of legacy $oldfunction");
8598 $function = $oldfunction;
8601 if (function_exists($function)) {
8602 // Function exists, so just return function result
8603 $ret = call_user_func_array($function, $params);
8604 if (is_null($ret)) {
8605 return $default;
8606 } else {
8607 return $ret;
8610 return $default;
8614 * Checks whether a plugin supports a specified feature.
8616 * @param string $type Plugin type e.g. 'mod'
8617 * @param string $name Plugin name e.g. 'forum'
8618 * @param string $feature Feature code (FEATURE_xx constant)
8619 * @param mixed $default default value if feature support unknown
8620 * @return mixed Feature result (false if not supported, null if feature is unknown,
8621 * otherwise usually true but may have other feature-specific value such as array)
8623 function plugin_supports($type, $name, $feature, $default = NULL) {
8624 global $CFG;
8626 if ($type === 'mod' and $name === 'NEWMODULE') {
8627 //somebody forgot to rename the module template
8628 return false;
8631 $component = clean_param($type . '_' . $name, PARAM_COMPONENT);
8632 if (empty($component)) {
8633 throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name);
8636 $function = null;
8638 if ($type === 'mod') {
8639 // we need this special case because we support subplugins in modules,
8640 // otherwise it would end up in infinite loop
8641 if (file_exists("$CFG->dirroot/mod/$name/lib.php")) {
8642 include_once("$CFG->dirroot/mod/$name/lib.php");
8643 $function = $component.'_supports';
8644 if (!function_exists($function)) {
8645 // legacy non-frankenstyle function name
8646 $function = $name.'_supports';
8648 } else {
8649 // invalid module
8652 } else {
8653 if (!$path = get_plugin_directory($type, $name)) {
8654 // non existent plugin type
8655 return false;
8657 if (file_exists("$path/lib.php")) {
8658 include_once("$path/lib.php");
8659 $function = $component.'_supports';
8663 if ($function and function_exists($function)) {
8664 $supports = $function($feature);
8665 if (is_null($supports)) {
8666 // plugin does not know - use default
8667 return $default;
8668 } else {
8669 return $supports;
8673 //plugin does not care, so use default
8674 return $default;
8678 * Returns true if the current version of PHP is greater that the specified one.
8680 * @todo Check PHP version being required here is it too low?
8682 * @param string $version The version of php being tested.
8683 * @return bool
8685 function check_php_version($version='5.2.4') {
8686 return (version_compare(phpversion(), $version) >= 0);
8690 * Checks to see if is the browser operating system matches the specified
8691 * brand.
8693 * Known brand: 'Windows','Linux','Macintosh','SGI','SunOS','HP-UX'
8695 * @uses $_SERVER
8696 * @param string $brand The operating system identifier being tested
8697 * @return bool true if the given brand below to the detected operating system
8699 function check_browser_operating_system($brand) {
8700 if (empty($_SERVER['HTTP_USER_AGENT'])) {
8701 return false;
8704 if (preg_match("/$brand/i", $_SERVER['HTTP_USER_AGENT'])) {
8705 return true;
8708 return false;
8712 * Checks to see if is a browser matches the specified
8713 * brand and is equal or better version.
8715 * @uses $_SERVER
8716 * @param string $brand The browser identifier being tested
8717 * @param int $version The version of the browser, if not specified any version (except 5.5 for IE for BC reasons)
8718 * @return bool true if the given version is below that of the detected browser
8720 function check_browser_version($brand, $version = null) {
8721 if (empty($_SERVER['HTTP_USER_AGENT'])) {
8722 return false;
8725 $agent = $_SERVER['HTTP_USER_AGENT'];
8727 switch ($brand) {
8729 case 'Camino': /// OSX browser using Gecke engine
8730 if (strpos($agent, 'Camino') === false) {
8731 return false;
8733 if (empty($version)) {
8734 return true; // no version specified
8736 if (preg_match("/Camino\/([0-9\.]+)/i", $agent, $match)) {
8737 if (version_compare($match[1], $version) >= 0) {
8738 return true;
8741 break;
8744 case 'Firefox': /// Mozilla Firefox browsers
8745 if (strpos($agent, 'Iceweasel') === false and strpos($agent, 'Firefox') === false) {
8746 return false;
8748 if (empty($version)) {
8749 return true; // no version specified
8751 if (preg_match("/(Iceweasel|Firefox)\/([0-9\.]+)/i", $agent, $match)) {
8752 if (version_compare($match[2], $version) >= 0) {
8753 return true;
8756 break;
8759 case 'Gecko': /// Gecko based browsers
8760 // Do not look for dates any more, we expect real Firefox version here.
8761 if (empty($version)) {
8762 $version = 1;
8763 } else if ($version > 20000000) {
8764 // This is just a guess, it is not supposed to be 100% accurate!
8765 if (preg_match('/^201/', $version)) {
8766 $version = 3.6;
8767 } else if (preg_match('/^200[7-9]/', $version)) {
8768 $version = 3;
8769 } else if (preg_match('/^2006/', $version)) {
8770 $version = 2;
8771 } else {
8772 $version = 1.5;
8775 if (preg_match("/(Iceweasel|Firefox)\/([0-9\.]+)/i", $agent, $match)) {
8776 // Use real Firefox version if specified in user agent string.
8777 if (version_compare($match[2], $version) >= 0) {
8778 return true;
8780 } else if (preg_match("/Gecko\/([0-9\.]+)/i", $agent, $match)) {
8781 // Gecko might contain date or Firefox revision, let's just guess the Firefox version from the date.
8782 $browserver = $match[1];
8783 if ($browserver > 20000000) {
8784 // This is just a guess, it is not supposed to be 100% accurate!
8785 if (preg_match('/^201/', $browserver)) {
8786 $browserver = 3.6;
8787 } else if (preg_match('/^200[7-9]/', $browserver)) {
8788 $browserver = 3;
8789 } else if (preg_match('/^2006/', $version)) {
8790 $browserver = 2;
8791 } else {
8792 $browserver = 1.5;
8795 if (version_compare($browserver, $version) >= 0) {
8796 return true;
8799 break;
8802 case 'MSIE': /// Internet Explorer
8803 if (strpos($agent, 'Opera') !== false) { // Reject Opera
8804 return false;
8806 // In case of IE we have to deal with BC of the version parameter.
8807 if (is_null($version)) {
8808 $version = 5.5; // Anything older is not considered a browser at all!
8810 // IE uses simple versions, let's cast it to float to simplify the logic here.
8811 $version = round($version, 1);
8812 // See: http://www.useragentstring.com/pages/Internet%20Explorer/
8813 if (preg_match("/MSIE ([0-9\.]+)/", $agent, $match)) {
8814 $browser = $match[1];
8815 } else {
8816 return false;
8818 // IE8 and later versions may pretend to be IE7 for intranet sites, use Trident version instead,
8819 // the Trident should always describe the capabilities of IE in any emulation mode.
8820 if ($browser === '7.0' and preg_match("/Trident\/([0-9\.]+)/", $agent, $match)) {
8821 $browser = $match[1] + 4; // NOTE: Hopefully this will work also for future IE versions.
8823 $browser = round($browser, 1);
8824 return ($browser >= $version);
8825 break;
8828 case 'Opera': /// Opera
8829 if (strpos($agent, 'Opera') === false) {
8830 return false;
8832 if (empty($version)) {
8833 return true; // no version specified
8835 // Recent Opera useragents have Version/ with the actual version, e.g.:
8836 // Opera/9.80 (Windows NT 6.1; WOW64; U; en) Presto/2.10.289 Version/12.01
8837 // That's Opera 12.01, not 9.8.
8838 if (preg_match("/Version\/([0-9\.]+)/i", $agent, $match)) {
8839 if (version_compare($match[1], $version) >= 0) {
8840 return true;
8842 } else if (preg_match("/Opera\/([0-9\.]+)/i", $agent, $match)) {
8843 if (version_compare($match[1], $version) >= 0) {
8844 return true;
8847 break;
8850 case 'WebKit': /// WebKit based browser - everything derived from it (Safari, Chrome, iOS, Android and other mobiles)
8851 if (strpos($agent, 'AppleWebKit') === false) {
8852 return false;
8854 if (empty($version)) {
8855 return true; // no version specified
8857 if (preg_match("/AppleWebKit\/([0-9.]+)/i", $agent, $match)) {
8858 if (version_compare($match[1], $version) >= 0) {
8859 return true;
8862 break;
8865 case 'Safari': /// Desktop version of Apple Safari browser - no mobile or touch devices
8866 if (strpos($agent, 'AppleWebKit') === false) {
8867 return false;
8869 // Look for AppleWebKit, excluding strings with OmniWeb, Shiira and SymbianOS and any other mobile devices
8870 if (strpos($agent, 'OmniWeb')) { // Reject OmniWeb
8871 return false;
8873 if (strpos($agent, 'Shiira')) { // Reject Shiira
8874 return false;
8876 if (strpos($agent, 'SymbianOS')) { // Reject SymbianOS
8877 return false;
8879 if (strpos($agent, 'Android')) { // Reject Androids too
8880 return false;
8882 if (strpos($agent, 'iPhone') or strpos($agent, 'iPad') or strpos($agent, 'iPod')) {
8883 // No Apple mobile devices here - editor does not work, course ajax is not touch compatible, etc.
8884 return false;
8886 if (strpos($agent, 'Chrome')) { // Reject chrome browsers - it needs to be tested explicitly
8887 return false;
8890 if (empty($version)) {
8891 return true; // no version specified
8893 if (preg_match("/AppleWebKit\/([0-9.]+)/i", $agent, $match)) {
8894 if (version_compare($match[1], $version) >= 0) {
8895 return true;
8898 break;
8901 case 'Chrome':
8902 if (strpos($agent, 'Chrome') === false) {
8903 return false;
8905 if (empty($version)) {
8906 return true; // no version specified
8908 if (preg_match("/Chrome\/(.*)[ ]+/i", $agent, $match)) {
8909 if (version_compare($match[1], $version) >= 0) {
8910 return true;
8913 break;
8916 case 'Safari iOS': /// Safari on iPhone, iPad and iPod touch
8917 if (strpos($agent, 'AppleWebKit') === false or strpos($agent, 'Safari') === false) {
8918 return false;
8920 if (!strpos($agent, 'iPhone') and !strpos($agent, 'iPad') and !strpos($agent, 'iPod')) {
8921 return false;
8923 if (empty($version)) {
8924 return true; // no version specified
8926 if (preg_match("/AppleWebKit\/([0-9]+)/i", $agent, $match)) {
8927 if (version_compare($match[1], $version) >= 0) {
8928 return true;
8931 break;
8934 case 'WebKit Android': /// WebKit browser on Android
8935 if (strpos($agent, 'Linux; U; Android') === false) {
8936 return false;
8938 if (empty($version)) {
8939 return true; // no version specified
8941 if (preg_match("/AppleWebKit\/([0-9]+)/i", $agent, $match)) {
8942 if (version_compare($match[1], $version) >= 0) {
8943 return true;
8946 break;
8950 return false;
8954 * Returns whether a device/browser combination is mobile, tablet, legacy, default or the result of
8955 * an optional admin specified regular expression. If enabledevicedetection is set to no or not set
8956 * it returns default
8958 * @return string device type
8960 function get_device_type() {
8961 global $CFG;
8963 if (empty($CFG->enabledevicedetection) || empty($_SERVER['HTTP_USER_AGENT'])) {
8964 return 'default';
8967 $useragent = $_SERVER['HTTP_USER_AGENT'];
8969 if (!empty($CFG->devicedetectregex)) {
8970 $regexes = json_decode($CFG->devicedetectregex);
8972 foreach ($regexes as $value=>$regex) {
8973 if (preg_match($regex, $useragent)) {
8974 return $value;
8979 //mobile detection PHP direct copy from open source detectmobilebrowser.com
8980 $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';
8981 $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';
8982 if (preg_match($phonesregex,$useragent) || preg_match($modelsregex,substr($useragent, 0, 4))){
8983 return 'mobile';
8986 $tabletregex = '/Tablet browser|android|iPad|iProd|GT-P1000|GT-I9000|SHW-M180S|SGH-T849|SCH-I800|Build\/ERE27|sholest/i';
8987 if (preg_match($tabletregex, $useragent)) {
8988 return 'tablet';
8991 // Safe way to check for IE6 and not get false positives for some IE 7/8 users
8992 if (substr($_SERVER['HTTP_USER_AGENT'], 0, 34) === 'Mozilla/4.0 (compatible; MSIE 6.0;') {
8993 return 'legacy';
8996 return 'default';
9000 * Returns a list of the device types supporting by Moodle
9002 * @param boolean $incusertypes includes types specified using the devicedetectregex admin setting
9003 * @return array $types
9005 function get_device_type_list($incusertypes = true) {
9006 global $CFG;
9008 $types = array('default', 'legacy', 'mobile', 'tablet');
9010 if ($incusertypes && !empty($CFG->devicedetectregex)) {
9011 $regexes = json_decode($CFG->devicedetectregex);
9013 foreach ($regexes as $value => $regex) {
9014 $types[] = $value;
9018 return $types;
9022 * Returns the theme selected for a particular device or false if none selected.
9024 * @param string $devicetype
9025 * @return string|false The name of the theme to use for the device or the false if not set
9027 function get_selected_theme_for_device_type($devicetype = null) {
9028 global $CFG;
9030 if (empty($devicetype)) {
9031 $devicetype = get_user_device_type();
9034 $themevarname = get_device_cfg_var_name($devicetype);
9035 if (empty($CFG->$themevarname)) {
9036 return false;
9039 return $CFG->$themevarname;
9043 * Returns the name of the device type theme var in $CFG (because there is not a standard convention to allow backwards compatability
9045 * @param string $devicetype
9046 * @return string The config variable to use to determine the theme
9048 function get_device_cfg_var_name($devicetype = null) {
9049 if ($devicetype == 'default' || empty($devicetype)) {
9050 return 'theme';
9053 return 'theme' . $devicetype;
9057 * Allows the user to switch the device they are seeing the theme for.
9058 * This allows mobile users to switch back to the default theme, or theme for any other device.
9060 * @param string $newdevice The device the user is currently using.
9061 * @return string The device the user has switched to
9063 function set_user_device_type($newdevice) {
9064 global $USER;
9066 $devicetype = get_device_type();
9067 $devicetypes = get_device_type_list();
9069 if ($newdevice == $devicetype) {
9070 unset_user_preference('switchdevice'.$devicetype);
9071 } else if (in_array($newdevice, $devicetypes)) {
9072 set_user_preference('switchdevice'.$devicetype, $newdevice);
9077 * Returns the device the user is currently using, or if the user has chosen to switch devices
9078 * for the current device type the type they have switched to.
9080 * @return string The device the user is currently using or wishes to use
9082 function get_user_device_type() {
9083 $device = get_device_type();
9084 $switched = get_user_preferences('switchdevice'.$device, false);
9085 if ($switched != false) {
9086 return $switched;
9088 return $device;
9092 * Returns one or several CSS class names that match the user's browser. These can be put
9093 * in the body tag of the page to apply browser-specific rules without relying on CSS hacks
9095 * @return array An array of browser version classes
9097 function get_browser_version_classes() {
9098 $classes = array();
9100 if (check_browser_version("MSIE", "0")) {
9101 $classes[] = 'ie';
9102 for($i=12; $i>=6; $i--) {
9103 if (check_browser_version("MSIE", $i)) {
9104 $classes[] = 'ie'.$i;
9105 break;
9109 } else if (check_browser_version("Firefox") || check_browser_version("Gecko") || check_browser_version("Camino")) {
9110 $classes[] = 'gecko';
9111 if (preg_match('/rv\:([1-2])\.([0-9])/', $_SERVER['HTTP_USER_AGENT'], $matches)) {
9112 $classes[] = "gecko{$matches[1]}{$matches[2]}";
9115 } else if (check_browser_version("WebKit")) {
9116 $classes[] = 'safari';
9117 if (check_browser_version("Safari iOS")) {
9118 $classes[] = 'ios';
9120 } else if (check_browser_version("WebKit Android")) {
9121 $classes[] = 'android';
9124 } else if (check_browser_version("Opera")) {
9125 $classes[] = 'opera';
9129 return $classes;
9133 * Can handle rotated text. Whether it is safe to use the trickery in textrotate.js.
9135 * @return bool True for yes, false for no
9137 function can_use_rotated_text() {
9138 return check_browser_version('MSIE', 9) || check_browser_version('Firefox', 2) ||
9139 check_browser_version('Chrome', 21) || check_browser_version('Safari', 536.25) ||
9140 check_browser_version('Opera', 12) || check_browser_version('Safari iOS', 533);
9144 * Determine if moodle installation requires update
9146 * Checks version numbers of main code and all modules to see
9147 * if there are any mismatches
9149 * @global moodle_database $DB
9150 * @return bool
9152 function moodle_needs_upgrading() {
9153 global $CFG, $DB, $OUTPUT;
9155 if (empty($CFG->version)) {
9156 return true;
9159 // We have to purge plugin related caches now to be sure we have fresh data
9160 // and new plugins can be detected.
9161 cache::make('core', 'plugintypes')->purge();
9162 cache::make('core', 'pluginlist')->purge();
9163 cache::make('core', 'plugininfo_base')->purge();
9164 cache::make('core', 'plugininfo_mod')->purge();
9165 cache::make('core', 'plugininfo_block')->purge();
9166 cache::make('core', 'plugininfo_filter')->purge();
9167 cache::make('core', 'plugininfo_repository')->purge();
9168 cache::make('core', 'plugininfo_portfolio')->purge();
9170 // Check the main version first.
9171 $version = null;
9172 include($CFG->dirroot.'/version.php'); // defines $version and upgrades
9173 if ($version > $CFG->version) {
9174 return true;
9177 // modules
9178 $mods = get_plugin_list('mod');
9179 $installed = $DB->get_records('modules', array(), '', 'name, version');
9180 foreach ($mods as $mod => $fullmod) {
9181 if ($mod === 'NEWMODULE') { // Someone has unzipped the template, ignore it
9182 continue;
9184 $module = new stdClass();
9185 $plugin = new stdClass();
9186 if (!is_readable($fullmod.'/version.php')) {
9187 continue;
9189 include($fullmod.'/version.php'); // defines $module with version etc
9190 if (!isset($module->version) and isset($plugin->version)) {
9191 $module = $plugin;
9193 if (empty($installed[$mod])) {
9194 return true;
9195 } else if ($module->version > $installed[$mod]->version) {
9196 return true;
9199 unset($installed);
9201 // blocks
9202 $blocks = get_plugin_list('block');
9203 $installed = $DB->get_records('block', array(), '', 'name, version');
9204 require_once($CFG->dirroot.'/blocks/moodleblock.class.php');
9205 foreach ($blocks as $blockname=>$fullblock) {
9206 if ($blockname === 'NEWBLOCK') { // Someone has unzipped the template, ignore it
9207 continue;
9209 if (!is_readable($fullblock.'/version.php')) {
9210 continue;
9212 $plugin = new stdClass();
9213 $plugin->version = NULL;
9214 include($fullblock.'/version.php');
9215 if (empty($installed[$blockname])) {
9216 return true;
9217 } else if ($plugin->version > $installed[$blockname]->version) {
9218 return true;
9221 unset($installed);
9223 // now the rest of plugins
9224 $plugintypes = get_plugin_types();
9225 unset($plugintypes['mod']);
9226 unset($plugintypes['block']);
9228 $versions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin, value');
9229 foreach ($plugintypes as $type=>$unused) {
9230 $plugs = get_plugin_list($type);
9231 foreach ($plugs as $plug=>$fullplug) {
9232 $component = $type.'_'.$plug;
9233 if (!is_readable($fullplug.'/version.php')) {
9234 continue;
9236 $plugin = new stdClass();
9237 include($fullplug.'/version.php'); // defines $plugin with version etc
9238 if (array_key_exists($component, $versions)) {
9239 $installedversion = $versions[$component];
9240 } else {
9241 $installedversion = get_config($component, 'version');
9243 if (empty($installedversion)) { // new installation
9244 return true;
9245 } else if ($installedversion < $plugin->version) { // upgrade
9246 return true;
9251 return false;
9255 * Returns the major version of this site
9257 * Moodle version numbers consist of three numbers separated by a dot, for
9258 * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so
9259 * called major version. This function extracts the major version from either
9260 * $CFG->release (default) or eventually from the $release variable defined in
9261 * the main version.php.
9263 * @param bool $fromdisk should the version if source code files be used
9264 * @return string|false the major version like '2.3', false if could not be determined
9266 function moodle_major_version($fromdisk = false) {
9267 global $CFG;
9269 if ($fromdisk) {
9270 $release = null;
9271 require($CFG->dirroot.'/version.php');
9272 if (empty($release)) {
9273 return false;
9276 } else {
9277 if (empty($CFG->release)) {
9278 return false;
9280 $release = $CFG->release;
9283 if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) {
9284 return $matches[0];
9285 } else {
9286 return false;
9290 /// MISCELLANEOUS ////////////////////////////////////////////////////////////////////
9293 * Sets the system locale
9295 * @category string
9296 * @param string $locale Can be used to force a locale
9298 function moodle_setlocale($locale='') {
9299 global $CFG;
9301 static $currentlocale = ''; // last locale caching
9303 $oldlocale = $currentlocale;
9305 /// Fetch the correct locale based on ostype
9306 if ($CFG->ostype == 'WINDOWS') {
9307 $stringtofetch = 'localewin';
9308 } else {
9309 $stringtofetch = 'locale';
9312 /// the priority is the same as in get_string() - parameter, config, course, session, user, global language
9313 if (!empty($locale)) {
9314 $currentlocale = $locale;
9315 } else if (!empty($CFG->locale)) { // override locale for all language packs
9316 $currentlocale = $CFG->locale;
9317 } else {
9318 $currentlocale = get_string($stringtofetch, 'langconfig');
9321 /// do nothing if locale already set up
9322 if ($oldlocale == $currentlocale) {
9323 return;
9326 /// Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values,
9327 /// set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7
9328 /// Some day, numeric, monetary and other categories should be set too, I think. :-/
9330 /// Get current values
9331 $monetary= setlocale (LC_MONETARY, 0);
9332 $numeric = setlocale (LC_NUMERIC, 0);
9333 $ctype = setlocale (LC_CTYPE, 0);
9334 if ($CFG->ostype != 'WINDOWS') {
9335 $messages= setlocale (LC_MESSAGES, 0);
9337 /// Set locale to all
9338 setlocale (LC_ALL, $currentlocale);
9339 /// Set old values
9340 setlocale (LC_MONETARY, $monetary);
9341 setlocale (LC_NUMERIC, $numeric);
9342 if ($CFG->ostype != 'WINDOWS') {
9343 setlocale (LC_MESSAGES, $messages);
9345 if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') { // To workaround a well-known PHP problem with Turkish letter Ii
9346 setlocale (LC_CTYPE, $ctype);
9351 * Count words in a string.
9353 * Words are defined as things between whitespace.
9355 * @category string
9356 * @param string $string The text to be searched for words.
9357 * @return int The count of words in the specified string
9359 function count_words($string) {
9360 $string = strip_tags($string);
9361 return count(preg_split("/\w\b/", $string)) - 1;
9364 /** Count letters in a string.
9366 * Letters are defined as chars not in tags and different from whitespace.
9368 * @category string
9369 * @param string $string The text to be searched for letters.
9370 * @return int The count of letters in the specified text.
9372 function count_letters($string) {
9373 /// Loading the textlib singleton instance. We are going to need it.
9374 $string = strip_tags($string); // Tags are out now
9375 $string = preg_replace('/[[:space:]]*/','',$string); //Whitespace are out now
9377 return textlib::strlen($string);
9381 * Generate and return a random string of the specified length.
9383 * @param int $length The length of the string to be created.
9384 * @return string
9386 function random_string ($length=15) {
9387 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
9388 $pool .= 'abcdefghijklmnopqrstuvwxyz';
9389 $pool .= '0123456789';
9390 $poollen = strlen($pool);
9391 mt_srand ((double) microtime() * 1000000);
9392 $string = '';
9393 for ($i = 0; $i < $length; $i++) {
9394 $string .= substr($pool, (mt_rand()%($poollen)), 1);
9396 return $string;
9400 * Generate a complex random string (useful for md5 salts)
9402 * This function is based on the above {@link random_string()} however it uses a
9403 * larger pool of characters and generates a string between 24 and 32 characters
9405 * @param int $length Optional if set generates a string to exactly this length
9406 * @return string
9408 function complex_random_string($length=null) {
9409 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
9410 $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
9411 $poollen = strlen($pool);
9412 mt_srand ((double) microtime() * 1000000);
9413 if ($length===null) {
9414 $length = floor(rand(24,32));
9416 $string = '';
9417 for ($i = 0; $i < $length; $i++) {
9418 $string .= $pool[(mt_rand()%$poollen)];
9420 return $string;
9424 * Given some text (which may contain HTML) and an ideal length,
9425 * this function truncates the text neatly on a word boundary if possible
9427 * @category string
9428 * @global stdClass $CFG
9429 * @param string $text text to be shortened
9430 * @param int $ideal ideal string length
9431 * @param boolean $exact if false, $text will not be cut mid-word
9432 * @param string $ending The string to append if the passed string is truncated
9433 * @return string $truncate shortened string
9435 function shorten_text($text, $ideal=30, $exact = false, $ending='...') {
9437 global $CFG;
9439 // If the plain text is shorter than the maximum length, return the whole text.
9440 if (textlib::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) {
9441 return $text;
9444 // Splits on HTML tags. Each open/close/empty tag will be the first thing
9445 // and only tag in its 'line'.
9446 preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
9448 $total_length = textlib::strlen($ending);
9449 $truncate = '';
9451 // This array stores information about open and close tags and their position
9452 // in the truncated string. Each item in the array is an object with fields
9453 // ->open (true if open), ->tag (tag name in lower case), and ->pos
9454 // (byte position in truncated text).
9455 $tagdetails = array();
9457 foreach ($lines as $line_matchings) {
9458 // If there is any html-tag in this line, handle it and add it (uncounted) to the output.
9459 if (!empty($line_matchings[1])) {
9460 // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>).
9461 if (preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $line_matchings[1])) {
9462 // Do nothing.
9464 } else if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $line_matchings[1], $tag_matchings)) {
9465 // Record closing tag.
9466 $tagdetails[] = (object) array(
9467 'open' => false,
9468 'tag' => textlib::strtolower($tag_matchings[1]),
9469 'pos' => textlib::strlen($truncate),
9472 } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $line_matchings[1], $tag_matchings)) {
9473 // Record opening tag.
9474 $tagdetails[] = (object) array(
9475 'open' => true,
9476 'tag' => textlib::strtolower($tag_matchings[1]),
9477 'pos' => textlib::strlen($truncate),
9480 // Add html-tag to $truncate'd text.
9481 $truncate .= $line_matchings[1];
9484 // Calculate the length of the plain text part of the line; handle entities as one character.
9485 $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]));
9486 if ($total_length + $content_length > $ideal) {
9487 // The number of characters which are left.
9488 $left = $ideal - $total_length;
9489 $entities_length = 0;
9490 // Search for html entities.
9491 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)) {
9492 // calculate the real length of all entities in the legal range
9493 foreach ($entities[0] as $entity) {
9494 if ($entity[1]+1-$entities_length <= $left) {
9495 $left--;
9496 $entities_length += textlib::strlen($entity[0]);
9497 } else {
9498 // no more characters left
9499 break;
9503 $breakpos = $left + $entities_length;
9505 // if the words shouldn't be cut in the middle...
9506 if (!$exact) {
9507 // ...search the last occurence of a space...
9508 for (; $breakpos > 0; $breakpos--) {
9509 if ($char = textlib::substr($line_matchings[2], $breakpos, 1)) {
9510 if ($char === '.' or $char === ' ') {
9511 $breakpos += 1;
9512 break;
9513 } else if (strlen($char) > 2) { // Chinese/Japanese/Korean text
9514 $breakpos += 1; // can be truncated at any UTF-8
9515 break; // character boundary.
9520 if ($breakpos == 0) {
9521 // This deals with the test_shorten_text_no_spaces case.
9522 $breakpos = $left + $entities_length;
9523 } else if ($breakpos > $left + $entities_length) {
9524 // This deals with the previous for loop breaking on the first char.
9525 $breakpos = $left + $entities_length;
9528 $truncate .= textlib::substr($line_matchings[2], 0, $breakpos);
9529 // maximum length is reached, so get off the loop
9530 break;
9531 } else {
9532 $truncate .= $line_matchings[2];
9533 $total_length += $content_length;
9536 // If the maximum length is reached, get off the loop.
9537 if($total_length >= $ideal) {
9538 break;
9542 // Add the defined ending to the text.
9543 $truncate .= $ending;
9545 // Now calculate the list of open html tags based on the truncate position.
9546 $open_tags = array();
9547 foreach ($tagdetails as $taginfo) {
9548 if ($taginfo->open) {
9549 // Add tag to the beginning of $open_tags list.
9550 array_unshift($open_tags, $taginfo->tag);
9551 } else {
9552 // Can have multiple exact same open tags, close the last one.
9553 $pos = array_search($taginfo->tag, array_reverse($open_tags, true));
9554 if ($pos !== false) {
9555 unset($open_tags[$pos]);
9560 // Close all unclosed html-tags.
9561 foreach ($open_tags as $tag) {
9562 $truncate .= '</' . $tag . '>';
9565 return $truncate;
9570 * Given dates in seconds, how many weeks is the date from startdate
9571 * The first week is 1, the second 2 etc ...
9573 * @todo Finish documenting this function
9575 * @uses WEEKSECS
9576 * @param int $startdate Timestamp for the start date
9577 * @param int $thedate Timestamp for the end date
9578 * @return string
9580 function getweek ($startdate, $thedate) {
9581 if ($thedate < $startdate) { // error
9582 return 0;
9585 return floor(($thedate - $startdate) / WEEKSECS) + 1;
9589 * returns a randomly generated password of length $maxlen. inspired by
9591 * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and
9592 * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254}
9594 * @global stdClass $CFG
9595 * @param int $maxlen The maximum size of the password being generated.
9596 * @return string
9598 function generate_password($maxlen=10) {
9599 global $CFG;
9601 if (empty($CFG->passwordpolicy)) {
9602 $fillers = PASSWORD_DIGITS;
9603 $wordlist = file($CFG->wordlist);
9604 $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]);
9605 $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]);
9606 $filler1 = $fillers[rand(0, strlen($fillers) - 1)];
9607 $password = $word1 . $filler1 . $word2;
9608 } else {
9609 $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0;
9610 $digits = $CFG->minpassworddigits;
9611 $lower = $CFG->minpasswordlower;
9612 $upper = $CFG->minpasswordupper;
9613 $nonalphanum = $CFG->minpasswordnonalphanum;
9614 $total = $lower + $upper + $digits + $nonalphanum;
9615 // minlength should be the greater one of the two ( $minlen and $total )
9616 $minlen = $minlen < $total ? $total : $minlen;
9617 // maxlen can never be smaller than minlen
9618 $maxlen = $minlen > $maxlen ? $minlen : $maxlen;
9619 $additional = $maxlen - $total;
9621 // Make sure we have enough characters to fulfill
9622 // complexity requirements
9623 $passworddigits = PASSWORD_DIGITS;
9624 while ($digits > strlen($passworddigits)) {
9625 $passworddigits .= PASSWORD_DIGITS;
9627 $passwordlower = PASSWORD_LOWER;
9628 while ($lower > strlen($passwordlower)) {
9629 $passwordlower .= PASSWORD_LOWER;
9631 $passwordupper = PASSWORD_UPPER;
9632 while ($upper > strlen($passwordupper)) {
9633 $passwordupper .= PASSWORD_UPPER;
9635 $passwordnonalphanum = PASSWORD_NONALPHANUM;
9636 while ($nonalphanum > strlen($passwordnonalphanum)) {
9637 $passwordnonalphanum .= PASSWORD_NONALPHANUM;
9640 // Now mix and shuffle it all
9641 $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) .
9642 substr(str_shuffle ($passwordupper), 0, $upper) .
9643 substr(str_shuffle ($passworddigits), 0, $digits) .
9644 substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) .
9645 substr(str_shuffle ($passwordlower .
9646 $passwordupper .
9647 $passworddigits .
9648 $passwordnonalphanum), 0 , $additional));
9651 return substr ($password, 0, $maxlen);
9655 * Given a float, prints it nicely.
9656 * Localized floats must not be used in calculations!
9658 * The stripzeros feature is intended for making numbers look nicer in small
9659 * areas where it is not necessary to indicate the degree of accuracy by showing
9660 * ending zeros. If you turn it on with $decimalpoints set to 3, for example,
9661 * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'.
9663 * @param float $float The float to print
9664 * @param int $decimalpoints The number of decimal places to print.
9665 * @param bool $localized use localized decimal separator
9666 * @param bool $stripzeros If true, removes final zeros after decimal point
9667 * @return string locale float
9669 function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=false) {
9670 if (is_null($float)) {
9671 return '';
9673 if ($localized) {
9674 $separator = get_string('decsep', 'langconfig');
9675 } else {
9676 $separator = '.';
9678 $result = number_format($float, $decimalpoints, $separator, '');
9679 if ($stripzeros) {
9680 // Remove zeros and final dot if not needed
9681 $result = preg_replace('~(' . preg_quote($separator) . ')?0+$~', '', $result);
9683 return $result;
9687 * Converts locale specific floating point/comma number back to standard PHP float value
9688 * Do NOT try to do any math operations before this conversion on any user submitted floats!
9690 * @param string $locale_float locale aware float representation
9691 * @param bool $strict If true, then check the input and return false if it is not a valid number.
9692 * @return mixed float|bool - false or the parsed float.
9694 function unformat_float($locale_float, $strict = false) {
9695 $locale_float = trim($locale_float);
9697 if ($locale_float == '') {
9698 return null;
9701 $locale_float = str_replace(' ', '', $locale_float); // no spaces - those might be used as thousand separators
9702 $locale_float = str_replace(get_string('decsep', 'langconfig'), '.', $locale_float);
9704 if ($strict && !is_numeric($locale_float)) {
9705 return false;
9708 return (float)$locale_float;
9712 * Given a simple array, this shuffles it up just like shuffle()
9713 * Unlike PHP's shuffle() this function works on any machine.
9715 * @param array $array The array to be rearranged
9716 * @return array
9718 function swapshuffle($array) {
9720 srand ((double) microtime() * 10000000);
9721 $last = count($array) - 1;
9722 for ($i=0;$i<=$last;$i++) {
9723 $from = rand(0,$last);
9724 $curr = $array[$i];
9725 $array[$i] = $array[$from];
9726 $array[$from] = $curr;
9728 return $array;
9732 * Like {@link swapshuffle()}, but works on associative arrays
9734 * @param array $array The associative array to be rearranged
9735 * @return array
9737 function swapshuffle_assoc($array) {
9739 $newarray = array();
9740 $newkeys = swapshuffle(array_keys($array));
9742 foreach ($newkeys as $newkey) {
9743 $newarray[$newkey] = $array[$newkey];
9745 return $newarray;
9749 * Given an arbitrary array, and a number of draws,
9750 * this function returns an array with that amount
9751 * of items. The indexes are retained.
9753 * @todo Finish documenting this function
9755 * @param array $array
9756 * @param int $draws
9757 * @return array
9759 function draw_rand_array($array, $draws) {
9760 srand ((double) microtime() * 10000000);
9762 $return = array();
9764 $last = count($array);
9766 if ($draws > $last) {
9767 $draws = $last;
9770 while ($draws > 0) {
9771 $last--;
9773 $keys = array_keys($array);
9774 $rand = rand(0, $last);
9776 $return[$keys[$rand]] = $array[$keys[$rand]];
9777 unset($array[$keys[$rand]]);
9779 $draws--;
9782 return $return;
9786 * Calculate the difference between two microtimes
9788 * @param string $a The first Microtime
9789 * @param string $b The second Microtime
9790 * @return string
9792 function microtime_diff($a, $b) {
9793 list($a_dec, $a_sec) = explode(' ', $a);
9794 list($b_dec, $b_sec) = explode(' ', $b);
9795 return $b_sec - $a_sec + $b_dec - $a_dec;
9799 * Given a list (eg a,b,c,d,e) this function returns
9800 * an array of 1->a, 2->b, 3->c etc
9802 * @param string $list The string to explode into array bits
9803 * @param string $separator The separator used within the list string
9804 * @return array The now assembled array
9806 function make_menu_from_list($list, $separator=',') {
9808 $array = array_reverse(explode($separator, $list), true);
9809 foreach ($array as $key => $item) {
9810 $outarray[$key+1] = trim($item);
9812 return $outarray;
9816 * Creates an array that represents all the current grades that
9817 * can be chosen using the given grading type.
9819 * Negative numbers
9820 * are scales, zero is no grade, and positive numbers are maximum
9821 * grades.
9823 * @todo Finish documenting this function or better deprecated this completely!
9825 * @param int $gradingtype
9826 * @return array
9828 function make_grades_menu($gradingtype) {
9829 global $DB;
9831 $grades = array();
9832 if ($gradingtype < 0) {
9833 if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) {
9834 return make_menu_from_list($scale->scale);
9836 } else if ($gradingtype > 0) {
9837 for ($i=$gradingtype; $i>=0; $i--) {
9838 $grades[$i] = $i .' / '. $gradingtype;
9840 return $grades;
9842 return $grades;
9846 * This function returns the number of activities
9847 * using scaleid in a courseid
9849 * @todo Finish documenting this function
9851 * @global object
9852 * @global object
9853 * @param int $courseid ?
9854 * @param int $scaleid ?
9855 * @return int
9857 function course_scale_used($courseid, $scaleid) {
9858 global $CFG, $DB;
9860 $return = 0;
9862 if (!empty($scaleid)) {
9863 if ($cms = get_course_mods($courseid)) {
9864 foreach ($cms as $cm) {
9865 //Check cm->name/lib.php exists
9866 if (file_exists($CFG->dirroot.'/mod/'.$cm->modname.'/lib.php')) {
9867 include_once($CFG->dirroot.'/mod/'.$cm->modname.'/lib.php');
9868 $function_name = $cm->modname.'_scale_used';
9869 if (function_exists($function_name)) {
9870 if ($function_name($cm->instance,$scaleid)) {
9871 $return++;
9878 // check if any course grade item makes use of the scale
9879 $return += $DB->count_records('grade_items', array('courseid'=>$courseid, 'scaleid'=>$scaleid));
9881 // check if any outcome in the course makes use of the scale
9882 $return += $DB->count_records_sql("SELECT COUNT('x')
9883 FROM {grade_outcomes_courses} goc,
9884 {grade_outcomes} go
9885 WHERE go.id = goc.outcomeid
9886 AND go.scaleid = ? AND goc.courseid = ?",
9887 array($scaleid, $courseid));
9889 return $return;
9893 * This function returns the number of activities
9894 * using scaleid in the entire site
9896 * @param int $scaleid
9897 * @param array $courses
9898 * @return int
9900 function site_scale_used($scaleid, &$courses) {
9901 $return = 0;
9903 if (!is_array($courses) || count($courses) == 0) {
9904 $courses = get_courses("all",false,"c.id,c.shortname");
9907 if (!empty($scaleid)) {
9908 if (is_array($courses) && count($courses) > 0) {
9909 foreach ($courses as $course) {
9910 $return += course_scale_used($course->id,$scaleid);
9914 return $return;
9918 * make_unique_id_code
9920 * @todo Finish documenting this function
9922 * @uses $_SERVER
9923 * @param string $extra Extra string to append to the end of the code
9924 * @return string
9926 function make_unique_id_code($extra='') {
9928 $hostname = 'unknownhost';
9929 if (!empty($_SERVER['HTTP_HOST'])) {
9930 $hostname = $_SERVER['HTTP_HOST'];
9931 } else if (!empty($_ENV['HTTP_HOST'])) {
9932 $hostname = $_ENV['HTTP_HOST'];
9933 } else if (!empty($_SERVER['SERVER_NAME'])) {
9934 $hostname = $_SERVER['SERVER_NAME'];
9935 } else if (!empty($_ENV['SERVER_NAME'])) {
9936 $hostname = $_ENV['SERVER_NAME'];
9939 $date = gmdate("ymdHis");
9941 $random = random_string(6);
9943 if ($extra) {
9944 return $hostname .'+'. $date .'+'. $random .'+'. $extra;
9945 } else {
9946 return $hostname .'+'. $date .'+'. $random;
9952 * Function to check the passed address is within the passed subnet
9954 * The parameter is a comma separated string of subnet definitions.
9955 * Subnet strings can be in one of three formats:
9956 * 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn (number of bits in net mask)
9957 * 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)
9958 * 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. (incomplete address, a bit non-technical ;-)
9959 * Code for type 1 modified from user posted comments by mediator at
9960 * {@link http://au.php.net/manual/en/function.ip2long.php}
9962 * @param string $addr The address you are checking
9963 * @param string $subnetstr The string of subnet addresses
9964 * @return bool
9966 function address_in_subnet($addr, $subnetstr) {
9968 if ($addr == '0.0.0.0') {
9969 return false;
9971 $subnets = explode(',', $subnetstr);
9972 $found = false;
9973 $addr = trim($addr);
9974 $addr = cleanremoteaddr($addr, false); // normalise
9975 if ($addr === null) {
9976 return false;
9978 $addrparts = explode(':', $addr);
9980 $ipv6 = strpos($addr, ':');
9982 foreach ($subnets as $subnet) {
9983 $subnet = trim($subnet);
9984 if ($subnet === '') {
9985 continue;
9988 if (strpos($subnet, '/') !== false) {
9989 ///1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn
9990 list($ip, $mask) = explode('/', $subnet);
9991 $mask = trim($mask);
9992 if (!is_number($mask)) {
9993 continue; // incorect mask number, eh?
9995 $ip = cleanremoteaddr($ip, false); // normalise
9996 if ($ip === null) {
9997 continue;
9999 if (strpos($ip, ':') !== false) {
10000 // IPv6
10001 if (!$ipv6) {
10002 continue;
10004 if ($mask > 128 or $mask < 0) {
10005 continue; // nonsense
10007 if ($mask == 0) {
10008 return true; // any address
10010 if ($mask == 128) {
10011 if ($ip === $addr) {
10012 return true;
10014 continue;
10016 $ipparts = explode(':', $ip);
10017 $modulo = $mask % 16;
10018 $ipnet = array_slice($ipparts, 0, ($mask-$modulo)/16);
10019 $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16);
10020 if (implode(':', $ipnet) === implode(':', $addrnet)) {
10021 if ($modulo == 0) {
10022 return true;
10024 $pos = ($mask-$modulo)/16;
10025 $ipnet = hexdec($ipparts[$pos]);
10026 $addrnet = hexdec($addrparts[$pos]);
10027 $mask = 0xffff << (16 - $modulo);
10028 if (($addrnet & $mask) == ($ipnet & $mask)) {
10029 return true;
10033 } else {
10034 // IPv4
10035 if ($ipv6) {
10036 continue;
10038 if ($mask > 32 or $mask < 0) {
10039 continue; // nonsense
10041 if ($mask == 0) {
10042 return true;
10044 if ($mask == 32) {
10045 if ($ip === $addr) {
10046 return true;
10048 continue;
10050 $mask = 0xffffffff << (32 - $mask);
10051 if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
10052 return true;
10056 } else if (strpos($subnet, '-') !== false) {
10057 /// 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.
10058 $parts = explode('-', $subnet);
10059 if (count($parts) != 2) {
10060 continue;
10063 if (strpos($subnet, ':') !== false) {
10064 // IPv6
10065 if (!$ipv6) {
10066 continue;
10068 $ipstart = cleanremoteaddr(trim($parts[0]), false); // normalise
10069 if ($ipstart === null) {
10070 continue;
10072 $ipparts = explode(':', $ipstart);
10073 $start = hexdec(array_pop($ipparts));
10074 $ipparts[] = trim($parts[1]);
10075 $ipend = cleanremoteaddr(implode(':', $ipparts), false); // normalise
10076 if ($ipend === null) {
10077 continue;
10079 $ipparts[7] = '';
10080 $ipnet = implode(':', $ipparts);
10081 if (strpos($addr, $ipnet) !== 0) {
10082 continue;
10084 $ipparts = explode(':', $ipend);
10085 $end = hexdec($ipparts[7]);
10087 $addrend = hexdec($addrparts[7]);
10089 if (($addrend >= $start) and ($addrend <= $end)) {
10090 return true;
10093 } else {
10094 // IPv4
10095 if ($ipv6) {
10096 continue;
10098 $ipstart = cleanremoteaddr(trim($parts[0]), false); // normalise
10099 if ($ipstart === null) {
10100 continue;
10102 $ipparts = explode('.', $ipstart);
10103 $ipparts[3] = trim($parts[1]);
10104 $ipend = cleanremoteaddr(implode('.', $ipparts), false); // normalise
10105 if ($ipend === null) {
10106 continue;
10109 if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
10110 return true;
10114 } else {
10115 /// 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
10116 if (strpos($subnet, ':') !== false) {
10117 // IPv6
10118 if (!$ipv6) {
10119 continue;
10121 $parts = explode(':', $subnet);
10122 $count = count($parts);
10123 if ($parts[$count-1] === '') {
10124 unset($parts[$count-1]); // trim trailing :
10125 $count--;
10126 $subnet = implode('.', $parts);
10128 $isip = cleanremoteaddr($subnet, false); // normalise
10129 if ($isip !== null) {
10130 if ($isip === $addr) {
10131 return true;
10133 continue;
10134 } else if ($count > 8) {
10135 continue;
10137 $zeros = array_fill(0, 8-$count, '0');
10138 $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16);
10139 if (address_in_subnet($addr, $subnet)) {
10140 return true;
10143 } else {
10144 // IPv4
10145 if ($ipv6) {
10146 continue;
10148 $parts = explode('.', $subnet);
10149 $count = count($parts);
10150 if ($parts[$count-1] === '') {
10151 unset($parts[$count-1]); // trim trailing .
10152 $count--;
10153 $subnet = implode('.', $parts);
10155 if ($count == 4) {
10156 $subnet = cleanremoteaddr($subnet, false); // normalise
10157 if ($subnet === $addr) {
10158 return true;
10160 continue;
10161 } else if ($count > 4) {
10162 continue;
10164 $zeros = array_fill(0, 4-$count, '0');
10165 $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8);
10166 if (address_in_subnet($addr, $subnet)) {
10167 return true;
10173 return false;
10177 * For outputting debugging info
10179 * @uses STDOUT
10180 * @param string $string The string to write
10181 * @param string $eol The end of line char(s) to use
10182 * @param string $sleep Period to make the application sleep
10183 * This ensures any messages have time to display before redirect
10185 function mtrace($string, $eol="\n", $sleep=0) {
10187 if (defined('STDOUT') and !PHPUNIT_TEST) {
10188 fwrite(STDOUT, $string.$eol);
10189 } else {
10190 echo $string . $eol;
10193 flush();
10195 //delay to keep message on user's screen in case of subsequent redirect
10196 if ($sleep) {
10197 sleep($sleep);
10202 * Replace 1 or more slashes or backslashes to 1 slash
10204 * @param string $path The path to strip
10205 * @return string the path with double slashes removed
10207 function cleardoubleslashes ($path) {
10208 return preg_replace('/(\/|\\\){1,}/','/',$path);
10212 * Is current ip in give list?
10214 * @param string $list
10215 * @return bool
10217 function remoteip_in_list($list){
10218 $inlist = false;
10219 $client_ip = getremoteaddr(null);
10221 if(!$client_ip){
10222 // ensure access on cli
10223 return true;
10226 $list = explode("\n", $list);
10227 foreach($list as $subnet) {
10228 $subnet = trim($subnet);
10229 if (address_in_subnet($client_ip, $subnet)) {
10230 $inlist = true;
10231 break;
10234 return $inlist;
10238 * Returns most reliable client address
10240 * @global object
10241 * @param string $default If an address can't be determined, then return this
10242 * @return string The remote IP address
10244 function getremoteaddr($default='0.0.0.0') {
10245 global $CFG;
10247 if (empty($CFG->getremoteaddrconf)) {
10248 // This will happen, for example, before just after the upgrade, as the
10249 // user is redirected to the admin screen.
10250 $variablestoskip = 0;
10251 } else {
10252 $variablestoskip = $CFG->getremoteaddrconf;
10254 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) {
10255 if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
10256 $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']);
10257 return $address ? $address : $default;
10260 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
10261 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
10262 $address = cleanremoteaddr($_SERVER['HTTP_X_FORWARDED_FOR']);
10263 return $address ? $address : $default;
10266 if (!empty($_SERVER['REMOTE_ADDR'])) {
10267 $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']);
10268 return $address ? $address : $default;
10269 } else {
10270 return $default;
10275 * Cleans an ip address. Internal addresses are now allowed.
10276 * (Originally local addresses were not allowed.)
10278 * @param string $addr IPv4 or IPv6 address
10279 * @param bool $compress use IPv6 address compression
10280 * @return string normalised ip address string, null if error
10282 function cleanremoteaddr($addr, $compress=false) {
10283 $addr = trim($addr);
10285 //TODO: maybe add a separate function is_addr_public() or something like this
10287 if (strpos($addr, ':') !== false) {
10288 // can be only IPv6
10289 $parts = explode(':', $addr);
10290 $count = count($parts);
10292 if (strpos($parts[$count-1], '.') !== false) {
10293 //legacy ipv4 notation
10294 $last = array_pop($parts);
10295 $ipv4 = cleanremoteaddr($last, true);
10296 if ($ipv4 === null) {
10297 return null;
10299 $bits = explode('.', $ipv4);
10300 $parts[] = dechex($bits[0]).dechex($bits[1]);
10301 $parts[] = dechex($bits[2]).dechex($bits[3]);
10302 $count = count($parts);
10303 $addr = implode(':', $parts);
10306 if ($count < 3 or $count > 8) {
10307 return null; // severly malformed
10310 if ($count != 8) {
10311 if (strpos($addr, '::') === false) {
10312 return null; // malformed
10314 // uncompress ::
10315 $insertat = array_search('', $parts, true);
10316 $missing = array_fill(0, 1 + 8 - $count, '0');
10317 array_splice($parts, $insertat, 1, $missing);
10318 foreach ($parts as $key=>$part) {
10319 if ($part === '') {
10320 $parts[$key] = '0';
10325 $adr = implode(':', $parts);
10326 if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) {
10327 return null; // incorrect format - sorry
10330 // normalise 0s and case
10331 $parts = array_map('hexdec', $parts);
10332 $parts = array_map('dechex', $parts);
10334 $result = implode(':', $parts);
10336 if (!$compress) {
10337 return $result;
10340 if ($result === '0:0:0:0:0:0:0:0') {
10341 return '::'; // all addresses
10344 $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1);
10345 if ($compressed !== $result) {
10346 return $compressed;
10349 $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
10350 if ($compressed !== $result) {
10351 return $compressed;
10354 $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
10355 if ($compressed !== $result) {
10356 return $compressed;
10359 return $result;
10362 // first get all things that look like IPv4 addresses
10363 $parts = array();
10364 if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
10365 return null;
10367 unset($parts[0]);
10369 foreach ($parts as $key=>$match) {
10370 if ($match > 255) {
10371 return null;
10373 $parts[$key] = (int)$match; // normalise 0s
10376 return implode('.', $parts);
10380 * This function will make a complete copy of anything it's given,
10381 * regardless of whether it's an object or not.
10383 * @param mixed $thing Something you want cloned
10384 * @return mixed What ever it is you passed it
10386 function fullclone($thing) {
10387 return unserialize(serialize($thing));
10392 * This function expects to called during shutdown
10393 * should be set via register_shutdown_function()
10394 * in lib/setup.php .
10396 * @return void
10398 function moodle_request_shutdown() {
10399 global $CFG;
10401 // help apache server if possible
10402 $apachereleasemem = false;
10403 if (function_exists('apache_child_terminate') && function_exists('memory_get_usage')
10404 && ini_get_bool('child_terminate')) {
10406 $limit = (empty($CFG->apachemaxmem) ? 64*1024*1024 : $CFG->apachemaxmem); //64MB default
10407 if (memory_get_usage() > get_real_size($limit)) {
10408 $apachereleasemem = $limit;
10409 @apache_child_terminate();
10413 // deal with perf logging
10414 if (defined('MDL_PERF') || (!empty($CFG->perfdebug) and $CFG->perfdebug > 7)) {
10415 if ($apachereleasemem) {
10416 error_log('Mem usage over '.$apachereleasemem.': marking Apache child for reaping.');
10418 if (defined('MDL_PERFTOLOG')) {
10419 $perf = get_performance_info();
10420 error_log("PERF: " . $perf['txt']);
10422 if (defined('MDL_PERFINC')) {
10423 $inc = get_included_files();
10424 $ts = 0;
10425 foreach($inc as $f) {
10426 if (preg_match(':^/:', $f)) {
10427 $fs = filesize($f);
10428 $ts += $fs;
10429 $hfs = display_size($fs);
10430 error_log(substr($f,strlen($CFG->dirroot)) . " size: $fs ($hfs)"
10431 , NULL, NULL, 0);
10432 } else {
10433 error_log($f , NULL, NULL, 0);
10436 if ($ts > 0 ) {
10437 $hts = display_size($ts);
10438 error_log("Total size of files included: $ts ($hts)");
10445 * If new messages are waiting for the current user, then insert
10446 * JavaScript to pop up the messaging window into the page
10448 * @global moodle_page $PAGE
10449 * @return void
10451 function message_popup_window() {
10452 global $USER, $DB, $PAGE, $CFG, $SITE;
10454 if (!$PAGE->get_popup_notification_allowed() || empty($CFG->messaging)) {
10455 return;
10458 if (!isloggedin() || isguestuser()) {
10459 return;
10462 if (!isset($USER->message_lastpopup)) {
10463 $USER->message_lastpopup = 0;
10464 } else if ($USER->message_lastpopup > (time()-120)) {
10465 //dont run the query to check whether to display a popup if its been run in the last 2 minutes
10466 return;
10469 //a quick query to check whether the user has new messages
10470 $messagecount = $DB->count_records('message', array('useridto' => $USER->id));
10471 if ($messagecount<1) {
10472 return;
10475 //got unread messages so now do another query that joins with the user table
10476 $messagesql = "SELECT m.id, m.smallmessage, m.fullmessageformat, m.notification, u.firstname, u.lastname
10477 FROM {message} m
10478 JOIN {message_working} mw ON m.id=mw.unreadmessageid
10479 JOIN {message_processors} p ON mw.processorid=p.id
10480 JOIN {user} u ON m.useridfrom=u.id
10481 WHERE m.useridto = :userid
10482 AND p.name='popup'";
10484 //if the user was last notified over an hour ago we can renotify them of old messages
10485 //so don't worry about when the new message was sent
10486 $lastnotifiedlongago = $USER->message_lastpopup < (time()-3600);
10487 if (!$lastnotifiedlongago) {
10488 $messagesql .= 'AND m.timecreated > :lastpopuptime';
10491 $message_users = $DB->get_records_sql($messagesql, array('userid'=>$USER->id, 'lastpopuptime'=>$USER->message_lastpopup));
10493 //if we have new messages to notify the user about
10494 if (!empty($message_users)) {
10496 $strmessages = '';
10497 if (count($message_users)>1) {
10498 $strmessages = get_string('unreadnewmessages', 'message', count($message_users));
10499 } else {
10500 $message_users = reset($message_users);
10502 //show who the message is from if its not a notification
10503 if (!$message_users->notification) {
10504 $strmessages = get_string('unreadnewmessage', 'message', fullname($message_users) );
10507 //try to display the small version of the message
10508 $smallmessage = null;
10509 if (!empty($message_users->smallmessage)) {
10510 //display the first 200 chars of the message in the popup
10511 $smallmessage = null;
10512 if (textlib::strlen($message_users->smallmessage) > 200) {
10513 $smallmessage = textlib::substr($message_users->smallmessage,0,200).'...';
10514 } else {
10515 $smallmessage = $message_users->smallmessage;
10518 //prevent html symbols being displayed
10519 if ($message_users->fullmessageformat == FORMAT_HTML) {
10520 $smallmessage = html_to_text($smallmessage);
10521 } else {
10522 $smallmessage = s($smallmessage);
10524 } else if ($message_users->notification) {
10525 //its a notification with no smallmessage so just say they have a notification
10526 $smallmessage = get_string('unreadnewnotification', 'message');
10528 if (!empty($smallmessage)) {
10529 $strmessages .= '<div id="usermessage">'.s($smallmessage).'</div>';
10533 $strgomessage = get_string('gotomessages', 'message');
10534 $strstaymessage = get_string('ignore','admin');
10536 $url = $CFG->wwwroot.'/message/index.php';
10537 $content = html_writer::start_tag('div', array('id'=>'newmessageoverlay','class'=>'mdl-align')).
10538 html_writer::start_tag('div', array('id'=>'newmessagetext')).
10539 $strmessages.
10540 html_writer::end_tag('div').
10542 html_writer::start_tag('div', array('id'=>'newmessagelinks')).
10543 html_writer::link($url, $strgomessage, array('id'=>'notificationyes')).'&nbsp;&nbsp;&nbsp;'.
10544 html_writer::link('', $strstaymessage, array('id'=>'notificationno')).
10545 html_writer::end_tag('div');
10546 html_writer::end_tag('div');
10548 $PAGE->requires->js_init_call('M.core_message.init_notification', array('', $content, $url));
10550 $USER->message_lastpopup = time();
10555 * Used to make sure that $min <= $value <= $max
10557 * Make sure that value is between min, and max
10559 * @param int $min The minimum value
10560 * @param int $value The value to check
10561 * @param int $max The maximum value
10563 function bounded_number($min, $value, $max) {
10564 if($value < $min) {
10565 return $min;
10567 if($value > $max) {
10568 return $max;
10570 return $value;
10574 * Check if there is a nested array within the passed array
10576 * @param array $array
10577 * @return bool true if there is a nested array false otherwise
10579 function array_is_nested($array) {
10580 foreach ($array as $value) {
10581 if (is_array($value)) {
10582 return true;
10585 return false;
10589 * get_performance_info() pairs up with init_performance_info()
10590 * loaded in setup.php. Returns an array with 'html' and 'txt'
10591 * values ready for use, and each of the individual stats provided
10592 * separately as well.
10594 * @global object
10595 * @global object
10596 * @global object
10597 * @return array
10599 function get_performance_info() {
10600 global $CFG, $PERF, $DB, $PAGE;
10602 $info = array();
10603 $info['html'] = ''; // holds userfriendly HTML representation
10604 $info['txt'] = me() . ' '; // holds log-friendly representation
10606 $info['realtime'] = microtime_diff($PERF->starttime, microtime());
10608 $info['html'] .= '<span class="timeused">'.$info['realtime'].' secs</span> ';
10609 $info['txt'] .= 'time: '.$info['realtime'].'s ';
10611 if (function_exists('memory_get_usage')) {
10612 $info['memory_total'] = memory_get_usage();
10613 $info['memory_growth'] = memory_get_usage() - $PERF->startmemory;
10614 $info['html'] .= '<span class="memoryused">RAM: '.display_size($info['memory_total']).'</span> ';
10615 $info['txt'] .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '.$info['memory_growth'].'B ('.display_size($info['memory_growth']).') ';
10618 if (function_exists('memory_get_peak_usage')) {
10619 $info['memory_peak'] = memory_get_peak_usage();
10620 $info['html'] .= '<span class="memoryused">RAM peak: '.display_size($info['memory_peak']).'</span> ';
10621 $info['txt'] .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') ';
10624 $inc = get_included_files();
10625 //error_log(print_r($inc,1));
10626 $info['includecount'] = count($inc);
10627 $info['html'] .= '<span class="included">Included '.$info['includecount'].' files</span> ';
10628 $info['txt'] .= 'includecount: '.$info['includecount'].' ';
10630 if (!empty($CFG->early_install_lang) or empty($PAGE)) {
10631 // We can not track more performance before installation or before PAGE init, sorry.
10632 return $info;
10635 $filtermanager = filter_manager::instance();
10636 if (method_exists($filtermanager, 'get_performance_summary')) {
10637 list($filterinfo, $nicenames) = $filtermanager->get_performance_summary();
10638 $info = array_merge($filterinfo, $info);
10639 foreach ($filterinfo as $key => $value) {
10640 $info['html'] .= "<span class='$key'>$nicenames[$key]: $value </span> ";
10641 $info['txt'] .= "$key: $value ";
10645 $stringmanager = get_string_manager();
10646 if (method_exists($stringmanager, 'get_performance_summary')) {
10647 list($filterinfo, $nicenames) = $stringmanager->get_performance_summary();
10648 $info = array_merge($filterinfo, $info);
10649 foreach ($filterinfo as $key => $value) {
10650 $info['html'] .= "<span class='$key'>$nicenames[$key]: $value </span> ";
10651 $info['txt'] .= "$key: $value ";
10655 $jsmodules = $PAGE->requires->get_loaded_modules();
10656 if ($jsmodules) {
10657 $yuicount = 0;
10658 $othercount = 0;
10659 $details = '';
10660 foreach ($jsmodules as $module => $backtraces) {
10661 if (strpos($module, 'yui') === 0) {
10662 $yuicount += 1;
10663 } else {
10664 $othercount += 1;
10666 if (!empty($CFG->yuimoduledebug)) {
10667 // hidden feature for developers working on YUI module infrastructure
10668 $details .= "<div class='yui-module'><p>$module</p>";
10669 foreach ($backtraces as $backtrace) {
10670 $details .= "<div class='backtrace'>$backtrace</div>";
10672 $details .= '</div>';
10675 $info['html'] .= "<span class='includedyuimodules'>Included YUI modules: $yuicount</span> ";
10676 $info['txt'] .= "includedyuimodules: $yuicount ";
10677 $info['html'] .= "<span class='includedjsmodules'>Other JavaScript modules: $othercount</span> ";
10678 $info['txt'] .= "includedjsmodules: $othercount ";
10679 if ($details) {
10680 $info['html'] .= '<div id="yui-module-debug" class="notifytiny">'.$details.'</div>';
10684 if (!empty($PERF->logwrites)) {
10685 $info['logwrites'] = $PERF->logwrites;
10686 $info['html'] .= '<span class="logwrites">Log DB writes '.$info['logwrites'].'</span> ';
10687 $info['txt'] .= 'logwrites: '.$info['logwrites'].' ';
10690 $info['dbqueries'] = $DB->perf_get_reads().'/'.($DB->perf_get_writes() - $PERF->logwrites);
10691 $info['html'] .= '<span class="dbqueries">DB reads/writes: '.$info['dbqueries'].'</span> ';
10692 $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' ';
10694 if (function_exists('posix_times')) {
10695 $ptimes = posix_times();
10696 if (is_array($ptimes)) {
10697 foreach ($ptimes as $key => $val) {
10698 $info[$key] = $ptimes[$key] - $PERF->startposixtimes[$key];
10700 $info['html'] .= "<span class=\"posixtimes\">ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</span> ";
10701 $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] ";
10705 // Grab the load average for the last minute
10706 // /proc will only work under some linux configurations
10707 // while uptime is there under MacOSX/Darwin and other unices
10708 if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) {
10709 list($server_load) = explode(' ', $loadavg[0]);
10710 unset($loadavg);
10711 } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) {
10712 if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) {
10713 $server_load = $matches[1];
10714 } else {
10715 trigger_error('Could not parse uptime output!');
10718 if (!empty($server_load)) {
10719 $info['serverload'] = $server_load;
10720 $info['html'] .= '<span class="serverload">Load average: '.$info['serverload'].'</span> ';
10721 $info['txt'] .= "serverload: {$info['serverload']} ";
10724 // Display size of session if session started
10725 if (session_id()) {
10726 $info['sessionsize'] = display_size(strlen(session_encode()));
10727 $info['html'] .= '<span class="sessionsize">Session: ' . $info['sessionsize'] . '</span> ';
10728 $info['txt'] .= "Session: {$info['sessionsize']} ";
10731 if ($stats = cache_helper::get_stats()) {
10732 $html = '<span class="cachesused">';
10733 $html .= '<span class="cache-stats-heading">Caches used (hits/misses/sets)</span>';
10734 $text = 'Caches used (hits/misses/sets): ';
10735 $hits = 0;
10736 $misses = 0;
10737 $sets = 0;
10738 foreach ($stats as $definition => $stores) {
10739 $html .= '<span class="cache-definition-stats">';
10740 $html .= '<span class="cache-definition-stats-heading">'.$definition.'</span>';
10741 $text .= "$definition {";
10742 foreach ($stores as $store => $data) {
10743 $hits += $data['hits'];
10744 $misses += $data['misses'];
10745 $sets += $data['sets'];
10746 if ($data['hits'] == 0 and $data['misses'] > 0) {
10747 $cachestoreclass = 'nohits';
10748 } else if ($data['hits'] < $data['misses']) {
10749 $cachestoreclass = 'lowhits';
10750 } else {
10751 $cachestoreclass = 'hihits';
10753 $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
10754 $html .= "<span class=\"cache-store-stats $cachestoreclass\">$store: $data[hits] / $data[misses] / $data[sets]</span>";
10756 $html .= '</span>';
10757 $text .= '} ';
10759 $html .= "<span class='cache-total-stats'>Total: $hits / $misses / $sets</span>";
10760 $html .= '</span> ';
10761 $info['cachesused'] = "$hits / $misses / $sets";
10762 $info['html'] .= $html;
10763 $info['txt'] .= $text.'. ';
10764 } else {
10765 $info['cachesused'] = '0 / 0 / 0';
10766 $info['html'] .= '<span class="cachesused">Caches used (hits/misses/sets): 0/0/0</span>';
10767 $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 ';
10770 $info['html'] = '<div class="performanceinfo siteinfo">'.$info['html'].'</div>';
10771 return $info;
10775 * @todo Document this function linux people
10777 function apd_get_profiling() {
10778 return shell_exec('pprofp -u ' . ini_get('apd.dumpdir') . '/pprof.' . getmypid() . '.*');
10782 * Delete directory or only its content
10784 * @param string $dir directory path
10785 * @param bool $content_only
10786 * @return bool success, true also if dir does not exist
10788 function remove_dir($dir, $content_only=false) {
10789 if (!file_exists($dir)) {
10790 // nothing to do
10791 return true;
10793 if (!$handle = opendir($dir)) {
10794 return false;
10796 $result = true;
10797 while (false!==($item = readdir($handle))) {
10798 if($item != '.' && $item != '..') {
10799 if(is_dir($dir.'/'.$item)) {
10800 $result = remove_dir($dir.'/'.$item) && $result;
10801 }else{
10802 $result = unlink($dir.'/'.$item) && $result;
10806 closedir($handle);
10807 if ($content_only) {
10808 clearstatcache(); // make sure file stat cache is properly invalidated
10809 return $result;
10811 $result = rmdir($dir); // if anything left the result will be false, no need for && $result
10812 clearstatcache(); // make sure file stat cache is properly invalidated
10813 return $result;
10817 * Detect if an object or a class contains a given property
10818 * will take an actual object or the name of a class
10820 * @param mix $obj Name of class or real object to test
10821 * @param string $property name of property to find
10822 * @return bool true if property exists
10824 function object_property_exists( $obj, $property ) {
10825 if (is_string( $obj )) {
10826 $properties = get_class_vars( $obj );
10828 else {
10829 $properties = get_object_vars( $obj );
10831 return array_key_exists( $property, $properties );
10835 * Converts an object into an associative array
10837 * This function converts an object into an associative array by iterating
10838 * over its public properties. Because this function uses the foreach
10839 * construct, Iterators are respected. It works recursively on arrays of objects.
10840 * Arrays and simple values are returned as is.
10842 * If class has magic properties, it can implement IteratorAggregate
10843 * and return all available properties in getIterator()
10845 * @param mixed $var
10846 * @return array
10848 function convert_to_array($var) {
10849 $result = array();
10851 // loop over elements/properties
10852 foreach ($var as $key => $value) {
10853 // recursively convert objects
10854 if (is_object($value) || is_array($value)) {
10855 $result[$key] = convert_to_array($value);
10856 } else {
10857 // simple values are untouched
10858 $result[$key] = $value;
10861 return $result;
10865 * Detect a custom script replacement in the data directory that will
10866 * replace an existing moodle script
10868 * @return string|bool full path name if a custom script exists, false if no custom script exists
10870 function custom_script_path() {
10871 global $CFG, $SCRIPT;
10873 if ($SCRIPT === null) {
10874 // Probably some weird external script
10875 return false;
10878 $scriptpath = $CFG->customscripts . $SCRIPT;
10880 // check the custom script exists
10881 if (file_exists($scriptpath) and is_file($scriptpath)) {
10882 return $scriptpath;
10883 } else {
10884 return false;
10889 * Returns whether or not the user object is a remote MNET user. This function
10890 * is in moodlelib because it does not rely on loading any of the MNET code.
10892 * @global object
10893 * @param object $user A valid user object
10894 * @return bool True if the user is from a remote Moodle.
10896 function is_mnet_remote_user($user) {
10897 global $CFG;
10899 if (!isset($CFG->mnet_localhost_id)) {
10900 include_once $CFG->dirroot . '/mnet/lib.php';
10901 $env = new mnet_environment();
10902 $env->init();
10903 unset($env);
10906 return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id);
10910 * This function will search for browser prefereed languages, setting Moodle
10911 * to use the best one available if $SESSION->lang is undefined
10913 * @global object
10914 * @global object
10915 * @global object
10917 function setup_lang_from_browser() {
10919 global $CFG, $SESSION, $USER;
10921 if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) {
10922 // Lang is defined in session or user profile, nothing to do
10923 return;
10926 if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do
10927 return;
10930 /// Extract and clean langs from headers
10931 $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
10932 $rawlangs = str_replace('-', '_', $rawlangs); // we are using underscores
10933 $rawlangs = explode(',', $rawlangs); // Convert to array
10934 $langs = array();
10936 $order = 1.0;
10937 foreach ($rawlangs as $lang) {
10938 if (strpos($lang, ';') === false) {
10939 $langs[(string)$order] = $lang;
10940 $order = $order-0.01;
10941 } else {
10942 $parts = explode(';', $lang);
10943 $pos = strpos($parts[1], '=');
10944 $langs[substr($parts[1], $pos+1)] = $parts[0];
10947 krsort($langs, SORT_NUMERIC);
10949 /// Look for such langs under standard locations
10950 foreach ($langs as $lang) {
10951 $lang = strtolower(clean_param($lang, PARAM_SAFEDIR)); // clean it properly for include
10952 if (get_string_manager()->translation_exists($lang, false)) {
10953 $SESSION->lang = $lang; /// Lang exists, set it in session
10954 break; /// We have finished. Go out
10957 return;
10961 * check if $url matches anything in proxybypass list
10963 * any errors just result in the proxy being used (least bad)
10965 * @global object
10966 * @param string $url url to check
10967 * @return boolean true if we should bypass the proxy
10969 function is_proxybypass( $url ) {
10970 global $CFG;
10972 // sanity check
10973 if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) {
10974 return false;
10977 // get the host part out of the url
10978 if (!$host = parse_url( $url, PHP_URL_HOST )) {
10979 return false;
10982 // get the possible bypass hosts into an array
10983 $matches = explode( ',', $CFG->proxybypass );
10985 // check for a match
10986 // (IPs need to match the left hand side and hosts the right of the url,
10987 // but we can recklessly check both as there can't be a false +ve)
10988 $bypass = false;
10989 foreach ($matches as $match) {
10990 $match = trim($match);
10992 // try for IP match (Left side)
10993 $lhs = substr($host,0,strlen($match));
10994 if (strcasecmp($match,$lhs)==0) {
10995 return true;
10998 // try for host match (Right side)
10999 $rhs = substr($host,-strlen($match));
11000 if (strcasecmp($match,$rhs)==0) {
11001 return true;
11005 // nothing matched.
11006 return false;
11010 ////////////////////////////////////////////////////////////////////////////////
11013 * Check if the passed navigation is of the new style
11015 * @param mixed $navigation
11016 * @return bool true for yes false for no
11018 function is_newnav($navigation) {
11019 if (is_array($navigation) && !empty($navigation['newnav'])) {
11020 return true;
11021 } else {
11022 return false;
11027 * Checks whether the given variable name is defined as a variable within the given object.
11029 * This will NOT work with stdClass objects, which have no class variables.
11031 * @param string $var The variable name
11032 * @param object $object The object to check
11033 * @return boolean
11035 function in_object_vars($var, $object) {
11036 $class_vars = get_class_vars(get_class($object));
11037 $class_vars = array_keys($class_vars);
11038 return in_array($var, $class_vars);
11042 * Returns an array without repeated objects.
11043 * This function is similar to array_unique, but for arrays that have objects as values
11045 * @param array $array
11046 * @param bool $keep_key_assoc
11047 * @return array
11049 function object_array_unique($array, $keep_key_assoc = true) {
11050 $duplicate_keys = array();
11051 $tmp = array();
11053 foreach ($array as $key=>$val) {
11054 // convert objects to arrays, in_array() does not support objects
11055 if (is_object($val)) {
11056 $val = (array)$val;
11059 if (!in_array($val, $tmp)) {
11060 $tmp[] = $val;
11061 } else {
11062 $duplicate_keys[] = $key;
11066 foreach ($duplicate_keys as $key) {
11067 unset($array[$key]);
11070 return $keep_key_assoc ? $array : array_values($array);
11074 * Is a userid the primary administrator?
11076 * @param int $userid int id of user to check
11077 * @return boolean
11079 function is_primary_admin($userid){
11080 $primaryadmin = get_admin();
11082 if($userid == $primaryadmin->id){
11083 return true;
11084 }else{
11085 return false;
11090 * Returns the site identifier
11092 * @global object
11093 * @return string $CFG->siteidentifier, first making sure it is properly initialised.
11095 function get_site_identifier() {
11096 global $CFG;
11097 // Check to see if it is missing. If so, initialise it.
11098 if (empty($CFG->siteidentifier)) {
11099 set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']);
11101 // Return it.
11102 return $CFG->siteidentifier;
11106 * Check whether the given password has no more than the specified
11107 * number of consecutive identical characters.
11109 * @param string $password password to be checked against the password policy
11110 * @param integer $maxchars maximum number of consecutive identical characters
11112 function check_consecutive_identical_characters($password, $maxchars) {
11114 if ($maxchars < 1) {
11115 return true; // 0 is to disable this check
11117 if (strlen($password) <= $maxchars) {
11118 return true; // too short to fail this test
11121 $previouschar = '';
11122 $consecutivecount = 1;
11123 foreach (str_split($password) as $char) {
11124 if ($char != $previouschar) {
11125 $consecutivecount = 1;
11127 else {
11128 $consecutivecount++;
11129 if ($consecutivecount > $maxchars) {
11130 return false; // check failed already
11134 $previouschar = $char;
11137 return true;
11141 * helper function to do partial function binding
11142 * so we can use it for preg_replace_callback, for example
11143 * this works with php functions, user functions, static methods and class methods
11144 * it returns you a callback that you can pass on like so:
11146 * $callback = partial('somefunction', $arg1, $arg2);
11147 * or
11148 * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
11149 * or even
11150 * $obj = new someclass();
11151 * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2);
11153 * and then the arguments that are passed through at calltime are appended to the argument list.
11155 * @param mixed $function a php callback
11156 * $param mixed $arg1.. $argv arguments to partially bind with
11158 * @return callback
11160 function partial() {
11161 if (!class_exists('partial')) {
11162 class partial{
11163 var $values = array();
11164 var $func;
11166 function __construct($func, $args) {
11167 $this->values = $args;
11168 $this->func = $func;
11171 function method() {
11172 $args = func_get_args();
11173 return call_user_func_array($this->func, array_merge($this->values, $args));
11177 $args = func_get_args();
11178 $func = array_shift($args);
11179 $p = new partial($func, $args);
11180 return array($p, 'method');
11184 * helper function to load up and initialise the mnet environment
11185 * this must be called before you use mnet functions.
11187 * @return mnet_environment the equivalent of old $MNET global
11189 function get_mnet_environment() {
11190 global $CFG;
11191 require_once($CFG->dirroot . '/mnet/lib.php');
11192 static $instance = null;
11193 if (empty($instance)) {
11194 $instance = new mnet_environment();
11195 $instance->init();
11197 return $instance;
11201 * during xmlrpc server code execution, any code wishing to access
11202 * information about the remote peer must use this to get it.
11204 * @return mnet_remote_client the equivalent of old $MNET_REMOTE_CLIENT global
11206 function get_mnet_remote_client() {
11207 if (!defined('MNET_SERVER')) {
11208 debugging(get_string('notinxmlrpcserver', 'mnet'));
11209 return false;
11211 global $MNET_REMOTE_CLIENT;
11212 if (isset($MNET_REMOTE_CLIENT)) {
11213 return $MNET_REMOTE_CLIENT;
11215 return false;
11219 * during the xmlrpc server code execution, this will be called
11220 * to setup the object returned by {@see get_mnet_remote_client}
11222 * @param mnet_remote_client $client the client to set up
11224 function set_mnet_remote_client($client) {
11225 if (!defined('MNET_SERVER')) {
11226 throw new moodle_exception('notinxmlrpcserver', 'mnet');
11228 global $MNET_REMOTE_CLIENT;
11229 $MNET_REMOTE_CLIENT = $client;
11233 * return the jump url for a given remote user
11234 * this is used for rewriting forum post links in emails, etc
11236 * @param stdclass $user the user to get the idp url for
11238 function mnet_get_idp_jump_url($user) {
11239 global $CFG;
11241 static $mnetjumps = array();
11242 if (!array_key_exists($user->mnethostid, $mnetjumps)) {
11243 $idp = mnet_get_peer_host($user->mnethostid);
11244 $idpjumppath = mnet_get_app_jumppath($idp->applicationid);
11245 $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl=';
11247 return $mnetjumps[$user->mnethostid];
11251 * Gets the homepage to use for the current user
11253 * @return int One of HOMEPAGE_*
11255 function get_home_page() {
11256 global $CFG;
11258 if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) {
11259 if ($CFG->defaulthomepage == HOMEPAGE_MY) {
11260 return HOMEPAGE_MY;
11261 } else {
11262 return (int)get_user_preferences('user_home_page_preference', HOMEPAGE_MY);
11265 return HOMEPAGE_SITE;
11269 * Gets the name of a course to be displayed when showing a list of courses.
11270 * By default this is just $course->fullname but user can configure it. The
11271 * result of this function should be passed through print_string.
11272 * @param stdClass|course_in_list $course Moodle course object
11273 * @return string Display name of course (either fullname or short + fullname)
11275 function get_course_display_name_for_list($course) {
11276 global $CFG;
11277 if (!empty($CFG->courselistshortnames)) {
11278 if (!($course instanceof stdClass)) {
11279 $course = (object)convert_to_array($course);
11281 return get_string('courseextendednamedisplay', '', $course);
11282 } else {
11283 return $course->fullname;
11288 * The lang_string class
11290 * This special class is used to create an object representation of a string request.
11291 * It is special because processing doesn't occur until the object is first used.
11292 * The class was created especially to aid performance in areas where strings were
11293 * required to be generated but were not necessarily used.
11294 * As an example the admin tree when generated uses over 1500 strings, of which
11295 * normally only 1/3 are ever actually printed at any time.
11296 * The performance advantage is achieved by not actually processing strings that
11297 * arn't being used, as such reducing the processing required for the page.
11299 * How to use the lang_string class?
11300 * There are two methods of using the lang_string class, first through the
11301 * forth argument of the get_string function, and secondly directly.
11302 * The following are examples of both.
11303 * 1. Through get_string calls e.g.
11304 * $string = get_string($identifier, $component, $a, true);
11305 * $string = get_string('yes', 'moodle', null, true);
11306 * 2. Direct instantiation
11307 * $string = new lang_string($identifier, $component, $a, $lang);
11308 * $string = new lang_string('yes');
11310 * How do I use a lang_string object?
11311 * The lang_string object makes use of a magic __toString method so that you
11312 * are able to use the object exactly as you would use a string in most cases.
11313 * This means you are able to collect it into a variable and then directly
11314 * echo it, or concatenate it into another string, or similar.
11315 * The other thing you can do is manually get the string by calling the
11316 * lang_strings out method e.g.
11317 * $string = new lang_string('yes');
11318 * $string->out();
11319 * Also worth noting is that the out method can take one argument, $lang which
11320 * allows the developer to change the language on the fly.
11322 * When should I use a lang_string object?
11323 * The lang_string object is designed to be used in any situation where a
11324 * string may not be needed, but needs to be generated.
11325 * The admin tree is a good example of where lang_string objects should be
11326 * used.
11327 * A more practical example would be any class that requries strings that may
11328 * not be printed (after all classes get renderer by renderers and who knows
11329 * what they will do ;))
11331 * When should I not use a lang_string object?
11332 * Don't use lang_strings when you are going to use a string immediately.
11333 * There is no need as it will be processed immediately and there will be no
11334 * advantage, and in fact perhaps a negative hit as a class has to be
11335 * instantiated for a lang_string object, however get_string won't require
11336 * that.
11338 * Limitations:
11339 * 1. You cannot use a lang_string object as an array offset. Doing so will
11340 * result in PHP throwing an error. (You can use it as an object property!)
11342 * @package core
11343 * @category string
11344 * @copyright 2011 Sam Hemelryk
11345 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
11347 class lang_string {
11349 /** @var string The strings identifier */
11350 protected $identifier;
11351 /** @var string The strings component. Default '' */
11352 protected $component = '';
11353 /** @var array|stdClass Any arguments required for the string. Default null */
11354 protected $a = null;
11355 /** @var string The language to use when processing the string. Default null */
11356 protected $lang = null;
11358 /** @var string The processed string (once processed) */
11359 protected $string = null;
11362 * A special boolean. If set to true then the object has been woken up and
11363 * cannot be regenerated. If this is set then $this->string MUST be used.
11364 * @var bool
11366 protected $forcedstring = false;
11369 * Constructs a lang_string object
11371 * This function should do as little processing as possible to ensure the best
11372 * performance for strings that won't be used.
11374 * @param string $identifier The strings identifier
11375 * @param string $component The strings component
11376 * @param stdClass|array $a Any arguments the string requires
11377 * @param string $lang The language to use when processing the string.
11379 public function __construct($identifier, $component = '', $a = null, $lang = null) {
11380 if (empty($component)) {
11381 $component = 'moodle';
11384 $this->identifier = $identifier;
11385 $this->component = $component;
11386 $this->lang = $lang;
11388 // We MUST duplicate $a to ensure that it if it changes by reference those
11389 // changes are not carried across.
11390 // To do this we always ensure $a or its properties/values are strings
11391 // and that any properties/values that arn't convertable are forgotten.
11392 if (!empty($a)) {
11393 if (is_scalar($a)) {
11394 $this->a = $a;
11395 } else if ($a instanceof lang_string) {
11396 $this->a = $a->out();
11397 } else if (is_object($a) or is_array($a)) {
11398 $a = (array)$a;
11399 $this->a = array();
11400 foreach ($a as $key => $value) {
11401 // Make sure conversion errors don't get displayed (results in '')
11402 if (is_array($value)) {
11403 $this->a[$key] = '';
11404 } else if (is_object($value)) {
11405 if (method_exists($value, '__toString')) {
11406 $this->a[$key] = $value->__toString();
11407 } else {
11408 $this->a[$key] = '';
11410 } else {
11411 $this->a[$key] = (string)$value;
11417 if (debugging(false, DEBUG_DEVELOPER)) {
11418 if (clean_param($this->identifier, PARAM_STRINGID) == '') {
11419 throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition');
11421 if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') {
11422 throw new coding_exception('Invalid string compontent. Please check your string definition');
11424 if (!get_string_manager()->string_exists($this->identifier, $this->component)) {
11425 debugging('String does not exist. Please check your string definition for '.$this->identifier.'/'.$this->component, DEBUG_DEVELOPER);
11431 * Processes the string.
11433 * This function actually processes the string, stores it in the string property
11434 * and then returns it.
11435 * You will notice that this function is VERY similar to the get_string method.
11436 * That is because it is pretty much doing the same thing.
11437 * However as this function is an upgrade it isn't as tolerant to backwards
11438 * compatability.
11440 * @return string
11442 protected function get_string() {
11443 global $CFG;
11445 // Check if we need to process the string
11446 if ($this->string === null) {
11447 // Check the quality of the identifier.
11448 if (debugging('', DEBUG_DEVELOPER) && clean_param($this->identifier, PARAM_STRINGID) === '') {
11449 throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition');
11452 // Process the string
11453 $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang);
11454 // Debugging feature lets you display string identifier and component
11455 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
11456 $this->string .= ' {' . $this->identifier . '/' . $this->component . '}';
11459 // Return the string
11460 return $this->string;
11464 * Returns the string
11466 * @param string $lang The langauge to use when processing the string
11467 * @return string
11469 public function out($lang = null) {
11470 if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) {
11471 if ($this->forcedstring) {
11472 debugging('lang_string objects that have been serialised and unserialised cannot be printed in another language. ('.$this->lang.' used)', DEBUG_DEVELOPER);
11473 return $this->get_string();
11475 $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang);
11476 return $translatedstring->out();
11478 return $this->get_string();
11482 * Magic __toString method for printing a string
11484 * @return string
11486 public function __toString() {
11487 return $this->get_string();
11491 * Magic __set_state method used for var_export
11493 * @return string
11495 public function __set_state() {
11496 return $this->get_string();
11500 * Prepares the lang_string for sleep and stores only the forcedstring and
11501 * string properties... the string cannot be regenerated so we need to ensure
11502 * it is generated for this.
11504 * @return string
11506 public function __sleep() {
11507 $this->get_string();
11508 $this->forcedstring = true;
11509 return array('forcedstring', 'string', 'lang');