Merge branch 'm25_MDL-40200_Notices_When_Viewing_Profile_Invalid_UserId' of https...
[moodle.git] / lib / moodlelib.php
blob976059134cfedeff8d164303a5668d950acfdc4d
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;
1185 // Lower error reporting because glibc throws bogus notices.
1186 $olderror = error_reporting();
1187 if ($olderror & E_NOTICE) {
1188 error_reporting($olderror ^ E_NOTICE);
1191 // Note: this duplicates min_fix_utf8() intentionally.
1192 static $buggyiconv = null;
1193 if ($buggyiconv === null) {
1194 $buggyiconv = (!function_exists('iconv') or iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€');
1197 if ($buggyiconv) {
1198 if (function_exists('mb_convert_encoding')) {
1199 $subst = mb_substitute_character();
1200 mb_substitute_character('');
1201 $result = mb_convert_encoding($value, 'utf-8', 'utf-8');
1202 mb_substitute_character($subst);
1204 } else {
1205 // Warn admins on admin/index.php page.
1206 $result = $value;
1209 } else {
1210 $result = iconv('UTF-8', 'UTF-8//IGNORE', $value);
1213 if ($olderror & E_NOTICE) {
1214 error_reporting($olderror);
1217 return $result;
1219 } else if (is_array($value)) {
1220 foreach ($value as $k=>$v) {
1221 $value[$k] = fix_utf8($v);
1223 return $value;
1225 } else if (is_object($value)) {
1226 $value = clone($value); // do not modify original
1227 foreach ($value as $k=>$v) {
1228 $value->$k = fix_utf8($v);
1230 return $value;
1232 } else {
1233 // this is some other type, no utf-8 here
1234 return $value;
1239 * Return true if given value is integer or string with integer value
1241 * @param mixed $value String or Int
1242 * @return bool true if number, false if not
1244 function is_number($value) {
1245 if (is_int($value)) {
1246 return true;
1247 } else if (is_string($value)) {
1248 return ((string)(int)$value) === $value;
1249 } else {
1250 return false;
1255 * Returns host part from url
1256 * @param string $url full url
1257 * @return string host, null if not found
1259 function get_host_from_url($url) {
1260 preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches);
1261 if ($matches) {
1262 return $matches[1];
1264 return null;
1268 * Tests whether anything was returned by text editor
1270 * This function is useful for testing whether something you got back from
1271 * the HTML editor actually contains anything. Sometimes the HTML editor
1272 * appear to be empty, but actually you get back a <br> tag or something.
1274 * @param string $string a string containing HTML.
1275 * @return boolean does the string contain any actual content - that is text,
1276 * images, objects, etc.
1278 function html_is_blank($string) {
1279 return trim(strip_tags($string, '<img><object><applet><input><select><textarea><hr>')) == '';
1283 * Set a key in global configuration
1285 * Set a key/value pair in both this session's {@link $CFG} global variable
1286 * and in the 'config' database table for future sessions.
1288 * Can also be used to update keys for plugin-scoped configs in config_plugin table.
1289 * In that case it doesn't affect $CFG.
1291 * A NULL value will delete the entry.
1293 * @global object
1294 * @global object
1295 * @param string $name the key to set
1296 * @param string $value the value to set (without magic quotes)
1297 * @param string $plugin (optional) the plugin scope, default NULL
1298 * @return bool true or exception
1300 function set_config($name, $value, $plugin=NULL) {
1301 global $CFG, $DB;
1303 if (empty($plugin)) {
1304 if (!array_key_exists($name, $CFG->config_php_settings)) {
1305 // So it's defined for this invocation at least
1306 if (is_null($value)) {
1307 unset($CFG->$name);
1308 } else {
1309 $CFG->$name = (string)$value; // settings from db are always strings
1313 if ($DB->get_field('config', 'name', array('name'=>$name))) {
1314 if ($value === null) {
1315 $DB->delete_records('config', array('name'=>$name));
1316 } else {
1317 $DB->set_field('config', 'value', $value, array('name'=>$name));
1319 } else {
1320 if ($value !== null) {
1321 $config = new stdClass();
1322 $config->name = $name;
1323 $config->value = $value;
1324 $DB->insert_record('config', $config, false);
1327 if ($name === 'siteidentifier') {
1328 cache_helper::update_site_identifier($value);
1330 cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1331 } else { // plugin scope
1332 if ($id = $DB->get_field('config_plugins', 'id', array('name'=>$name, 'plugin'=>$plugin))) {
1333 if ($value===null) {
1334 $DB->delete_records('config_plugins', array('name'=>$name, 'plugin'=>$plugin));
1335 } else {
1336 $DB->set_field('config_plugins', 'value', $value, array('id'=>$id));
1338 } else {
1339 if ($value !== null) {
1340 $config = new stdClass();
1341 $config->plugin = $plugin;
1342 $config->name = $name;
1343 $config->value = $value;
1344 $DB->insert_record('config_plugins', $config, false);
1347 cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1350 return true;
1354 * Get configuration values from the global config table
1355 * or the config_plugins table.
1357 * If called with one parameter, it will load all the config
1358 * variables for one plugin, and return them as an object.
1360 * If called with 2 parameters it will return a string single
1361 * value or false if the value is not found.
1363 * @static $siteidentifier The site identifier is not cached. We use this static cache so
1364 * that we need only fetch it once per request.
1365 * @param string $plugin full component name
1366 * @param string $name default NULL
1367 * @return mixed hash-like object or single value, return false no config found
1369 function get_config($plugin, $name = NULL) {
1370 global $CFG, $DB;
1372 static $siteidentifier = null;
1374 if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) {
1375 $forced =& $CFG->config_php_settings;
1376 $iscore = true;
1377 $plugin = 'core';
1378 } else {
1379 if (array_key_exists($plugin, $CFG->forced_plugin_settings)) {
1380 $forced =& $CFG->forced_plugin_settings[$plugin];
1381 } else {
1382 $forced = array();
1384 $iscore = false;
1387 if ($siteidentifier === null) {
1388 try {
1389 // This may fail during installation.
1390 // If you have a look at {@link initialise_cfg()} you will see that this is how we detect the need to
1391 // install the database.
1392 $siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
1393 } catch (dml_exception $ex) {
1394 // Set siteidentifier to false. We don't want to trip this continually.
1395 $siteidentifier = false;
1396 throw $ex;
1400 if (!empty($name)) {
1401 if (array_key_exists($name, $forced)) {
1402 return (string)$forced[$name];
1403 } else if ($name === 'siteidentifier' && $plugin == 'core') {
1404 return $siteidentifier;
1408 $cache = cache::make('core', 'config');
1409 $result = $cache->get($plugin);
1410 if ($result === false) {
1411 // the user is after a recordset
1412 $result = new stdClass;
1413 if (!$iscore) {
1414 $result = $DB->get_records_menu('config_plugins', array('plugin'=>$plugin), '', 'name,value');
1415 } else {
1416 // this part is not really used any more, but anyway...
1417 $result = $DB->get_records_menu('config', array(), '', 'name,value');;
1419 $cache->set($plugin, $result);
1422 if (!empty($name)) {
1423 if (array_key_exists($name, $result)) {
1424 return $result[$name];
1426 return false;
1429 if ($plugin === 'core') {
1430 $result['siteidentifier'] = $siteidentifier;
1433 foreach ($forced as $key => $value) {
1434 if (is_null($value) or is_array($value) or is_object($value)) {
1435 // we do not want any extra mess here, just real settings that could be saved in db
1436 unset($result[$key]);
1437 } else {
1438 //convert to string as if it went through the DB
1439 $result[$key] = (string)$value;
1443 return (object)$result;
1447 * Removes a key from global configuration
1449 * @param string $name the key to set
1450 * @param string $plugin (optional) the plugin scope
1451 * @global object
1452 * @return boolean whether the operation succeeded.
1454 function unset_config($name, $plugin=NULL) {
1455 global $CFG, $DB;
1457 if (empty($plugin)) {
1458 unset($CFG->$name);
1459 $DB->delete_records('config', array('name'=>$name));
1460 cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1461 } else {
1462 $DB->delete_records('config_plugins', array('name'=>$name, 'plugin'=>$plugin));
1463 cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1466 return true;
1470 * Remove all the config variables for a given plugin.
1472 * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice';
1473 * @return boolean whether the operation succeeded.
1475 function unset_all_config_for_plugin($plugin) {
1476 global $DB;
1477 // Delete from the obvious config_plugins first
1478 $DB->delete_records('config_plugins', array('plugin' => $plugin));
1479 // Next delete any suspect settings from config
1480 $like = $DB->sql_like('name', '?', true, true, false, '|');
1481 $params = array($DB->sql_like_escape($plugin.'_', '|') . '%');
1482 $DB->delete_records_select('config', $like, $params);
1483 // Finally clear both the plugin cache and the core cache (suspect settings now removed from core).
1484 cache_helper::invalidate_by_definition('core', 'config', array(), array('core', $plugin));
1486 return true;
1490 * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability.
1492 * All users are verified if they still have the necessary capability.
1494 * @param string $value the value of the config setting.
1495 * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor.
1496 * @param bool $include admins, include administrators
1497 * @return array of user objects.
1499 function get_users_from_config($value, $capability, $includeadmins = true) {
1500 global $CFG, $DB;
1502 if (empty($value) or $value === '$@NONE@$') {
1503 return array();
1506 // we have to make sure that users still have the necessary capability,
1507 // it should be faster to fetch them all first and then test if they are present
1508 // instead of validating them one-by-one
1509 $users = get_users_by_capability(context_system::instance(), $capability);
1510 if ($includeadmins) {
1511 $admins = get_admins();
1512 foreach ($admins as $admin) {
1513 $users[$admin->id] = $admin;
1517 if ($value === '$@ALL@$') {
1518 return $users;
1521 $result = array(); // result in correct order
1522 $allowed = explode(',', $value);
1523 foreach ($allowed as $uid) {
1524 if (isset($users[$uid])) {
1525 $user = $users[$uid];
1526 $result[$user->id] = $user;
1530 return $result;
1535 * Invalidates browser caches and cached data in temp
1537 * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
1538 * {@see phpunit_util::reset_dataroot()}
1540 * @return void
1542 function purge_all_caches() {
1543 global $CFG;
1545 reset_text_filters_cache();
1546 js_reset_all_caches();
1547 theme_reset_all_caches();
1548 get_string_manager()->reset_caches();
1549 textlib::reset_caches();
1551 cache_helper::purge_all();
1553 // purge all other caches: rss, simplepie, etc.
1554 remove_dir($CFG->cachedir.'', true);
1556 // make sure cache dir is writable, throws exception if not
1557 make_cache_directory('');
1559 // hack: this script may get called after the purifier was initialised,
1560 // but we do not want to verify repeatedly this exists in each call
1561 make_cache_directory('htmlpurifier');
1565 * Get volatile flags
1567 * @param string $type
1568 * @param int $changedsince default null
1569 * @return records array
1571 function get_cache_flags($type, $changedsince=NULL) {
1572 global $DB;
1574 $params = array('type'=>$type, 'expiry'=>time());
1575 $sqlwhere = "flagtype = :type AND expiry >= :expiry";
1576 if ($changedsince !== NULL) {
1577 $params['changedsince'] = $changedsince;
1578 $sqlwhere .= " AND timemodified > :changedsince";
1580 $cf = array();
1582 if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) {
1583 foreach ($flags as $flag) {
1584 $cf[$flag->name] = $flag->value;
1587 return $cf;
1591 * Get volatile flags
1593 * @param string $type
1594 * @param string $name
1595 * @param int $changedsince default null
1596 * @return records array
1598 function get_cache_flag($type, $name, $changedsince=NULL) {
1599 global $DB;
1601 $params = array('type'=>$type, 'name'=>$name, 'expiry'=>time());
1603 $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry";
1604 if ($changedsince !== NULL) {
1605 $params['changedsince'] = $changedsince;
1606 $sqlwhere .= " AND timemodified > :changedsince";
1609 return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params);
1613 * Set a volatile flag
1615 * @param string $type the "type" namespace for the key
1616 * @param string $name the key to set
1617 * @param string $value the value to set (without magic quotes) - NULL will remove the flag
1618 * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs
1619 * @return bool Always returns true
1621 function set_cache_flag($type, $name, $value, $expiry=NULL) {
1622 global $DB;
1624 $timemodified = time();
1625 if ($expiry===NULL || $expiry < $timemodified) {
1626 $expiry = $timemodified + 24 * 60 * 60;
1627 } else {
1628 $expiry = (int)$expiry;
1631 if ($value === NULL) {
1632 unset_cache_flag($type,$name);
1633 return true;
1636 if ($f = $DB->get_record('cache_flags', array('name'=>$name, 'flagtype'=>$type), '*', IGNORE_MULTIPLE)) { // this is a potential problem in DEBUG_DEVELOPER
1637 if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) {
1638 return true; //no need to update
1640 $f->value = $value;
1641 $f->expiry = $expiry;
1642 $f->timemodified = $timemodified;
1643 $DB->update_record('cache_flags', $f);
1644 } else {
1645 $f = new stdClass();
1646 $f->flagtype = $type;
1647 $f->name = $name;
1648 $f->value = $value;
1649 $f->expiry = $expiry;
1650 $f->timemodified = $timemodified;
1651 $DB->insert_record('cache_flags', $f);
1653 return true;
1657 * Removes a single volatile flag
1659 * @global object
1660 * @param string $type the "type" namespace for the key
1661 * @param string $name the key to set
1662 * @return bool
1664 function unset_cache_flag($type, $name) {
1665 global $DB;
1666 $DB->delete_records('cache_flags', array('name'=>$name, 'flagtype'=>$type));
1667 return true;
1671 * Garbage-collect volatile flags
1673 * @return bool Always returns true
1675 function gc_cache_flags() {
1676 global $DB;
1677 $DB->delete_records_select('cache_flags', 'expiry < ?', array(time()));
1678 return true;
1681 // USER PREFERENCE API
1684 * Refresh user preference cache. This is used most often for $USER
1685 * object that is stored in session, but it also helps with performance in cron script.
1687 * Preferences for each user are loaded on first use on every page, then again after the timeout expires.
1689 * @package core
1690 * @category preference
1691 * @access public
1692 * @param stdClass $user User object. Preferences are preloaded into 'preference' property
1693 * @param int $cachelifetime Cache life time on the current page (in seconds)
1694 * @throws coding_exception
1695 * @return null
1697 function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120) {
1698 global $DB;
1699 static $loadedusers = array(); // Static cache, we need to check on each page load, not only every 2 minutes.
1701 if (!isset($user->id)) {
1702 throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field');
1705 if (empty($user->id) or isguestuser($user->id)) {
1706 // No permanent storage for not-logged-in users and guest
1707 if (!isset($user->preference)) {
1708 $user->preference = array();
1710 return;
1713 $timenow = time();
1715 if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) {
1716 // Already loaded at least once on this page. Are we up to date?
1717 if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) {
1718 // no need to reload - we are on the same page and we loaded prefs just a moment ago
1719 return;
1721 } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) {
1722 // no change since the lastcheck on this page
1723 $user->preference['_lastloaded'] = $timenow;
1724 return;
1728 // OK, so we have to reload all preferences
1729 $loadedusers[$user->id] = true;
1730 $user->preference = $DB->get_records_menu('user_preferences', array('userid'=>$user->id), '', 'name,value'); // All values
1731 $user->preference['_lastloaded'] = $timenow;
1735 * Called from set/unset_user_preferences, so that the prefs can
1736 * be correctly reloaded in different sessions.
1738 * NOTE: internal function, do not call from other code.
1740 * @package core
1741 * @access private
1742 * @param integer $userid the user whose prefs were changed.
1744 function mark_user_preferences_changed($userid) {
1745 global $CFG;
1747 if (empty($userid) or isguestuser($userid)) {
1748 // no cache flags for guest and not-logged-in users
1749 return;
1752 set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout);
1756 * Sets a preference for the specified user.
1758 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1760 * @package core
1761 * @category preference
1762 * @access public
1763 * @param string $name The key to set as preference for the specified user
1764 * @param string $value The value to set for the $name key in the specified user's
1765 * record, null means delete current value.
1766 * @param stdClass|int|null $user A moodle user object or id, null means current user
1767 * @throws coding_exception
1768 * @return bool Always true or exception
1770 function set_user_preference($name, $value, $user = null) {
1771 global $USER, $DB;
1773 if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1774 throw new coding_exception('Invalid preference name in set_user_preference() call');
1777 if (is_null($value)) {
1778 // null means delete current
1779 return unset_user_preference($name, $user);
1780 } else if (is_object($value)) {
1781 throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed');
1782 } else if (is_array($value)) {
1783 throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed');
1785 $value = (string)$value;
1786 if (textlib::strlen($value) > 1333) { //value column maximum length is 1333 characters
1787 throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column');
1790 if (is_null($user)) {
1791 $user = $USER;
1792 } else if (isset($user->id)) {
1793 // $user is valid object
1794 } else if (is_numeric($user)) {
1795 $user = (object)array('id'=>(int)$user);
1796 } else {
1797 throw new coding_exception('Invalid $user parameter in set_user_preference() call');
1800 check_user_preferences_loaded($user);
1802 if (empty($user->id) or isguestuser($user->id)) {
1803 // no permanent storage for not-logged-in users and guest
1804 $user->preference[$name] = $value;
1805 return true;
1808 if ($preference = $DB->get_record('user_preferences', array('userid'=>$user->id, 'name'=>$name))) {
1809 if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) {
1810 // preference already set to this value
1811 return true;
1813 $DB->set_field('user_preferences', 'value', $value, array('id'=>$preference->id));
1815 } else {
1816 $preference = new stdClass();
1817 $preference->userid = $user->id;
1818 $preference->name = $name;
1819 $preference->value = $value;
1820 $DB->insert_record('user_preferences', $preference);
1823 // update value in cache
1824 $user->preference[$name] = $value;
1826 // set reload flag for other sessions
1827 mark_user_preferences_changed($user->id);
1829 return true;
1833 * Sets a whole array of preferences for the current user
1835 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1837 * @package core
1838 * @category preference
1839 * @access public
1840 * @param array $prefarray An array of key/value pairs to be set
1841 * @param stdClass|int|null $user A moodle user object or id, null means current user
1842 * @return bool Always true or exception
1844 function set_user_preferences(array $prefarray, $user = null) {
1845 foreach ($prefarray as $name => $value) {
1846 set_user_preference($name, $value, $user);
1848 return true;
1852 * Unsets a preference completely by deleting it from the database
1854 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1856 * @package core
1857 * @category preference
1858 * @access public
1859 * @param string $name The key to unset as preference for the specified user
1860 * @param stdClass|int|null $user A moodle user object or id, null means current user
1861 * @throws coding_exception
1862 * @return bool Always true or exception
1864 function unset_user_preference($name, $user = null) {
1865 global $USER, $DB;
1867 if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1868 throw new coding_exception('Invalid preference name in unset_user_preference() call');
1871 if (is_null($user)) {
1872 $user = $USER;
1873 } else if (isset($user->id)) {
1874 // $user is valid object
1875 } else if (is_numeric($user)) {
1876 $user = (object)array('id'=>(int)$user);
1877 } else {
1878 throw new coding_exception('Invalid $user parameter in unset_user_preference() call');
1881 check_user_preferences_loaded($user);
1883 if (empty($user->id) or isguestuser($user->id)) {
1884 // no permanent storage for not-logged-in user and guest
1885 unset($user->preference[$name]);
1886 return true;
1889 // delete from DB
1890 $DB->delete_records('user_preferences', array('userid'=>$user->id, 'name'=>$name));
1892 // delete the preference from cache
1893 unset($user->preference[$name]);
1895 // set reload flag for other sessions
1896 mark_user_preferences_changed($user->id);
1898 return true;
1902 * Used to fetch user preference(s)
1904 * If no arguments are supplied this function will return
1905 * all of the current user preferences as an array.
1907 * If a name is specified then this function
1908 * attempts to return that particular preference value. If
1909 * none is found, then the optional value $default is returned,
1910 * otherwise NULL.
1912 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1914 * @package core
1915 * @category preference
1916 * @access public
1917 * @param string $name Name of the key to use in finding a preference value
1918 * @param mixed|null $default Value to be returned if the $name key is not set in the user preferences
1919 * @param stdClass|int|null $user A moodle user object or id, null means current user
1920 * @throws coding_exception
1921 * @return string|mixed|null A string containing the value of a single preference. An
1922 * array with all of the preferences or null
1924 function get_user_preferences($name = null, $default = null, $user = null) {
1925 global $USER;
1927 if (is_null($name)) {
1928 // all prefs
1929 } else if (is_numeric($name) or $name === '_lastloaded') {
1930 throw new coding_exception('Invalid preference name in get_user_preferences() call');
1933 if (is_null($user)) {
1934 $user = $USER;
1935 } else if (isset($user->id)) {
1936 // $user is valid object
1937 } else if (is_numeric($user)) {
1938 $user = (object)array('id'=>(int)$user);
1939 } else {
1940 throw new coding_exception('Invalid $user parameter in get_user_preferences() call');
1943 check_user_preferences_loaded($user);
1945 if (empty($name)) {
1946 return $user->preference; // All values
1947 } else if (isset($user->preference[$name])) {
1948 return $user->preference[$name]; // The single string value
1949 } else {
1950 return $default; // Default value (null if not specified)
1954 /// FUNCTIONS FOR HANDLING TIME ////////////////////////////////////////////
1957 * Given date parts in user time produce a GMT timestamp.
1959 * @package core
1960 * @category time
1961 * @param int $year The year part to create timestamp of
1962 * @param int $month The month part to create timestamp of
1963 * @param int $day The day part to create timestamp of
1964 * @param int $hour The hour part to create timestamp of
1965 * @param int $minute The minute part to create timestamp of
1966 * @param int $second The second part to create timestamp of
1967 * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset.
1968 * if 99 then default user's timezone is used {@link http://docs.moodle.org/dev/Time_API#Timezone}
1969 * @param bool $applydst Toggle Daylight Saving Time, default true, will be
1970 * applied only if timezone is 99 or string.
1971 * @return int GMT timestamp
1973 function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) {
1975 //save input timezone, required for dst offset check.
1976 $passedtimezone = $timezone;
1978 $timezone = get_user_timezone_offset($timezone);
1980 if (abs($timezone) > 13) { //server time
1981 $time = mktime((int)$hour, (int)$minute, (int)$second, (int)$month, (int)$day, (int)$year);
1982 } else {
1983 $time = gmmktime((int)$hour, (int)$minute, (int)$second, (int)$month, (int)$day, (int)$year);
1984 $time = usertime($time, $timezone);
1986 //Apply dst for string timezones or if 99 then try dst offset with user's default timezone
1987 if ($applydst && ((99 == $passedtimezone) || !is_numeric($passedtimezone))) {
1988 $time -= dst_offset_on($time, $passedtimezone);
1992 return $time;
1997 * Format a date/time (seconds) as weeks, days, hours etc as needed
1999 * Given an amount of time in seconds, returns string
2000 * formatted nicely as weeks, days, hours etc as needed
2002 * @package core
2003 * @category time
2004 * @uses MINSECS
2005 * @uses HOURSECS
2006 * @uses DAYSECS
2007 * @uses YEARSECS
2008 * @param int $totalsecs Time in seconds
2009 * @param object $str Should be a time object
2010 * @return string A nicely formatted date/time string
2012 function format_time($totalsecs, $str=NULL) {
2014 $totalsecs = abs($totalsecs);
2016 if (!$str) { // Create the str structure the slow way
2017 $str = new stdClass();
2018 $str->day = get_string('day');
2019 $str->days = get_string('days');
2020 $str->hour = get_string('hour');
2021 $str->hours = get_string('hours');
2022 $str->min = get_string('min');
2023 $str->mins = get_string('mins');
2024 $str->sec = get_string('sec');
2025 $str->secs = get_string('secs');
2026 $str->year = get_string('year');
2027 $str->years = get_string('years');
2031 $years = floor($totalsecs/YEARSECS);
2032 $remainder = $totalsecs - ($years*YEARSECS);
2033 $days = floor($remainder/DAYSECS);
2034 $remainder = $totalsecs - ($days*DAYSECS);
2035 $hours = floor($remainder/HOURSECS);
2036 $remainder = $remainder - ($hours*HOURSECS);
2037 $mins = floor($remainder/MINSECS);
2038 $secs = $remainder - ($mins*MINSECS);
2040 $ss = ($secs == 1) ? $str->sec : $str->secs;
2041 $sm = ($mins == 1) ? $str->min : $str->mins;
2042 $sh = ($hours == 1) ? $str->hour : $str->hours;
2043 $sd = ($days == 1) ? $str->day : $str->days;
2044 $sy = ($years == 1) ? $str->year : $str->years;
2046 $oyears = '';
2047 $odays = '';
2048 $ohours = '';
2049 $omins = '';
2050 $osecs = '';
2052 if ($years) $oyears = $years .' '. $sy;
2053 if ($days) $odays = $days .' '. $sd;
2054 if ($hours) $ohours = $hours .' '. $sh;
2055 if ($mins) $omins = $mins .' '. $sm;
2056 if ($secs) $osecs = $secs .' '. $ss;
2058 if ($years) return trim($oyears .' '. $odays);
2059 if ($days) return trim($odays .' '. $ohours);
2060 if ($hours) return trim($ohours .' '. $omins);
2061 if ($mins) return trim($omins .' '. $osecs);
2062 if ($secs) return $osecs;
2063 return get_string('now');
2067 * Returns a formatted string that represents a date in user time
2069 * Returns a formatted string that represents a date in user time
2070 * <b>WARNING: note that the format is for strftime(), not date().</b>
2071 * Because of a bug in most Windows time libraries, we can't use
2072 * the nicer %e, so we have to use %d which has leading zeroes.
2073 * A lot of the fuss in the function is just getting rid of these leading
2074 * zeroes as efficiently as possible.
2076 * If parameter fixday = true (default), then take off leading
2077 * zero from %d, else maintain it.
2079 * @package core
2080 * @category time
2081 * @param int $date the timestamp in UTC, as obtained from the database.
2082 * @param string $format strftime format. You should probably get this using
2083 * get_string('strftime...', 'langconfig');
2084 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
2085 * not 99 then daylight saving will not be added.
2086 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2087 * @param bool $fixday If true (default) then the leading zero from %d is removed.
2088 * If false then the leading zero is maintained.
2089 * @param bool $fixhour If true (default) then the leading zero from %I is removed.
2090 * @return string the formatted date/time.
2092 function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
2094 global $CFG;
2096 if (empty($format)) {
2097 $format = get_string('strftimedaydatetime', 'langconfig');
2100 if (!empty($CFG->nofixday)) { // Config.php can force %d not to be fixed.
2101 $fixday = false;
2102 } else if ($fixday) {
2103 $formatnoday = str_replace('%d', 'DD', $format);
2104 $fixday = ($formatnoday != $format);
2105 $format = $formatnoday;
2108 // Note: This logic about fixing 12-hour time to remove unnecessary leading
2109 // zero is required because on Windows, PHP strftime function does not
2110 // support the correct 'hour without leading zero' parameter (%l).
2111 if (!empty($CFG->nofixhour)) {
2112 // Config.php can force %I not to be fixed.
2113 $fixhour = false;
2114 } else if ($fixhour) {
2115 $formatnohour = str_replace('%I', 'HH', $format);
2116 $fixhour = ($formatnohour != $format);
2117 $format = $formatnohour;
2120 //add daylight saving offset for string timezones only, as we can't get dst for
2121 //float values. if timezone is 99 (user default timezone), then try update dst.
2122 if ((99 == $timezone) || !is_numeric($timezone)) {
2123 $date += dst_offset_on($date, $timezone);
2126 $timezone = get_user_timezone_offset($timezone);
2128 // If we are running under Windows convert to windows encoding and then back to UTF-8
2129 // (because it's impossible to specify UTF-8 to fetch locale info in Win32)
2131 if (abs($timezone) > 13) { /// Server time
2132 $datestring = date_format_string($date, $format, $timezone);
2133 if ($fixday) {
2134 $daystring = ltrim(str_replace(array(' 0', ' '), '', strftime(' %d', $date)));
2135 $datestring = str_replace('DD', $daystring, $datestring);
2137 if ($fixhour) {
2138 $hourstring = ltrim(str_replace(array(' 0', ' '), '', strftime(' %I', $date)));
2139 $datestring = str_replace('HH', $hourstring, $datestring);
2142 } else {
2143 $date += (int)($timezone * 3600);
2144 $datestring = date_format_string($date, $format, $timezone);
2145 if ($fixday) {
2146 $daystring = ltrim(str_replace(array(' 0', ' '), '', gmstrftime(' %d', $date)));
2147 $datestring = str_replace('DD', $daystring, $datestring);
2149 if ($fixhour) {
2150 $hourstring = ltrim(str_replace(array(' 0', ' '), '', gmstrftime(' %I', $date)));
2151 $datestring = str_replace('HH', $hourstring, $datestring);
2155 return $datestring;
2159 * Returns a formatted date ensuring it is UTF-8.
2161 * If we are running under Windows convert to Windows encoding and then back to UTF-8
2162 * (because it's impossible to specify UTF-8 to fetch locale info in Win32).
2164 * This function does not do any calculation regarding the user preferences and should
2165 * therefore receive the final date timestamp, format and timezone. Timezone being only used
2166 * to differenciate the use of server time or not (strftime() against gmstrftime()).
2168 * @param int $date the timestamp.
2169 * @param string $format strftime format.
2170 * @param int|float $timezone the numerical timezone, typically returned by {@link get_user_timezone_offset()}.
2171 * @return string the formatted date/time.
2172 * @since 2.3.3
2174 function date_format_string($date, $format, $tz = 99) {
2175 global $CFG;
2176 if (abs($tz) > 13) {
2177 if ($CFG->ostype == 'WINDOWS' and $localewincharset = get_string('localewincharset', 'langconfig')) {
2178 $format = textlib::convert($format, 'utf-8', $localewincharset);
2179 $datestring = strftime($format, $date);
2180 $datestring = textlib::convert($datestring, $localewincharset, 'utf-8');
2181 } else {
2182 $datestring = strftime($format, $date);
2184 } else {
2185 if ($CFG->ostype == 'WINDOWS' and $localewincharset = get_string('localewincharset', 'langconfig')) {
2186 $format = textlib::convert($format, 'utf-8', $localewincharset);
2187 $datestring = gmstrftime($format, $date);
2188 $datestring = textlib::convert($datestring, $localewincharset, 'utf-8');
2189 } else {
2190 $datestring = gmstrftime($format, $date);
2193 return $datestring;
2197 * Given a $time timestamp in GMT (seconds since epoch),
2198 * returns an array that represents the date in user time
2200 * @package core
2201 * @category time
2202 * @uses HOURSECS
2203 * @param int $time Timestamp in GMT
2204 * @param float|int|string $timezone offset's time with timezone, if float and not 99, then no
2205 * dst offset is applyed {@link http://docs.moodle.org/dev/Time_API#Timezone}
2206 * @return array An array that represents the date in user time
2208 function usergetdate($time, $timezone=99) {
2210 //save input timezone, required for dst offset check.
2211 $passedtimezone = $timezone;
2213 $timezone = get_user_timezone_offset($timezone);
2215 if (abs($timezone) > 13) { // Server time
2216 return getdate($time);
2219 //add daylight saving offset for string timezones only, as we can't get dst for
2220 //float values. if timezone is 99 (user default timezone), then try update dst.
2221 if ($passedtimezone == 99 || !is_numeric($passedtimezone)) {
2222 $time += dst_offset_on($time, $passedtimezone);
2225 $time += intval((float)$timezone * HOURSECS);
2227 $datestring = gmstrftime('%B_%A_%j_%Y_%m_%w_%d_%H_%M_%S', $time);
2229 //be careful to ensure the returned array matches that produced by getdate() above
2230 list(
2231 $getdate['month'],
2232 $getdate['weekday'],
2233 $getdate['yday'],
2234 $getdate['year'],
2235 $getdate['mon'],
2236 $getdate['wday'],
2237 $getdate['mday'],
2238 $getdate['hours'],
2239 $getdate['minutes'],
2240 $getdate['seconds']
2241 ) = explode('_', $datestring);
2243 // set correct datatype to match with getdate()
2244 $getdate['seconds'] = (int)$getdate['seconds'];
2245 $getdate['yday'] = (int)$getdate['yday'] - 1; // gettime returns 0 through 365
2246 $getdate['year'] = (int)$getdate['year'];
2247 $getdate['mon'] = (int)$getdate['mon'];
2248 $getdate['wday'] = (int)$getdate['wday'];
2249 $getdate['mday'] = (int)$getdate['mday'];
2250 $getdate['hours'] = (int)$getdate['hours'];
2251 $getdate['minutes'] = (int)$getdate['minutes'];
2252 return $getdate;
2256 * Given a GMT timestamp (seconds since epoch), offsets it by
2257 * the timezone. eg 3pm in India is 3pm GMT - 7 * 3600 seconds
2259 * @package core
2260 * @category time
2261 * @uses HOURSECS
2262 * @param int $date Timestamp in GMT
2263 * @param float|int|string $timezone timezone to calculate GMT time offset before
2264 * calculating user time, 99 is default user timezone
2265 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2266 * @return int
2268 function usertime($date, $timezone=99) {
2270 $timezone = get_user_timezone_offset($timezone);
2272 if (abs($timezone) > 13) {
2273 return $date;
2275 return $date - (int)($timezone * HOURSECS);
2279 * Given a time, return the GMT timestamp of the most recent midnight
2280 * for the current user.
2282 * @package core
2283 * @category time
2284 * @param int $date Timestamp in GMT
2285 * @param float|int|string $timezone timezone to calculate GMT time offset before
2286 * calculating user midnight time, 99 is default user timezone
2287 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2288 * @return int Returns a GMT timestamp
2290 function usergetmidnight($date, $timezone=99) {
2292 $userdate = usergetdate($date, $timezone);
2294 // Time of midnight of this user's day, in GMT
2295 return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone);
2300 * Returns a string that prints the user's timezone
2302 * @package core
2303 * @category time
2304 * @param float|int|string $timezone timezone to calculate GMT time offset before
2305 * calculating user timezone, 99 is default user timezone
2306 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2307 * @return string
2309 function usertimezone($timezone=99) {
2311 $tz = get_user_timezone($timezone);
2313 if (!is_float($tz)) {
2314 return $tz;
2317 if(abs($tz) > 13) { // Server time
2318 return get_string('serverlocaltime');
2321 if($tz == intval($tz)) {
2322 // Don't show .0 for whole hours
2323 $tz = intval($tz);
2326 if($tz == 0) {
2327 return 'UTC';
2329 else if($tz > 0) {
2330 return 'UTC+'.$tz;
2332 else {
2333 return 'UTC'.$tz;
2339 * Returns a float which represents the user's timezone difference from GMT in hours
2340 * Checks various settings and picks the most dominant of those which have a value
2342 * @package core
2343 * @category time
2344 * @param float|int|string $tz timezone to calculate GMT time offset for user,
2345 * 99 is default user timezone
2346 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2347 * @return float
2349 function get_user_timezone_offset($tz = 99) {
2351 global $USER, $CFG;
2353 $tz = get_user_timezone($tz);
2355 if (is_float($tz)) {
2356 return $tz;
2357 } else {
2358 $tzrecord = get_timezone_record($tz);
2359 if (empty($tzrecord)) {
2360 return 99.0;
2362 return (float)$tzrecord->gmtoff / HOURMINS;
2367 * Returns an int which represents the systems's timezone difference from GMT in seconds
2369 * @package core
2370 * @category time
2371 * @param float|int|string $tz timezone for which offset is required.
2372 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2373 * @return int|bool if found, false is timezone 99 or error
2375 function get_timezone_offset($tz) {
2376 global $CFG;
2378 if ($tz == 99) {
2379 return false;
2382 if (is_numeric($tz)) {
2383 return intval($tz * 60*60);
2386 if (!$tzrecord = get_timezone_record($tz)) {
2387 return false;
2389 return intval($tzrecord->gmtoff * 60);
2393 * Returns a float or a string which denotes the user's timezone
2394 * 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)
2395 * means that for this timezone there are also DST rules to be taken into account
2396 * Checks various settings and picks the most dominant of those which have a value
2398 * @package core
2399 * @category time
2400 * @param float|int|string $tz timezone to calculate GMT time offset before
2401 * calculating user timezone, 99 is default user timezone
2402 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2403 * @return float|string
2405 function get_user_timezone($tz = 99) {
2406 global $USER, $CFG;
2408 $timezones = array(
2409 $tz,
2410 isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99,
2411 isset($USER->timezone) ? $USER->timezone : 99,
2412 isset($CFG->timezone) ? $CFG->timezone : 99,
2415 $tz = 99;
2417 // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array
2418 while(((empty($tz) && !is_numeric($tz)) || $tz == 99) && $next = each($timezones)) {
2419 $tz = $next['value'];
2421 return is_numeric($tz) ? (float) $tz : $tz;
2425 * Returns cached timezone record for given $timezonename
2427 * @package core
2428 * @param string $timezonename name of the timezone
2429 * @return stdClass|bool timezonerecord or false
2431 function get_timezone_record($timezonename) {
2432 global $CFG, $DB;
2433 static $cache = NULL;
2435 if ($cache === NULL) {
2436 $cache = array();
2439 if (isset($cache[$timezonename])) {
2440 return $cache[$timezonename];
2443 return $cache[$timezonename] = $DB->get_record_sql('SELECT * FROM {timezone}
2444 WHERE name = ? ORDER BY year DESC', array($timezonename), IGNORE_MULTIPLE);
2448 * Build and store the users Daylight Saving Time (DST) table
2450 * @package core
2451 * @param int $from_year Start year for the table, defaults to 1971
2452 * @param int $to_year End year for the table, defaults to 2035
2453 * @param int|float|string $strtimezone, timezone to check if dst should be applyed.
2454 * @return bool
2456 function calculate_user_dst_table($from_year = NULL, $to_year = NULL, $strtimezone = NULL) {
2457 global $CFG, $SESSION, $DB;
2459 $usertz = get_user_timezone($strtimezone);
2461 if (is_float($usertz)) {
2462 // Trivial timezone, no DST
2463 return false;
2466 if (!empty($SESSION->dst_offsettz) && $SESSION->dst_offsettz != $usertz) {
2467 // We have precalculated values, but the user's effective TZ has changed in the meantime, so reset
2468 unset($SESSION->dst_offsets);
2469 unset($SESSION->dst_range);
2472 if (!empty($SESSION->dst_offsets) && empty($from_year) && empty($to_year)) {
2473 // Repeat calls which do not request specific year ranges stop here, we have already calculated the table
2474 // This will be the return path most of the time, pretty light computationally
2475 return true;
2478 // Reaching here means we either need to extend our table or create it from scratch
2480 // Remember which TZ we calculated these changes for
2481 $SESSION->dst_offsettz = $usertz;
2483 if(empty($SESSION->dst_offsets)) {
2484 // If we 're creating from scratch, put the two guard elements in there
2485 $SESSION->dst_offsets = array(1 => NULL, 0 => NULL);
2487 if(empty($SESSION->dst_range)) {
2488 // If creating from scratch
2489 $from = max((empty($from_year) ? intval(date('Y')) - 3 : $from_year), 1971);
2490 $to = min((empty($to_year) ? intval(date('Y')) + 3 : $to_year), 2035);
2492 // Fill in the array with the extra years we need to process
2493 $yearstoprocess = array();
2494 for($i = $from; $i <= $to; ++$i) {
2495 $yearstoprocess[] = $i;
2498 // Take note of which years we have processed for future calls
2499 $SESSION->dst_range = array($from, $to);
2501 else {
2502 // If needing to extend the table, do the same
2503 $yearstoprocess = array();
2505 $from = max((empty($from_year) ? $SESSION->dst_range[0] : $from_year), 1971);
2506 $to = min((empty($to_year) ? $SESSION->dst_range[1] : $to_year), 2035);
2508 if($from < $SESSION->dst_range[0]) {
2509 // Take note of which years we need to process and then note that we have processed them for future calls
2510 for($i = $from; $i < $SESSION->dst_range[0]; ++$i) {
2511 $yearstoprocess[] = $i;
2513 $SESSION->dst_range[0] = $from;
2515 if($to > $SESSION->dst_range[1]) {
2516 // Take note of which years we need to process and then note that we have processed them for future calls
2517 for($i = $SESSION->dst_range[1] + 1; $i <= $to; ++$i) {
2518 $yearstoprocess[] = $i;
2520 $SESSION->dst_range[1] = $to;
2524 if(empty($yearstoprocess)) {
2525 // This means that there was a call requesting a SMALLER range than we have already calculated
2526 return true;
2529 // From now on, we know that the array has at least the two guard elements, and $yearstoprocess has the years we need
2530 // Also, the array is sorted in descending timestamp order!
2532 // Get DB data
2534 static $presets_cache = array();
2535 if (!isset($presets_cache[$usertz])) {
2536 $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');
2538 if(empty($presets_cache[$usertz])) {
2539 return false;
2542 // Remove ending guard (first element of the array)
2543 reset($SESSION->dst_offsets);
2544 unset($SESSION->dst_offsets[key($SESSION->dst_offsets)]);
2546 // Add all required change timestamps
2547 foreach($yearstoprocess as $y) {
2548 // Find the record which is in effect for the year $y
2549 foreach($presets_cache[$usertz] as $year => $preset) {
2550 if($year <= $y) {
2551 break;
2555 $changes = dst_changes_for_year($y, $preset);
2557 if($changes === NULL) {
2558 continue;
2560 if($changes['dst'] != 0) {
2561 $SESSION->dst_offsets[$changes['dst']] = $preset->dstoff * MINSECS;
2563 if($changes['std'] != 0) {
2564 $SESSION->dst_offsets[$changes['std']] = 0;
2568 // Put in a guard element at the top
2569 $maxtimestamp = max(array_keys($SESSION->dst_offsets));
2570 $SESSION->dst_offsets[($maxtimestamp + DAYSECS)] = NULL; // DAYSECS is arbitrary, any "small" number will do
2572 // Sort again
2573 krsort($SESSION->dst_offsets);
2575 return true;
2579 * Calculates the required DST change and returns a Timestamp Array
2581 * @package core
2582 * @category time
2583 * @uses HOURSECS
2584 * @uses MINSECS
2585 * @param int|string $year Int or String Year to focus on
2586 * @param object $timezone Instatiated Timezone object
2587 * @return array|null Array dst=>xx, 0=>xx, std=>yy, 1=>yy or NULL
2589 function dst_changes_for_year($year, $timezone) {
2591 if($timezone->dst_startday == 0 && $timezone->dst_weekday == 0 && $timezone->std_startday == 0 && $timezone->std_weekday == 0) {
2592 return NULL;
2595 $monthdaydst = find_day_in_month($timezone->dst_startday, $timezone->dst_weekday, $timezone->dst_month, $year);
2596 $monthdaystd = find_day_in_month($timezone->std_startday, $timezone->std_weekday, $timezone->std_month, $year);
2598 list($dst_hour, $dst_min) = explode(':', $timezone->dst_time);
2599 list($std_hour, $std_min) = explode(':', $timezone->std_time);
2601 $timedst = make_timestamp($year, $timezone->dst_month, $monthdaydst, 0, 0, 0, 99, false);
2602 $timestd = make_timestamp($year, $timezone->std_month, $monthdaystd, 0, 0, 0, 99, false);
2604 // Instead of putting hour and minute in make_timestamp(), we add them afterwards.
2605 // This has the advantage of being able to have negative values for hour, i.e. for timezones
2606 // where GMT time would be in the PREVIOUS day than the local one on which DST changes.
2608 $timedst += $dst_hour * HOURSECS + $dst_min * MINSECS;
2609 $timestd += $std_hour * HOURSECS + $std_min * MINSECS;
2611 return array('dst' => $timedst, 0 => $timedst, 'std' => $timestd, 1 => $timestd);
2615 * Calculates the Daylight Saving Offset for a given date/time (timestamp)
2616 * - Note: Daylight saving only works for string timezones and not for float.
2618 * @package core
2619 * @category time
2620 * @param int $time must NOT be compensated at all, it has to be a pure timestamp
2621 * @param int|float|string $strtimezone timezone for which offset is expected, if 99 or null
2622 * then user's default timezone is used. {@link http://docs.moodle.org/dev/Time_API#Timezone}
2623 * @return int
2625 function dst_offset_on($time, $strtimezone = NULL) {
2626 global $SESSION;
2628 if(!calculate_user_dst_table(NULL, NULL, $strtimezone) || empty($SESSION->dst_offsets)) {
2629 return 0;
2632 reset($SESSION->dst_offsets);
2633 while(list($from, $offset) = each($SESSION->dst_offsets)) {
2634 if($from <= $time) {
2635 break;
2639 // This is the normal return path
2640 if($offset !== NULL) {
2641 return $offset;
2644 // Reaching this point means we haven't calculated far enough, do it now:
2645 // Calculate extra DST changes if needed and recurse. The recursion always
2646 // moves toward the stopping condition, so will always end.
2648 if($from == 0) {
2649 // We need a year smaller than $SESSION->dst_range[0]
2650 if($SESSION->dst_range[0] == 1971) {
2651 return 0;
2653 calculate_user_dst_table($SESSION->dst_range[0] - 5, NULL, $strtimezone);
2654 return dst_offset_on($time, $strtimezone);
2656 else {
2657 // We need a year larger than $SESSION->dst_range[1]
2658 if($SESSION->dst_range[1] == 2035) {
2659 return 0;
2661 calculate_user_dst_table(NULL, $SESSION->dst_range[1] + 5, $strtimezone);
2662 return dst_offset_on($time, $strtimezone);
2667 * Calculates when the day appears in specific month
2669 * @package core
2670 * @category time
2671 * @param int $startday starting day of the month
2672 * @param int $weekday The day when week starts (normally taken from user preferences)
2673 * @param int $month The month whose day is sought
2674 * @param int $year The year of the month whose day is sought
2675 * @return int
2677 function find_day_in_month($startday, $weekday, $month, $year) {
2679 $daysinmonth = days_in_month($month, $year);
2681 if($weekday == -1) {
2682 // Don't care about weekday, so return:
2683 // abs($startday) if $startday != -1
2684 // $daysinmonth otherwise
2685 return ($startday == -1) ? $daysinmonth : abs($startday);
2688 // From now on we 're looking for a specific weekday
2690 // Give "end of month" its actual value, since we know it
2691 if($startday == -1) {
2692 $startday = -1 * $daysinmonth;
2695 // Starting from day $startday, the sign is the direction
2697 if($startday < 1) {
2699 $startday = abs($startday);
2700 $lastmonthweekday = strftime('%w', mktime(12, 0, 0, $month, $daysinmonth, $year));
2702 // This is the last such weekday of the month
2703 $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday;
2704 if($lastinmonth > $daysinmonth) {
2705 $lastinmonth -= 7;
2708 // Find the first such weekday <= $startday
2709 while($lastinmonth > $startday) {
2710 $lastinmonth -= 7;
2713 return $lastinmonth;
2716 else {
2718 $indexweekday = strftime('%w', mktime(12, 0, 0, $month, $startday, $year));
2720 $diff = $weekday - $indexweekday;
2721 if($diff < 0) {
2722 $diff += 7;
2725 // This is the first such weekday of the month equal to or after $startday
2726 $firstfromindex = $startday + $diff;
2728 return $firstfromindex;
2734 * Calculate the number of days in a given month
2736 * @package core
2737 * @category time
2738 * @param int $month The month whose day count is sought
2739 * @param int $year The year of the month whose day count is sought
2740 * @return int
2742 function days_in_month($month, $year) {
2743 return intval(date('t', mktime(12, 0, 0, $month, 1, $year)));
2747 * Calculate the position in the week of a specific calendar day
2749 * @package core
2750 * @category time
2751 * @param int $day The day of the date whose position in the week is sought
2752 * @param int $month The month of the date whose position in the week is sought
2753 * @param int $year The year of the date whose position in the week is sought
2754 * @return int
2756 function dayofweek($day, $month, $year) {
2757 // I wonder if this is any different from
2758 // strftime('%w', mktime(12, 0, 0, $month, $daysinmonth, $year, 0));
2759 return intval(date('w', mktime(12, 0, 0, $month, $day, $year)));
2762 /// USER AUTHENTICATION AND LOGIN ////////////////////////////////////////
2765 * Returns full login url.
2767 * @return string login url
2769 function get_login_url() {
2770 global $CFG;
2772 $url = "$CFG->wwwroot/login/index.php";
2774 if (!empty($CFG->loginhttps)) {
2775 $url = str_replace('http:', 'https:', $url);
2778 return $url;
2782 * This function checks that the current user is logged in and has the
2783 * required privileges
2785 * This function checks that the current user is logged in, and optionally
2786 * whether they are allowed to be in a particular course and view a particular
2787 * course module.
2788 * If they are not logged in, then it redirects them to the site login unless
2789 * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which
2790 * case they are automatically logged in as guests.
2791 * If $courseid is given and the user is not enrolled in that course then the
2792 * user is redirected to the course enrolment page.
2793 * If $cm is given and the course module is hidden and the user is not a teacher
2794 * in the course then the user is redirected to the course home page.
2796 * When $cm parameter specified, this function sets page layout to 'module'.
2797 * You need to change it manually later if some other layout needed.
2799 * @package core_access
2800 * @category access
2802 * @param mixed $courseorid id of the course or course object
2803 * @param bool $autologinguest default true
2804 * @param object $cm course module object
2805 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2806 * true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2807 * in order to keep redirects working properly. MDL-14495
2808 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2809 * @return mixed Void, exit, and die depending on path
2811 function require_login($courseorid = NULL, $autologinguest = true, $cm = NULL, $setwantsurltome = true, $preventredirect = false) {
2812 global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT;
2814 // Must not redirect when byteserving already started.
2815 if (!empty($_SERVER['HTTP_RANGE'])) {
2816 $preventredirect = true;
2819 // setup global $COURSE, themes, language and locale
2820 if (!empty($courseorid)) {
2821 if (is_object($courseorid)) {
2822 $course = $courseorid;
2823 } else if ($courseorid == SITEID) {
2824 $course = clone($SITE);
2825 } else {
2826 $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST);
2828 if ($cm) {
2829 if ($cm->course != $course->id) {
2830 throw new coding_exception('course and cm parameters in require_login() call do not match!!');
2832 // make sure we have a $cm from get_fast_modinfo as this contains activity access details
2833 if (!($cm instanceof cm_info)) {
2834 // note: nearly all pages call get_fast_modinfo anyway and it does not make any
2835 // db queries so this is not really a performance concern, however it is obviously
2836 // better if you use get_fast_modinfo to get the cm before calling this.
2837 $modinfo = get_fast_modinfo($course);
2838 $cm = $modinfo->get_cm($cm->id);
2840 $PAGE->set_cm($cm, $course); // set's up global $COURSE
2841 $PAGE->set_pagelayout('incourse');
2842 } else {
2843 $PAGE->set_course($course); // set's up global $COURSE
2845 } else {
2846 // do not touch global $COURSE via $PAGE->set_course(),
2847 // the reasons is we need to be able to call require_login() at any time!!
2848 $course = $SITE;
2849 if ($cm) {
2850 throw new coding_exception('cm parameter in require_login() requires valid course parameter!');
2854 // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false.
2855 // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future
2856 // risk leading the user back to the AJAX request URL.
2857 if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) {
2858 $setwantsurltome = false;
2861 // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour.
2862 if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !$preventredirect && !empty($CFG->dbsessions)) {
2863 if ($setwantsurltome) {
2864 $SESSION->wantsurl = qualified_me();
2866 redirect(get_login_url());
2869 // If the user is not even logged in yet then make sure they are
2870 if (!isloggedin()) {
2871 if ($autologinguest and !empty($CFG->guestloginbutton) and !empty($CFG->autologinguests)) {
2872 if (!$guest = get_complete_user_data('id', $CFG->siteguest)) {
2873 // misconfigured site guest, just redirect to login page
2874 redirect(get_login_url());
2875 exit; // never reached
2877 $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang;
2878 complete_user_login($guest);
2879 $USER->autologinguest = true;
2880 $SESSION->lang = $lang;
2881 } else {
2882 //NOTE: $USER->site check was obsoleted by session test cookie,
2883 // $USER->confirmed test is in login/index.php
2884 if ($preventredirect) {
2885 throw new require_login_exception('You are not logged in');
2888 if ($setwantsurltome) {
2889 $SESSION->wantsurl = qualified_me();
2891 if (!empty($_SERVER['HTTP_REFERER'])) {
2892 $SESSION->fromurl = $_SERVER['HTTP_REFERER'];
2894 redirect(get_login_url());
2895 exit; // never reached
2899 // loginas as redirection if needed
2900 if ($course->id != SITEID and session_is_loggedinas()) {
2901 if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) {
2902 if ($USER->loginascontext->instanceid != $course->id) {
2903 print_error('loginasonecourse', '', $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid);
2908 // check whether the user should be changing password (but only if it is REALLY them)
2909 if (get_user_preferences('auth_forcepasswordchange') && !session_is_loggedinas()) {
2910 $userauth = get_auth_plugin($USER->auth);
2911 if ($userauth->can_change_password() and !$preventredirect) {
2912 if ($setwantsurltome) {
2913 $SESSION->wantsurl = qualified_me();
2915 if ($changeurl = $userauth->change_password_url()) {
2916 //use plugin custom url
2917 redirect($changeurl);
2918 } else {
2919 //use moodle internal method
2920 if (empty($CFG->loginhttps)) {
2921 redirect($CFG->wwwroot .'/login/change_password.php');
2922 } else {
2923 $wwwroot = str_replace('http:','https:', $CFG->wwwroot);
2924 redirect($wwwroot .'/login/change_password.php');
2927 } else {
2928 print_error('nopasswordchangeforced', 'auth');
2932 // Check that the user account is properly set up
2933 if (user_not_fully_set_up($USER)) {
2934 if ($preventredirect) {
2935 throw new require_login_exception('User not fully set-up');
2937 if ($setwantsurltome) {
2938 $SESSION->wantsurl = qualified_me();
2940 redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&amp;course='. SITEID);
2943 // Make sure the USER has a sesskey set up. Used for CSRF protection.
2944 sesskey();
2946 // Do not bother admins with any formalities
2947 if (is_siteadmin()) {
2948 //set accesstime or the user will appear offline which messes up messaging
2949 user_accesstime_log($course->id);
2950 return;
2953 // Check that the user has agreed to a site policy if there is one - do not test in case of admins
2954 if (!$USER->policyagreed and !is_siteadmin()) {
2955 if (!empty($CFG->sitepolicy) and !isguestuser()) {
2956 if ($preventredirect) {
2957 throw new require_login_exception('Policy not agreed');
2959 if ($setwantsurltome) {
2960 $SESSION->wantsurl = qualified_me();
2962 redirect($CFG->wwwroot .'/user/policy.php');
2963 } else if (!empty($CFG->sitepolicyguest) and isguestuser()) {
2964 if ($preventredirect) {
2965 throw new require_login_exception('Policy not agreed');
2967 if ($setwantsurltome) {
2968 $SESSION->wantsurl = qualified_me();
2970 redirect($CFG->wwwroot .'/user/policy.php');
2974 // Fetch the system context, the course context, and prefetch its child contexts
2975 $sysctx = context_system::instance();
2976 $coursecontext = context_course::instance($course->id, MUST_EXIST);
2977 if ($cm) {
2978 $cmcontext = context_module::instance($cm->id, MUST_EXIST);
2979 } else {
2980 $cmcontext = null;
2983 // If the site is currently under maintenance, then print a message
2984 if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:config', $sysctx)) {
2985 if ($preventredirect) {
2986 throw new require_login_exception('Maintenance in progress');
2989 print_maintenance_message();
2992 // make sure the course itself is not hidden
2993 if ($course->id == SITEID) {
2994 // frontpage can not be hidden
2995 } else {
2996 if (is_role_switched($course->id)) {
2997 // when switching roles ignore the hidden flag - user had to be in course to do the switch
2998 } else {
2999 if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
3000 // originally there was also test of parent category visibility,
3001 // BUT is was very slow in complex queries involving "my courses"
3002 // now it is also possible to simply hide all courses user is not enrolled in :-)
3003 if ($preventredirect) {
3004 throw new require_login_exception('Course is hidden');
3006 // We need to override the navigation URL as the course won't have
3007 // been added to the navigation and thus the navigation will mess up
3008 // when trying to find it.
3009 navigation_node::override_active_url(new moodle_url('/'));
3010 notice(get_string('coursehidden'), $CFG->wwwroot .'/');
3015 // is the user enrolled?
3016 if ($course->id == SITEID) {
3017 // everybody is enrolled on the frontpage
3019 } else {
3020 if (session_is_loggedinas()) {
3021 // Make sure the REAL person can access this course first
3022 $realuser = session_get_realuser();
3023 if (!is_enrolled($coursecontext, $realuser->id, '', true) and !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) {
3024 if ($preventredirect) {
3025 throw new require_login_exception('Invalid course login-as access');
3027 echo $OUTPUT->header();
3028 notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/');
3032 $access = false;
3034 if (is_role_switched($course->id)) {
3035 // ok, user had to be inside this course before the switch
3036 $access = true;
3038 } else if (is_viewing($coursecontext, $USER)) {
3039 // ok, no need to mess with enrol
3040 $access = true;
3042 } else {
3043 if (isset($USER->enrol['enrolled'][$course->id])) {
3044 if ($USER->enrol['enrolled'][$course->id] > time()) {
3045 $access = true;
3046 if (isset($USER->enrol['tempguest'][$course->id])) {
3047 unset($USER->enrol['tempguest'][$course->id]);
3048 remove_temp_course_roles($coursecontext);
3050 } else {
3051 //expired
3052 unset($USER->enrol['enrolled'][$course->id]);
3055 if (isset($USER->enrol['tempguest'][$course->id])) {
3056 if ($USER->enrol['tempguest'][$course->id] == 0) {
3057 $access = true;
3058 } else if ($USER->enrol['tempguest'][$course->id] > time()) {
3059 $access = true;
3060 } else {
3061 //expired
3062 unset($USER->enrol['tempguest'][$course->id]);
3063 remove_temp_course_roles($coursecontext);
3067 if ($access) {
3068 // cache ok
3069 } else {
3070 $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id);
3071 if ($until !== false) {
3072 // active participants may always access, a timestamp in the future, 0 (always) or false.
3073 if ($until == 0) {
3074 $until = ENROL_MAX_TIMESTAMP;
3076 $USER->enrol['enrolled'][$course->id] = $until;
3077 $access = true;
3079 } else {
3080 $instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder, id ASC');
3081 $enrols = enrol_get_plugins(true);
3082 // first ask all enabled enrol instances in course if they want to auto enrol user
3083 foreach($instances as $instance) {
3084 if (!isset($enrols[$instance->enrol])) {
3085 continue;
3087 // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false.
3088 $until = $enrols[$instance->enrol]->try_autoenrol($instance);
3089 if ($until !== false) {
3090 if ($until == 0) {
3091 $until = ENROL_MAX_TIMESTAMP;
3093 $USER->enrol['enrolled'][$course->id] = $until;
3094 $access = true;
3095 break;
3098 // if not enrolled yet try to gain temporary guest access
3099 if (!$access) {
3100 foreach($instances as $instance) {
3101 if (!isset($enrols[$instance->enrol])) {
3102 continue;
3104 // Get a duration for the guest access, a timestamp in the future or false.
3105 $until = $enrols[$instance->enrol]->try_guestaccess($instance);
3106 if ($until !== false and $until > time()) {
3107 $USER->enrol['tempguest'][$course->id] = $until;
3108 $access = true;
3109 break;
3117 if (!$access) {
3118 if ($preventredirect) {
3119 throw new require_login_exception('Not enrolled');
3121 if ($setwantsurltome) {
3122 $SESSION->wantsurl = qualified_me();
3124 redirect($CFG->wwwroot .'/enrol/index.php?id='. $course->id);
3128 // Check visibility of activity to current user; includes visible flag, groupmembersonly,
3129 // conditional availability, etc
3130 if ($cm && !$cm->uservisible) {
3131 if ($preventredirect) {
3132 throw new require_login_exception('Activity is hidden');
3134 if ($course->id != SITEID) {
3135 $url = new moodle_url('/course/view.php', array('id'=>$course->id));
3136 } else {
3137 $url = new moodle_url('/');
3139 redirect($url, get_string('activityiscurrentlyhidden'));
3142 // Finally access granted, update lastaccess times
3143 user_accesstime_log($course->id);
3148 * This function just makes sure a user is logged out.
3150 * @package core_access
3152 function require_logout() {
3153 global $USER;
3155 $params = $USER;
3157 if (isloggedin()) {
3158 add_to_log(SITEID, "user", "logout", "view.php?id=$USER->id&course=".SITEID, $USER->id, 0, $USER->id);
3160 $authsequence = get_enabled_auth_plugins(); // auths, in sequence
3161 foreach($authsequence as $authname) {
3162 $authplugin = get_auth_plugin($authname);
3163 $authplugin->prelogout_hook();
3167 events_trigger('user_logout', $params);
3168 session_get_instance()->terminate_current();
3169 unset($params);
3173 * Weaker version of require_login()
3175 * This is a weaker version of {@link require_login()} which only requires login
3176 * when called from within a course rather than the site page, unless
3177 * the forcelogin option is turned on.
3178 * @see require_login()
3180 * @package core_access
3181 * @category access
3183 * @param mixed $courseorid The course object or id in question
3184 * @param bool $autologinguest Allow autologin guests if that is wanted
3185 * @param object $cm Course activity module if known
3186 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
3187 * true. Used to avoid (=false) some scripts (file.php...) to set that variable,
3188 * in order to keep redirects working properly. MDL-14495
3189 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
3190 * @return void
3192 function require_course_login($courseorid, $autologinguest = true, $cm = NULL, $setwantsurltome = true, $preventredirect = false) {
3193 global $CFG, $PAGE, $SITE;
3194 $issite = (is_object($courseorid) and $courseorid->id == SITEID)
3195 or (!is_object($courseorid) and $courseorid == SITEID);
3196 if ($issite && !empty($cm) && !($cm instanceof cm_info)) {
3197 // note: nearly all pages call get_fast_modinfo anyway and it does not make any
3198 // db queries so this is not really a performance concern, however it is obviously
3199 // better if you use get_fast_modinfo to get the cm before calling this.
3200 if (is_object($courseorid)) {
3201 $course = $courseorid;
3202 } else {
3203 $course = clone($SITE);
3205 $modinfo = get_fast_modinfo($course);
3206 $cm = $modinfo->get_cm($cm->id);
3208 if (!empty($CFG->forcelogin)) {
3209 // login required for both SITE and courses
3210 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3212 } else if ($issite && !empty($cm) and !$cm->uservisible) {
3213 // always login for hidden activities
3214 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3216 } else if ($issite) {
3217 //login for SITE not required
3218 if ($cm and empty($cm->visible)) {
3219 // hidden activities are not accessible without login
3220 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3221 } else if ($cm and !empty($CFG->enablegroupmembersonly) and $cm->groupmembersonly) {
3222 // not-logged-in users do not have any group membership
3223 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3224 } else {
3225 // We still need to instatiate PAGE vars properly so that things
3226 // that rely on it like navigation function correctly.
3227 if (!empty($courseorid)) {
3228 if (is_object($courseorid)) {
3229 $course = $courseorid;
3230 } else {
3231 $course = clone($SITE);
3233 if ($cm) {
3234 if ($cm->course != $course->id) {
3235 throw new coding_exception('course and cm parameters in require_course_login() call do not match!!');
3237 $PAGE->set_cm($cm, $course);
3238 $PAGE->set_pagelayout('incourse');
3239 } else {
3240 $PAGE->set_course($course);
3242 } else {
3243 // If $PAGE->course, and hence $PAGE->context, have not already been set
3244 // up properly, set them up now.
3245 $PAGE->set_course($PAGE->course);
3247 //TODO: verify conditional activities here
3248 user_accesstime_log(SITEID);
3249 return;
3252 } else {
3253 // course login always required
3254 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3259 * Require key login. Function terminates with error if key not found or incorrect.
3261 * @global object
3262 * @global object
3263 * @global object
3264 * @global object
3265 * @uses NO_MOODLE_COOKIES
3266 * @uses PARAM_ALPHANUM
3267 * @param string $script unique script identifier
3268 * @param int $instance optional instance id
3269 * @return int Instance ID
3271 function require_user_key_login($script, $instance=null) {
3272 global $USER, $SESSION, $CFG, $DB;
3274 if (!NO_MOODLE_COOKIES) {
3275 print_error('sessioncookiesdisable');
3278 /// extra safety
3279 @session_write_close();
3281 $keyvalue = required_param('key', PARAM_ALPHANUM);
3283 if (!$key = $DB->get_record('user_private_key', array('script'=>$script, 'value'=>$keyvalue, 'instance'=>$instance))) {
3284 print_error('invalidkey');
3287 if (!empty($key->validuntil) and $key->validuntil < time()) {
3288 print_error('expiredkey');
3291 if ($key->iprestriction) {
3292 $remoteaddr = getremoteaddr(null);
3293 if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) {
3294 print_error('ipmismatch');
3298 if (!$user = $DB->get_record('user', array('id'=>$key->userid))) {
3299 print_error('invaliduserid');
3302 /// emulate normal session
3303 enrol_check_plugins($user);
3304 session_set_user($user);
3306 /// note we are not using normal login
3307 if (!defined('USER_KEY_LOGIN')) {
3308 define('USER_KEY_LOGIN', true);
3311 /// return instance id - it might be empty
3312 return $key->instance;
3316 * Creates a new private user access key.
3318 * @global object
3319 * @param string $script unique target identifier
3320 * @param int $userid
3321 * @param int $instance optional instance id
3322 * @param string $iprestriction optional ip restricted access
3323 * @param timestamp $validuntil key valid only until given data
3324 * @return string access key value
3326 function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3327 global $DB;
3329 $key = new stdClass();
3330 $key->script = $script;
3331 $key->userid = $userid;
3332 $key->instance = $instance;
3333 $key->iprestriction = $iprestriction;
3334 $key->validuntil = $validuntil;
3335 $key->timecreated = time();
3337 $key->value = md5($userid.'_'.time().random_string(40)); // something long and unique
3338 while ($DB->record_exists('user_private_key', array('value'=>$key->value))) {
3339 // must be unique
3340 $key->value = md5($userid.'_'.time().random_string(40));
3342 $DB->insert_record('user_private_key', $key);
3343 return $key->value;
3347 * Delete the user's new private user access keys for a particular script.
3349 * @global object
3350 * @param string $script unique target identifier
3351 * @param int $userid
3352 * @return void
3354 function delete_user_key($script,$userid) {
3355 global $DB;
3356 $DB->delete_records('user_private_key', array('script'=>$script, 'userid'=>$userid));
3360 * Gets a private user access key (and creates one if one doesn't exist).
3362 * @global object
3363 * @param string $script unique target identifier
3364 * @param int $userid
3365 * @param int $instance optional instance id
3366 * @param string $iprestriction optional ip restricted access
3367 * @param timestamp $validuntil key valid only until given data
3368 * @return string access key value
3370 function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3371 global $DB;
3373 if ($key = $DB->get_record('user_private_key', array('script'=>$script, 'userid'=>$userid,
3374 'instance'=>$instance, 'iprestriction'=>$iprestriction,
3375 'validuntil'=>$validuntil))) {
3376 return $key->value;
3377 } else {
3378 return create_user_key($script, $userid, $instance, $iprestriction, $validuntil);
3384 * Modify the user table by setting the currently logged in user's
3385 * last login to now.
3387 * @global object
3388 * @global object
3389 * @return bool Always returns true
3391 function update_user_login_times() {
3392 global $USER, $DB;
3394 if (isguestuser()) {
3395 // Do not update guest access times/ips for performance.
3396 return true;
3399 $now = time();
3401 $user = new stdClass();
3402 $user->id = $USER->id;
3404 // Make sure all users that logged in have some firstaccess.
3405 if ($USER->firstaccess == 0) {
3406 $USER->firstaccess = $user->firstaccess = $now;
3409 // Store the previous current as lastlogin.
3410 $USER->lastlogin = $user->lastlogin = $USER->currentlogin;
3412 $USER->currentlogin = $user->currentlogin = $now;
3414 // Function user_accesstime_log() may not update immediately, better do it here.
3415 $USER->lastaccess = $user->lastaccess = $now;
3416 $USER->lastip = $user->lastip = getremoteaddr();
3418 $DB->update_record('user', $user);
3419 return true;
3423 * Determines if a user has completed setting up their account.
3425 * @param user $user A {@link $USER} object to test for the existence of a valid name and email
3426 * @return bool
3428 function user_not_fully_set_up($user) {
3429 if (isguestuser($user)) {
3430 return false;
3432 return (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user));
3436 * Check whether the user has exceeded the bounce threshold
3438 * @global object
3439 * @global object
3440 * @param user $user A {@link $USER} object
3441 * @return bool true=>User has exceeded bounce threshold
3443 function over_bounce_threshold($user) {
3444 global $CFG, $DB;
3446 if (empty($CFG->handlebounces)) {
3447 return false;
3450 if (empty($user->id)) { /// No real (DB) user, nothing to do here.
3451 return false;
3454 // set sensible defaults
3455 if (empty($CFG->minbounces)) {
3456 $CFG->minbounces = 10;
3458 if (empty($CFG->bounceratio)) {
3459 $CFG->bounceratio = .20;
3461 $bouncecount = 0;
3462 $sendcount = 0;
3463 if ($bounce = $DB->get_record('user_preferences', array ('userid'=>$user->id, 'name'=>'email_bounce_count'))) {
3464 $bouncecount = $bounce->value;
3466 if ($send = $DB->get_record('user_preferences', array('userid'=>$user->id, 'name'=>'email_send_count'))) {
3467 $sendcount = $send->value;
3469 return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio);
3473 * Used to increment or reset email sent count
3475 * @global object
3476 * @param user $user object containing an id
3477 * @param bool $reset will reset the count to 0
3478 * @return void
3480 function set_send_count($user,$reset=false) {
3481 global $DB;
3483 if (empty($user->id)) { /// No real (DB) user, nothing to do here.
3484 return;
3487 if ($pref = $DB->get_record('user_preferences', array('userid'=>$user->id, 'name'=>'email_send_count'))) {
3488 $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3489 $DB->update_record('user_preferences', $pref);
3491 else if (!empty($reset)) { // if it's not there and we're resetting, don't bother.
3492 // make a new one
3493 $pref = new stdClass();
3494 $pref->name = 'email_send_count';
3495 $pref->value = 1;
3496 $pref->userid = $user->id;
3497 $DB->insert_record('user_preferences', $pref, false);
3502 * Increment or reset user's email bounce count
3504 * @global object
3505 * @param user $user object containing an id
3506 * @param bool $reset will reset the count to 0
3508 function set_bounce_count($user,$reset=false) {
3509 global $DB;
3511 if ($pref = $DB->get_record('user_preferences', array('userid'=>$user->id, 'name'=>'email_bounce_count'))) {
3512 $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3513 $DB->update_record('user_preferences', $pref);
3515 else if (!empty($reset)) { // if it's not there and we're resetting, don't bother.
3516 // make a new one
3517 $pref = new stdClass();
3518 $pref->name = 'email_bounce_count';
3519 $pref->value = 1;
3520 $pref->userid = $user->id;
3521 $DB->insert_record('user_preferences', $pref, false);
3526 * Determines if the currently logged in user is in editing mode.
3527 * Note: originally this function had $userid parameter - it was not usable anyway
3529 * @deprecated since Moodle 2.0 - use $PAGE->user_is_editing() instead.
3530 * @todo Deprecated function remove when ready
3532 * @global object
3533 * @uses DEBUG_DEVELOPER
3534 * @return bool
3536 function isediting() {
3537 global $PAGE;
3538 debugging('call to deprecated function isediting(). Please use $PAGE->user_is_editing() instead', DEBUG_DEVELOPER);
3539 return $PAGE->user_is_editing();
3543 * Determines if the logged in user is currently moving an activity
3545 * @global object
3546 * @param int $courseid The id of the course being tested
3547 * @return bool
3549 function ismoving($courseid) {
3550 global $USER;
3552 if (!empty($USER->activitycopy)) {
3553 return ($USER->activitycopycourse == $courseid);
3555 return false;
3559 * Returns a persons full name
3561 * Given an object containing firstname and lastname
3562 * values, this function returns a string with the
3563 * full name of the person.
3564 * The result may depend on system settings
3565 * or language. 'override' will force both names
3566 * to be used even if system settings specify one.
3568 * @global object
3569 * @global object
3570 * @param object $user A {@link $USER} object to get full name of
3571 * @param bool $override If true then the name will be first name followed by last name rather than adhering to fullnamedisplay setting.
3572 * @return string
3574 function fullname($user, $override=false) {
3575 global $CFG, $SESSION;
3577 if (!isset($user->firstname) and !isset($user->lastname)) {
3578 return '';
3581 if (!$override) {
3582 if (!empty($CFG->forcefirstname)) {
3583 $user->firstname = $CFG->forcefirstname;
3585 if (!empty($CFG->forcelastname)) {
3586 $user->lastname = $CFG->forcelastname;
3590 if (!empty($SESSION->fullnamedisplay)) {
3591 $CFG->fullnamedisplay = $SESSION->fullnamedisplay;
3594 if (!isset($CFG->fullnamedisplay) or $CFG->fullnamedisplay === 'firstname lastname') {
3595 return $user->firstname .' '. $user->lastname;
3597 } else if ($CFG->fullnamedisplay == 'lastname firstname') {
3598 return $user->lastname .' '. $user->firstname;
3600 } else if ($CFG->fullnamedisplay == 'firstname') {
3601 if ($override) {
3602 return get_string('fullnamedisplay', '', $user);
3603 } else {
3604 return $user->firstname;
3608 return get_string('fullnamedisplay', '', $user);
3612 * Checks if current user is shown any extra fields when listing users.
3613 * @param object $context Context
3614 * @param array $already Array of fields that we're going to show anyway
3615 * so don't bother listing them
3616 * @return array Array of field names from user table, not including anything
3617 * listed in $already
3619 function get_extra_user_fields($context, $already = array()) {
3620 global $CFG;
3622 // Only users with permission get the extra fields
3623 if (!has_capability('moodle/site:viewuseridentity', $context)) {
3624 return array();
3627 // Split showuseridentity on comma
3628 if (empty($CFG->showuseridentity)) {
3629 // Explode gives wrong result with empty string
3630 $extra = array();
3631 } else {
3632 $extra = explode(',', $CFG->showuseridentity);
3634 $renumber = false;
3635 foreach ($extra as $key => $field) {
3636 if (in_array($field, $already)) {
3637 unset($extra[$key]);
3638 $renumber = true;
3641 if ($renumber) {
3642 // For consistency, if entries are removed from array, renumber it
3643 // so they are numbered as you would expect
3644 $extra = array_merge($extra);
3646 return $extra;
3650 * If the current user is to be shown extra user fields when listing or
3651 * selecting users, returns a string suitable for including in an SQL select
3652 * clause to retrieve those fields.
3653 * @param object $context Context
3654 * @param string $alias Alias of user table, e.g. 'u' (default none)
3655 * @param string $prefix Prefix for field names using AS, e.g. 'u_' (default none)
3656 * @param array $already Array of fields that we're going to include anyway
3657 * so don't list them (default none)
3658 * @return string Partial SQL select clause, beginning with comma, for example
3659 * ',u.idnumber,u.department' unless it is blank
3661 function get_extra_user_fields_sql($context, $alias='', $prefix='',
3662 $already = array()) {
3663 $fields = get_extra_user_fields($context, $already);
3664 $result = '';
3665 // Add punctuation for alias
3666 if ($alias !== '') {
3667 $alias .= '.';
3669 foreach ($fields as $field) {
3670 $result .= ', ' . $alias . $field;
3671 if ($prefix) {
3672 $result .= ' AS ' . $prefix . $field;
3675 return $result;
3679 * Returns the display name of a field in the user table. Works for most fields
3680 * that are commonly displayed to users.
3681 * @param string $field Field name, e.g. 'phone1'
3682 * @return string Text description taken from language file, e.g. 'Phone number'
3684 function get_user_field_name($field) {
3685 // Some fields have language strings which are not the same as field name
3686 switch ($field) {
3687 case 'phone1' : return get_string('phone');
3688 case 'url' : return get_string('webpage');
3689 case 'icq' : return get_string('icqnumber');
3690 case 'skype' : return get_string('skypeid');
3691 case 'aim' : return get_string('aimid');
3692 case 'yahoo' : return get_string('yahooid');
3693 case 'msn' : return get_string('msnid');
3695 // Otherwise just use the same lang string
3696 return get_string($field);
3700 * Returns whether a given authentication plugin exists.
3702 * @global object
3703 * @param string $auth Form of authentication to check for. Defaults to the
3704 * global setting in {@link $CFG}.
3705 * @return boolean Whether the plugin is available.
3707 function exists_auth_plugin($auth) {
3708 global $CFG;
3710 if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) {
3711 return is_readable("{$CFG->dirroot}/auth/$auth/auth.php");
3713 return false;
3717 * Checks if a given plugin is in the list of enabled authentication plugins.
3719 * @param string $auth Authentication plugin.
3720 * @return boolean Whether the plugin is enabled.
3722 function is_enabled_auth($auth) {
3723 if (empty($auth)) {
3724 return false;
3727 $enabled = get_enabled_auth_plugins();
3729 return in_array($auth, $enabled);
3733 * Returns an authentication plugin instance.
3735 * @global object
3736 * @param string $auth name of authentication plugin
3737 * @return auth_plugin_base An instance of the required authentication plugin.
3739 function get_auth_plugin($auth) {
3740 global $CFG;
3742 // check the plugin exists first
3743 if (! exists_auth_plugin($auth)) {
3744 print_error('authpluginnotfound', 'debug', '', $auth);
3747 // return auth plugin instance
3748 require_once "{$CFG->dirroot}/auth/$auth/auth.php";
3749 $class = "auth_plugin_$auth";
3750 return new $class;
3754 * Returns array of active auth plugins.
3756 * @param bool $fix fix $CFG->auth if needed
3757 * @return array
3759 function get_enabled_auth_plugins($fix=false) {
3760 global $CFG;
3762 $default = array('manual', 'nologin');
3764 if (empty($CFG->auth)) {
3765 $auths = array();
3766 } else {
3767 $auths = explode(',', $CFG->auth);
3770 if ($fix) {
3771 $auths = array_unique($auths);
3772 foreach($auths as $k=>$authname) {
3773 if (!exists_auth_plugin($authname) or in_array($authname, $default)) {
3774 unset($auths[$k]);
3777 $newconfig = implode(',', $auths);
3778 if (!isset($CFG->auth) or $newconfig != $CFG->auth) {
3779 set_config('auth', $newconfig);
3783 return (array_merge($default, $auths));
3787 * Returns true if an internal authentication method is being used.
3788 * if method not specified then, global default is assumed
3790 * @param string $auth Form of authentication required
3791 * @return bool
3793 function is_internal_auth($auth) {
3794 $authplugin = get_auth_plugin($auth); // throws error if bad $auth
3795 return $authplugin->is_internal();
3799 * Returns true if the user is a 'restored' one
3801 * Used in the login process to inform the user
3802 * and allow him/her to reset the password
3804 * @uses $CFG
3805 * @uses $DB
3806 * @param string $username username to be checked
3807 * @return bool
3809 function is_restored_user($username) {
3810 global $CFG, $DB;
3812 return $DB->record_exists('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id, 'password'=>'restored'));
3816 * Returns an array of user fields
3818 * @return array User field/column names
3820 function get_user_fieldnames() {
3821 global $DB;
3823 $fieldarray = $DB->get_columns('user');
3824 unset($fieldarray['id']);
3825 $fieldarray = array_keys($fieldarray);
3827 return $fieldarray;
3831 * Creates a bare-bones user record
3833 * @todo Outline auth types and provide code example
3835 * @param string $username New user's username to add to record
3836 * @param string $password New user's password to add to record
3837 * @param string $auth Form of authentication required
3838 * @return stdClass A complete user object
3840 function create_user_record($username, $password, $auth = 'manual') {
3841 global $CFG, $DB;
3843 //just in case check text case
3844 $username = trim(textlib::strtolower($username));
3846 $authplugin = get_auth_plugin($auth);
3848 $newuser = new stdClass();
3850 if ($newinfo = $authplugin->get_userinfo($username)) {
3851 $newinfo = truncate_userinfo($newinfo);
3852 foreach ($newinfo as $key => $value){
3853 $newuser->$key = $value;
3857 if (!empty($newuser->email)) {
3858 if (email_is_not_allowed($newuser->email)) {
3859 unset($newuser->email);
3863 if (!isset($newuser->city)) {
3864 $newuser->city = '';
3867 $newuser->auth = $auth;
3868 $newuser->username = $username;
3870 // fix for MDL-8480
3871 // user CFG lang for user if $newuser->lang is empty
3872 // or $user->lang is not an installed language
3873 if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) {
3874 $newuser->lang = $CFG->lang;
3876 $newuser->confirmed = 1;
3877 $newuser->lastip = getremoteaddr();
3878 $newuser->timecreated = time();
3879 $newuser->timemodified = $newuser->timecreated;
3880 $newuser->mnethostid = $CFG->mnet_localhost_id;
3882 $newuser->id = $DB->insert_record('user', $newuser);
3883 $user = get_complete_user_data('id', $newuser->id);
3884 if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})){
3885 set_user_preference('auth_forcepasswordchange', 1, $user);
3887 // Set the password.
3888 update_internal_user_password($user, $password);
3890 // fetch full user record for the event, the complete user data contains too much info
3891 // and we want to be consistent with other places that trigger this event
3892 events_trigger('user_created', $DB->get_record('user', array('id'=>$user->id)));
3894 return $user;
3898 * Will update a local user record from an external source.
3899 * (MNET users can not be updated using this method!)
3901 * @param string $username user's username to update the record
3902 * @return stdClass A complete user object
3904 function update_user_record($username) {
3905 global $DB, $CFG;
3907 $username = trim(textlib::strtolower($username)); /// just in case check text case
3909 $oldinfo = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id), '*', MUST_EXIST);
3910 $newuser = array();
3911 $userauth = get_auth_plugin($oldinfo->auth);
3913 if ($newinfo = $userauth->get_userinfo($username)) {
3914 $newinfo = truncate_userinfo($newinfo);
3915 foreach ($newinfo as $key => $value){
3916 $key = strtolower($key);
3917 if (!property_exists($oldinfo, $key) or $key === 'username' or $key === 'id'
3918 or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') {
3919 // unknown or must not be changed
3920 continue;
3922 $confval = $userauth->config->{'field_updatelocal_' . $key};
3923 $lockval = $userauth->config->{'field_lock_' . $key};
3924 if (empty($confval) || empty($lockval)) {
3925 continue;
3927 if ($confval === 'onlogin') {
3928 // MDL-4207 Don't overwrite modified user profile values with
3929 // empty LDAP values when 'unlocked if empty' is set. The purpose
3930 // of the setting 'unlocked if empty' is to allow the user to fill
3931 // in a value for the selected field _if LDAP is giving
3932 // nothing_ for this field. Thus it makes sense to let this value
3933 // stand in until LDAP is giving a value for this field.
3934 if (!(empty($value) && $lockval === 'unlockedifempty')) {
3935 if ((string)$oldinfo->$key !== (string)$value) {
3936 $newuser[$key] = (string)$value;
3941 if ($newuser) {
3942 $newuser['id'] = $oldinfo->id;
3943 $newuser['timemodified'] = time();
3944 $DB->update_record('user', $newuser);
3945 // fetch full user record for the event, the complete user data contains too much info
3946 // and we want to be consistent with other places that trigger this event
3947 events_trigger('user_updated', $DB->get_record('user', array('id'=>$oldinfo->id)));
3951 return get_complete_user_data('id', $oldinfo->id);
3955 * Will truncate userinfo as it comes from auth_get_userinfo (from external auth)
3956 * which may have large fields
3958 * @todo Add vartype handling to ensure $info is an array
3960 * @param array $info Array of user properties to truncate if needed
3961 * @return array The now truncated information that was passed in
3963 function truncate_userinfo($info) {
3964 // define the limits
3965 $limit = array(
3966 'username' => 100,
3967 'idnumber' => 255,
3968 'firstname' => 100,
3969 'lastname' => 100,
3970 'email' => 100,
3971 'icq' => 15,
3972 'phone1' => 20,
3973 'phone2' => 20,
3974 'institution' => 40,
3975 'department' => 30,
3976 'address' => 70,
3977 'city' => 120,
3978 'country' => 2,
3979 'url' => 255,
3982 // apply where needed
3983 foreach (array_keys($info) as $key) {
3984 if (!empty($limit[$key])) {
3985 $info[$key] = trim(textlib::substr($info[$key],0, $limit[$key]));
3989 return $info;
3993 * Marks user deleted in internal user database and notifies the auth plugin.
3994 * Also unenrols user from all roles and does other cleanup.
3996 * Any plugin that needs to purge user data should register the 'user_deleted' event.
3998 * @param stdClass $user full user object before delete
3999 * @return boolean success
4000 * @throws coding_exception if invalid $user parameter detected
4002 function delete_user(stdClass $user) {
4003 global $CFG, $DB;
4004 require_once($CFG->libdir.'/grouplib.php');
4005 require_once($CFG->libdir.'/gradelib.php');
4006 require_once($CFG->dirroot.'/message/lib.php');
4007 require_once($CFG->dirroot.'/tag/lib.php');
4009 // Make sure nobody sends bogus record type as parameter.
4010 if (!property_exists($user, 'id') or !property_exists($user, 'username')) {
4011 throw new coding_exception('Invalid $user parameter in delete_user() detected');
4014 // Better not trust the parameter and fetch the latest info,
4015 // this will be very expensive anyway.
4016 if (!$user = $DB->get_record('user', array('id'=>$user->id))) {
4017 debugging('Attempt to delete unknown user account.');
4018 return false;
4021 // There must be always exactly one guest record,
4022 // originally the guest account was identified by username only,
4023 // now we use $CFG->siteguest for performance reasons.
4024 if ($user->username === 'guest' or isguestuser($user)) {
4025 debugging('Guest user account can not be deleted.');
4026 return false;
4029 // Admin can be theoretically from different auth plugin,
4030 // but we want to prevent deletion of internal accoutns only,
4031 // if anything goes wrong ppl may force somebody to be admin via
4032 // config.php setting $CFG->siteadmins.
4033 if ($user->auth === 'manual' and is_siteadmin($user)) {
4034 debugging('Local administrator accounts can not be deleted.');
4035 return false;
4038 // delete all grades - backup is kept in grade_grades_history table
4039 grade_user_delete($user->id);
4041 //move unread messages from this user to read
4042 message_move_userfrom_unread2read($user->id);
4044 // TODO: remove from cohorts using standard API here
4046 // remove user tags
4047 tag_set('user', $user->id, array());
4049 // unconditionally unenrol from all courses
4050 enrol_user_delete($user);
4052 // unenrol from all roles in all contexts
4053 role_unassign_all(array('userid'=>$user->id)); // this might be slow but it is really needed - modules might do some extra cleanup!
4055 //now do a brute force cleanup
4057 // remove from all cohorts
4058 $DB->delete_records('cohort_members', array('userid'=>$user->id));
4060 // remove from all groups
4061 $DB->delete_records('groups_members', array('userid'=>$user->id));
4063 // brute force unenrol from all courses
4064 $DB->delete_records('user_enrolments', array('userid'=>$user->id));
4066 // purge user preferences
4067 $DB->delete_records('user_preferences', array('userid'=>$user->id));
4069 // purge user extra profile info
4070 $DB->delete_records('user_info_data', array('userid'=>$user->id));
4072 // last course access not necessary either
4073 $DB->delete_records('user_lastaccess', array('userid'=>$user->id));
4075 // remove all user tokens
4076 $DB->delete_records('external_tokens', array('userid'=>$user->id));
4078 // unauthorise the user for all services
4079 $DB->delete_records('external_services_users', array('userid'=>$user->id));
4081 // Remove users private keys.
4082 $DB->delete_records('user_private_key', array('userid' => $user->id));
4084 // force logout - may fail if file based sessions used, sorry
4085 session_kill_user($user->id);
4087 // now do a final accesslib cleanup - removes all role assignments in user context and context itself
4088 delete_context(CONTEXT_USER, $user->id);
4090 // workaround for bulk deletes of users with the same email address
4091 $delname = "$user->email.".time();
4092 while ($DB->record_exists('user', array('username'=>$delname))) { // no need to use mnethostid here
4093 $delname++;
4096 // mark internal user record as "deleted"
4097 $updateuser = new stdClass();
4098 $updateuser->id = $user->id;
4099 $updateuser->deleted = 1;
4100 $updateuser->username = $delname; // Remember it just in case
4101 $updateuser->email = md5($user->username);// Store hash of username, useful importing/restoring users
4102 $updateuser->idnumber = ''; // Clear this field to free it up
4103 $updateuser->picture = 0;
4104 $updateuser->timemodified = time();
4106 $DB->update_record('user', $updateuser);
4107 // Add this action to log
4108 add_to_log(SITEID, 'user', 'delete', "view.php?id=$user->id", $user->firstname.' '.$user->lastname);
4111 // We will update the user's timemodified, as it will be passed to the user_deleted event, which
4112 // should know about this updated property persisted to the user's table.
4113 $user->timemodified = $updateuser->timemodified;
4115 // notify auth plugin - do not block the delete even when plugin fails
4116 $authplugin = get_auth_plugin($user->auth);
4117 $authplugin->user_delete($user);
4119 // any plugin that needs to cleanup should register this event
4120 events_trigger('user_deleted', $user);
4122 return true;
4126 * Retrieve the guest user object
4128 * @global object
4129 * @global object
4130 * @return user A {@link $USER} object
4132 function guest_user() {
4133 global $CFG, $DB;
4135 if ($newuser = $DB->get_record('user', array('id'=>$CFG->siteguest))) {
4136 $newuser->confirmed = 1;
4137 $newuser->lang = $CFG->lang;
4138 $newuser->lastip = getremoteaddr();
4141 return $newuser;
4145 * Authenticates a user against the chosen authentication mechanism
4147 * Given a username and password, this function looks them
4148 * up using the currently selected authentication mechanism,
4149 * and if the authentication is successful, it returns a
4150 * valid $user object from the 'user' table.
4152 * Uses auth_ functions from the currently active auth module
4154 * After authenticate_user_login() returns success, you will need to
4155 * log that the user has logged in, and call complete_user_login() to set
4156 * the session up.
4158 * Note: this function works only with non-mnet accounts!
4160 * @param string $username User's username
4161 * @param string $password User's password
4162 * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO
4163 * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists)
4164 * @return stdClass|false A {@link $USER} object or false if error
4166 function authenticate_user_login($username, $password, $ignorelockout=false, &$failurereason=null) {
4167 global $CFG, $DB;
4168 require_once("$CFG->libdir/authlib.php");
4170 $authsenabled = get_enabled_auth_plugins();
4172 if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) {
4173 $auth = empty($user->auth) ? 'manual' : $user->auth; // use manual if auth not set
4174 if (!empty($user->suspended)) {
4175 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4176 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4177 $failurereason = AUTH_LOGIN_SUSPENDED;
4178 return false;
4180 if ($auth=='nologin' or !is_enabled_auth($auth)) {
4181 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4182 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Disabled Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4183 $failurereason = AUTH_LOGIN_SUSPENDED; // Legacy way to suspend user.
4184 return false;
4186 $auths = array($auth);
4188 } else {
4189 // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user().
4190 if ($DB->get_field('user', 'id', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id, 'deleted'=>1))) {
4191 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Deleted Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4192 $failurereason = AUTH_LOGIN_NOUSER;
4193 return false;
4196 // Do not try to authenticate non-existent accounts when user creation is not disabled.
4197 if (!empty($CFG->authpreventaccountcreation)) {
4198 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4199 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Unknown user, can not create new accounts: $username ".$_SERVER['HTTP_USER_AGENT']);
4200 $failurereason = AUTH_LOGIN_NOUSER;
4201 return false;
4204 // User does not exist
4205 $auths = $authsenabled;
4206 $user = new stdClass();
4207 $user->id = 0;
4210 if ($ignorelockout) {
4211 // Some other mechanism protects against brute force password guessing,
4212 // for example login form might include reCAPTCHA or this function
4213 // is called from a SSO script.
4215 } else if ($user->id) {
4216 // Verify login lockout after other ways that may prevent user login.
4217 if (login_is_lockedout($user)) {
4218 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4219 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Login lockout: $username ".$_SERVER['HTTP_USER_AGENT']);
4220 $failurereason = AUTH_LOGIN_LOCKOUT;
4221 return false;
4224 } else {
4225 // We can not lockout non-existing accounts.
4228 foreach ($auths as $auth) {
4229 $authplugin = get_auth_plugin($auth);
4231 // on auth fail fall through to the next plugin
4232 if (!$authplugin->user_login($username, $password)) {
4233 continue;
4236 // successful authentication
4237 if ($user->id) { // User already exists in database
4238 if (empty($user->auth)) { // For some reason auth isn't set yet
4239 $DB->set_field('user', 'auth', $auth, array('username'=>$username));
4240 $user->auth = $auth;
4243 // If the existing hash is using an out-of-date algorithm (or the
4244 // legacy md5 algorithm), then we should update to the current
4245 // hash algorithm while we have access to the user's password.
4246 update_internal_user_password($user, $password);
4248 if ($authplugin->is_synchronised_with_external()) { // update user record from external DB
4249 $user = update_user_record($username);
4251 } else {
4252 // Create account, we verified above that user creation is allowed.
4253 $user = create_user_record($username, $password, $auth);
4256 $authplugin->sync_roles($user);
4258 foreach ($authsenabled as $hau) {
4259 $hauth = get_auth_plugin($hau);
4260 $hauth->user_authenticated_hook($user, $username, $password);
4263 if (empty($user->id)) {
4264 $failurereason = AUTH_LOGIN_NOUSER;
4265 return false;
4268 if (!empty($user->suspended)) {
4269 // just in case some auth plugin suspended account
4270 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4271 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4272 $failurereason = AUTH_LOGIN_SUSPENDED;
4273 return false;
4276 login_attempt_valid($user);
4277 $failurereason = AUTH_LOGIN_OK;
4278 return $user;
4281 // failed if all the plugins have failed
4282 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
4283 if (debugging('', DEBUG_ALL)) {
4284 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Failed Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4287 if ($user->id) {
4288 login_attempt_failed($user);
4289 $failurereason = AUTH_LOGIN_FAILED;
4290 } else {
4291 $failurereason = AUTH_LOGIN_NOUSER;
4294 return false;
4298 * Call to complete the user login process after authenticate_user_login()
4299 * has succeeded. It will setup the $USER variable and other required bits
4300 * and pieces.
4302 * NOTE:
4303 * - It will NOT log anything -- up to the caller to decide what to log.
4304 * - this function does not set any cookies any more!
4306 * @param object $user
4307 * @return object A {@link $USER} object - BC only, do not use
4309 function complete_user_login($user) {
4310 global $CFG, $USER;
4312 // regenerate session id and delete old session,
4313 // this helps prevent session fixation attacks from the same domain
4314 session_regenerate_id(true);
4316 // let enrol plugins deal with new enrolments if necessary
4317 enrol_check_plugins($user);
4319 // check enrolments, load caps and setup $USER object
4320 session_set_user($user);
4322 // reload preferences from DB
4323 unset($USER->preference);
4324 check_user_preferences_loaded($USER);
4326 // update login times
4327 update_user_login_times();
4329 // extra session prefs init
4330 set_login_session_preferences();
4332 if (isguestuser()) {
4333 // no need to continue when user is THE guest
4334 return $USER;
4337 /// Select password change url
4338 $userauth = get_auth_plugin($USER->auth);
4340 /// check whether the user should be changing password
4341 if (get_user_preferences('auth_forcepasswordchange', false)){
4342 if ($userauth->can_change_password()) {
4343 if ($changeurl = $userauth->change_password_url()) {
4344 redirect($changeurl);
4345 } else {
4346 redirect($CFG->httpswwwroot.'/login/change_password.php');
4348 } else {
4349 print_error('nopasswordchangeforced', 'auth');
4352 return $USER;
4356 * Check a password hash to see if it was hashed using the
4357 * legacy hash algorithm (md5).
4359 * @param string $password String to check.
4360 * @return boolean True if the $password matches the format of an md5 sum.
4362 function password_is_legacy_hash($password) {
4363 return (bool) preg_match('/^[0-9a-f]{32}$/', $password);
4367 * Checks whether the password compatibility library will work with the current
4368 * version of PHP. This cannot be done using PHP version numbers since the fix
4369 * has been backported to earlier versions in some distributions.
4371 * See https://github.com/ircmaxell/password_compat/issues/10 for
4372 * more details.
4374 * @return bool True if the library is NOT supported.
4376 function password_compat_not_supported() {
4378 $hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG';
4380 // Create a one off application cache to store bcrypt support status as
4381 // the support status doesn't change and crypt() is slow.
4382 $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'password_compat');
4384 if (!$bcryptsupport = $cache->get('bcryptsupport')) {
4385 $test = crypt('password', $hash);
4386 // Cache string instead of boolean to avoid MDL-37472.
4387 if ($test == $hash) {
4388 $bcryptsupport = 'supported';
4389 } else {
4390 $bcryptsupport = 'not supported';
4392 $cache->set('bcryptsupport', $bcryptsupport);
4395 // Return true if bcrypt *not* supported.
4396 return ($bcryptsupport !== 'supported');
4400 * Compare password against hash stored in user object to determine if it is valid.
4402 * If necessary it also updates the stored hash to the current format.
4404 * @param stdClass $user (Password property may be updated).
4405 * @param string $password Plain text password.
4406 * @return bool True if password is valid.
4408 function validate_internal_user_password($user, $password) {
4409 global $CFG;
4410 require_once($CFG->libdir.'/password_compat/lib/password.php');
4412 if ($user->password === AUTH_PASSWORD_NOT_CACHED) {
4413 // Internal password is not used at all, it can not validate.
4414 return false;
4417 // If hash isn't a legacy (md5) hash, validate using the library function.
4418 if (!password_is_legacy_hash($user->password)) {
4419 return password_verify($password, $user->password);
4422 // Otherwise we need to check for a legacy (md5) hash instead. If the hash
4423 // is valid we can then update it to the new algorithm.
4425 $sitesalt = isset($CFG->passwordsaltmain) ? $CFG->passwordsaltmain : '';
4426 $validated = false;
4428 if ($user->password === md5($password.$sitesalt)
4429 or $user->password === md5($password)
4430 or $user->password === md5(addslashes($password).$sitesalt)
4431 or $user->password === md5(addslashes($password))) {
4432 // note: we are intentionally using the addslashes() here because we
4433 // need to accept old password hashes of passwords with magic quotes
4434 $validated = true;
4436 } else {
4437 for ($i=1; $i<=20; $i++) { //20 alternative salts should be enough, right?
4438 $alt = 'passwordsaltalt'.$i;
4439 if (!empty($CFG->$alt)) {
4440 if ($user->password === md5($password.$CFG->$alt) or $user->password === md5(addslashes($password).$CFG->$alt)) {
4441 $validated = true;
4442 break;
4448 if ($validated) {
4449 // If the password matches the existing md5 hash, update to the
4450 // current hash algorithm while we have access to the user's password.
4451 update_internal_user_password($user, $password);
4454 return $validated;
4458 * Calculate hash for a plain text password.
4460 * @param string $password Plain text password to be hashed.
4461 * @param bool $fasthash If true, use a low cost factor when generating the hash
4462 * This is much faster to generate but makes the hash
4463 * less secure. It is used when lots of hashes need to
4464 * be generated quickly.
4465 * @return string The hashed password.
4467 * @throws moodle_exception If a problem occurs while generating the hash.
4469 function hash_internal_user_password($password, $fasthash = false) {
4470 global $CFG;
4471 require_once($CFG->libdir.'/password_compat/lib/password.php');
4473 // Use the legacy hashing algorithm (md5) if PHP is not new enough
4474 // to support bcrypt properly
4475 if (password_compat_not_supported()) {
4476 if (isset($CFG->passwordsaltmain)) {
4477 return md5($password.$CFG->passwordsaltmain);
4478 } else {
4479 return md5($password);
4483 // Set the cost factor to 4 for fast hashing, otherwise use default cost.
4484 $options = ($fasthash) ? array('cost' => 4) : array();
4486 $generatedhash = password_hash($password, PASSWORD_DEFAULT, $options);
4488 if ($generatedhash === false || $generatedhash === null) {
4489 throw new moodle_exception('Failed to generate password hash.');
4492 return $generatedhash;
4496 * Update password hash in user object (if necessary).
4498 * The password is updated if:
4499 * 1. The password has changed (the hash of $user->password is different
4500 * to the hash of $password).
4501 * 2. The existing hash is using an out-of-date algorithm (or the legacy
4502 * md5 algorithm).
4504 * Updating the password will modify the $user object and the database
4505 * record to use the current hashing algorithm.
4507 * @param stdClass $user User object (password property may be updated).
4508 * @param string $password Plain text password.
4509 * @return bool Always returns true.
4511 function update_internal_user_password($user, $password) {
4512 global $CFG, $DB;
4513 require_once($CFG->libdir.'/password_compat/lib/password.php');
4515 // Use the legacy hashing algorithm (md5) if PHP doesn't support
4516 // bcrypt properly.
4517 $legacyhash = password_compat_not_supported();
4519 // Figure out what the hashed password should be.
4520 $authplugin = get_auth_plugin($user->auth);
4521 if ($authplugin->prevent_local_passwords()) {
4522 $hashedpassword = AUTH_PASSWORD_NOT_CACHED;
4523 } else {
4524 $hashedpassword = hash_internal_user_password($password);
4527 if ($legacyhash) {
4528 $passwordchanged = ($user->password !== $hashedpassword);
4529 $algorithmchanged = false;
4530 } else {
4531 // If verification fails then it means the password has changed.
4532 $passwordchanged = !password_verify($password, $user->password);
4533 $algorithmchanged = password_needs_rehash($user->password, PASSWORD_DEFAULT);
4536 if ($passwordchanged || $algorithmchanged) {
4537 $DB->set_field('user', 'password', $hashedpassword, array('id'=>$user->id));
4538 $user->password = $hashedpassword;
4541 return true;
4545 * Get a complete user record, which includes all the info
4546 * in the user record.
4548 * Intended for setting as $USER session variable
4550 * @param string $field The user field to be checked for a given value.
4551 * @param string $value The value to match for $field.
4552 * @param int $mnethostid
4553 * @return mixed False, or A {@link $USER} object.
4555 function get_complete_user_data($field, $value, $mnethostid = null) {
4556 global $CFG, $DB;
4558 if (!$field || !$value) {
4559 return false;
4562 /// Build the WHERE clause for an SQL query
4563 $params = array('fieldval'=>$value);
4564 $constraints = "$field = :fieldval AND deleted <> 1";
4566 // If we are loading user data based on anything other than id,
4567 // we must also restrict our search based on mnet host.
4568 if ($field != 'id') {
4569 if (empty($mnethostid)) {
4570 // if empty, we restrict to local users
4571 $mnethostid = $CFG->mnet_localhost_id;
4574 if (!empty($mnethostid)) {
4575 $params['mnethostid'] = $mnethostid;
4576 $constraints .= " AND mnethostid = :mnethostid";
4579 /// Get all the basic user data
4581 if (! $user = $DB->get_record_select('user', $constraints, $params)) {
4582 return false;
4585 /// Get various settings and preferences
4587 // preload preference cache
4588 check_user_preferences_loaded($user);
4590 // load course enrolment related stuff
4591 $user->lastcourseaccess = array(); // during last session
4592 $user->currentcourseaccess = array(); // during current session
4593 if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid'=>$user->id))) {
4594 foreach ($lastaccesses as $lastaccess) {
4595 $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess;
4599 $sql = "SELECT g.id, g.courseid
4600 FROM {groups} g, {groups_members} gm
4601 WHERE gm.groupid=g.id AND gm.userid=?";
4603 // this is a special hack to speedup calendar display
4604 $user->groupmember = array();
4605 if (!isguestuser($user)) {
4606 if ($groups = $DB->get_records_sql($sql, array($user->id))) {
4607 foreach ($groups as $group) {
4608 if (!array_key_exists($group->courseid, $user->groupmember)) {
4609 $user->groupmember[$group->courseid] = array();
4611 $user->groupmember[$group->courseid][$group->id] = $group->id;
4616 /// Add the custom profile fields to the user record
4617 $user->profile = array();
4618 if (!isguestuser($user)) {
4619 require_once($CFG->dirroot.'/user/profile/lib.php');
4620 profile_load_custom_fields($user);
4623 /// Rewrite some variables if necessary
4624 if (!empty($user->description)) {
4625 $user->description = true; // No need to cart all of it around
4627 if (isguestuser($user)) {
4628 $user->lang = $CFG->lang; // Guest language always same as site
4629 $user->firstname = get_string('guestuser'); // Name always in current language
4630 $user->lastname = ' ';
4633 return $user;
4637 * Validate a password against the configured password policy
4639 * @global object
4640 * @param string $password the password to be checked against the password policy
4641 * @param string $errmsg the error message to display when the password doesn't comply with the policy.
4642 * @return bool true if the password is valid according to the policy. false otherwise.
4644 function check_password_policy($password, &$errmsg) {
4645 global $CFG;
4647 if (empty($CFG->passwordpolicy)) {
4648 return true;
4651 $errmsg = '';
4652 if (textlib::strlen($password) < $CFG->minpasswordlength) {
4653 $errmsg .= '<div>'. get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength) .'</div>';
4656 if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) {
4657 $errmsg .= '<div>'. get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits) .'</div>';
4660 if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) {
4661 $errmsg .= '<div>'. get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower) .'</div>';
4664 if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) {
4665 $errmsg .= '<div>'. get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper) .'</div>';
4668 if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) {
4669 $errmsg .= '<div>'. get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum) .'</div>';
4671 if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) {
4672 $errmsg .= '<div>'. get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars) .'</div>';
4675 if ($errmsg == '') {
4676 return true;
4677 } else {
4678 return false;
4684 * When logging in, this function is run to set certain preferences
4685 * for the current SESSION
4687 * @global object
4688 * @global object
4690 function set_login_session_preferences() {
4691 global $SESSION, $CFG;
4693 $SESSION->justloggedin = true;
4695 unset($SESSION->lang);
4700 * Delete a course, including all related data from the database,
4701 * and any associated files.
4703 * @global object
4704 * @global object
4705 * @param mixed $courseorid The id of the course or course object to delete.
4706 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4707 * @return bool true if all the removals succeeded. false if there were any failures. If this
4708 * method returns false, some of the removals will probably have succeeded, and others
4709 * failed, but you have no way of knowing which.
4711 function delete_course($courseorid, $showfeedback = true) {
4712 global $DB;
4714 if (is_object($courseorid)) {
4715 $courseid = $courseorid->id;
4716 $course = $courseorid;
4717 } else {
4718 $courseid = $courseorid;
4719 if (!$course = $DB->get_record('course', array('id'=>$courseid))) {
4720 return false;
4723 $context = context_course::instance($courseid);
4725 // frontpage course can not be deleted!!
4726 if ($courseid == SITEID) {
4727 return false;
4730 // make the course completely empty
4731 remove_course_contents($courseid, $showfeedback);
4733 // delete the course and related context instance
4734 delete_context(CONTEXT_COURSE, $courseid);
4736 // We will update the course's timemodified, as it will be passed to the course_deleted event,
4737 // which should know about this updated property, as this event is meant to pass the full course record
4738 $course->timemodified = time();
4740 $DB->delete_records("course", array("id" => $courseid));
4741 $DB->delete_records("course_format_options", array("courseid" => $courseid));
4743 //trigger events
4744 $course->context = $context; // you can not fetch context in the event because it was already deleted
4745 events_trigger('course_deleted', $course);
4747 return true;
4751 * Clear a course out completely, deleting all content
4752 * but don't delete the course itself.
4753 * This function does not verify any permissions.
4755 * Please note this function also deletes all user enrolments,
4756 * enrolment instances and role assignments by default.
4758 * $options:
4759 * - 'keep_roles_and_enrolments' - false by default
4760 * - 'keep_groups_and_groupings' - false by default
4762 * @param int $courseid The id of the course that is being deleted
4763 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4764 * @param array $options extra options
4765 * @return bool true if all the removals succeeded. false if there were any failures. If this
4766 * method returns false, some of the removals will probably have succeeded, and others
4767 * failed, but you have no way of knowing which.
4769 function remove_course_contents($courseid, $showfeedback = true, array $options = null) {
4770 global $CFG, $DB, $OUTPUT;
4771 require_once($CFG->libdir.'/badgeslib.php');
4772 require_once($CFG->libdir.'/completionlib.php');
4773 require_once($CFG->libdir.'/questionlib.php');
4774 require_once($CFG->libdir.'/gradelib.php');
4775 require_once($CFG->dirroot.'/group/lib.php');
4776 require_once($CFG->dirroot.'/tag/coursetagslib.php');
4777 require_once($CFG->dirroot.'/comment/lib.php');
4778 require_once($CFG->dirroot.'/rating/lib.php');
4780 // Handle course badges.
4781 badges_handle_course_deletion($courseid);
4783 // NOTE: these concatenated strings are suboptimal, but it is just extra info...
4784 $strdeleted = get_string('deleted').' - ';
4786 // Some crazy wishlist of stuff we should skip during purging of course content
4787 $options = (array)$options;
4789 $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST);
4790 $coursecontext = context_course::instance($courseid);
4791 $fs = get_file_storage();
4793 // Delete course completion information, this has to be done before grades and enrols
4794 $cc = new completion_info($course);
4795 $cc->clear_criteria();
4796 if ($showfeedback) {
4797 echo $OUTPUT->notification($strdeleted.get_string('completion', 'completion'), 'notifysuccess');
4800 // Remove all data from gradebook - this needs to be done before course modules
4801 // because while deleting this information, the system may need to reference
4802 // the course modules that own the grades.
4803 remove_course_grades($courseid, $showfeedback);
4804 remove_grade_letters($coursecontext, $showfeedback);
4806 // Delete course blocks in any all child contexts,
4807 // they may depend on modules so delete them first
4808 $childcontexts = $coursecontext->get_child_contexts(); // returns all subcontexts since 2.2
4809 foreach ($childcontexts as $childcontext) {
4810 blocks_delete_all_for_context($childcontext->id);
4812 unset($childcontexts);
4813 blocks_delete_all_for_context($coursecontext->id);
4814 if ($showfeedback) {
4815 echo $OUTPUT->notification($strdeleted.get_string('type_block_plural', 'plugin'), 'notifysuccess');
4818 // Delete every instance of every module,
4819 // this has to be done before deleting of course level stuff
4820 $locations = get_plugin_list('mod');
4821 foreach ($locations as $modname=>$moddir) {
4822 if ($modname === 'NEWMODULE') {
4823 continue;
4825 if ($module = $DB->get_record('modules', array('name'=>$modname))) {
4826 include_once("$moddir/lib.php"); // Shows php warning only if plugin defective
4827 $moddelete = $modname .'_delete_instance'; // Delete everything connected to an instance
4828 $moddeletecourse = $modname .'_delete_course'; // Delete other stray stuff (uncommon)
4830 if ($instances = $DB->get_records($modname, array('course'=>$course->id))) {
4831 foreach ($instances as $instance) {
4832 if ($cm = get_coursemodule_from_instance($modname, $instance->id, $course->id)) {
4833 /// Delete activity context questions and question categories
4834 question_delete_activity($cm, $showfeedback);
4836 if (function_exists($moddelete)) {
4837 // This purges all module data in related tables, extra user prefs, settings, etc.
4838 $moddelete($instance->id);
4839 } else {
4840 // NOTE: we should not allow installation of modules with missing delete support!
4841 debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!");
4842 $DB->delete_records($modname, array('id'=>$instance->id));
4845 if ($cm) {
4846 // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition
4847 context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4848 $DB->delete_records('course_modules', array('id'=>$cm->id));
4852 if (function_exists($moddeletecourse)) {
4853 // Execute ptional course cleanup callback
4854 $moddeletecourse($course, $showfeedback);
4856 if ($instances and $showfeedback) {
4857 echo $OUTPUT->notification($strdeleted.get_string('pluginname', $modname), 'notifysuccess');
4859 } else {
4860 // Ooops, this module is not properly installed, force-delete it in the next block
4864 // We have tried to delete everything the nice way - now let's force-delete any remaining module data
4866 // Remove all data from availability and completion tables that is associated
4867 // with course-modules belonging to this course. Note this is done even if the
4868 // features are not enabled now, in case they were enabled previously.
4869 $DB->delete_records_select('course_modules_completion',
4870 'coursemoduleid IN (SELECT id from {course_modules} WHERE course=?)',
4871 array($courseid));
4872 $DB->delete_records_select('course_modules_availability',
4873 'coursemoduleid IN (SELECT id from {course_modules} WHERE course=?)',
4874 array($courseid));
4875 $DB->delete_records_select('course_modules_avail_fields',
4876 'coursemoduleid IN (SELECT id from {course_modules} WHERE course=?)',
4877 array($courseid));
4879 // Remove course-module data.
4880 $cms = $DB->get_records('course_modules', array('course'=>$course->id));
4881 foreach ($cms as $cm) {
4882 if ($module = $DB->get_record('modules', array('id'=>$cm->module))) {
4883 try {
4884 $DB->delete_records($module->name, array('id'=>$cm->instance));
4885 } catch (Exception $e) {
4886 // Ignore weird or missing table problems
4889 context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4890 $DB->delete_records('course_modules', array('id'=>$cm->id));
4893 if ($showfeedback) {
4894 echo $OUTPUT->notification($strdeleted.get_string('type_mod_plural', 'plugin'), 'notifysuccess');
4897 // Cleanup the rest of plugins
4898 $cleanuplugintypes = array('report', 'coursereport', 'format');
4899 foreach ($cleanuplugintypes as $type) {
4900 $plugins = get_plugin_list_with_function($type, 'delete_course', 'lib.php');
4901 foreach ($plugins as $plugin=>$pluginfunction) {
4902 $pluginfunction($course->id, $showfeedback);
4904 if ($showfeedback) {
4905 echo $OUTPUT->notification($strdeleted.get_string('type_'.$type.'_plural', 'plugin'), 'notifysuccess');
4909 // Delete questions and question categories
4910 question_delete_course($course, $showfeedback);
4911 if ($showfeedback) {
4912 echo $OUTPUT->notification($strdeleted.get_string('questions', 'question'), 'notifysuccess');
4915 // Make sure there are no subcontexts left - all valid blocks and modules should be already gone
4916 $childcontexts = $coursecontext->get_child_contexts(); // returns all subcontexts since 2.2
4917 foreach ($childcontexts as $childcontext) {
4918 $childcontext->delete();
4920 unset($childcontexts);
4922 // Remove all roles and enrolments by default
4923 if (empty($options['keep_roles_and_enrolments'])) {
4924 // this hack is used in restore when deleting contents of existing course
4925 role_unassign_all(array('contextid'=>$coursecontext->id, 'component'=>''), true);
4926 enrol_course_delete($course);
4927 if ($showfeedback) {
4928 echo $OUTPUT->notification($strdeleted.get_string('type_enrol_plural', 'plugin'), 'notifysuccess');
4932 // Delete any groups, removing members and grouping/course links first.
4933 if (empty($options['keep_groups_and_groupings'])) {
4934 groups_delete_groupings($course->id, $showfeedback);
4935 groups_delete_groups($course->id, $showfeedback);
4938 // filters be gone!
4939 filter_delete_all_for_context($coursecontext->id);
4941 // die comments!
4942 comment::delete_comments($coursecontext->id);
4944 // ratings are history too
4945 $delopt = new stdclass();
4946 $delopt->contextid = $coursecontext->id;
4947 $rm = new rating_manager();
4948 $rm->delete_ratings($delopt);
4950 // Delete course tags
4951 coursetag_delete_course_tags($course->id, $showfeedback);
4953 // Delete calendar events
4954 $DB->delete_records('event', array('courseid'=>$course->id));
4955 $fs->delete_area_files($coursecontext->id, 'calendar');
4957 // Delete all related records in other core tables that may have a courseid
4958 // This array stores the tables that need to be cleared, as
4959 // table_name => column_name that contains the course id.
4960 $tablestoclear = array(
4961 'log' => 'course', // Course logs (NOTE: this might be changed in the future)
4962 'backup_courses' => 'courseid', // Scheduled backup stuff
4963 'user_lastaccess' => 'courseid', // User access info
4965 foreach ($tablestoclear as $table => $col) {
4966 $DB->delete_records($table, array($col=>$course->id));
4969 // delete all course backup files
4970 $fs->delete_area_files($coursecontext->id, 'backup');
4972 // cleanup course record - remove links to deleted stuff
4973 $oldcourse = new stdClass();
4974 $oldcourse->id = $course->id;
4975 $oldcourse->summary = '';
4976 $oldcourse->modinfo = NULL;
4977 $oldcourse->legacyfiles = 0;
4978 $oldcourse->enablecompletion = 0;
4979 if (!empty($options['keep_groups_and_groupings'])) {
4980 $oldcourse->defaultgroupingid = 0;
4982 $DB->update_record('course', $oldcourse);
4984 // Delete course sections and availability options.
4985 $DB->delete_records_select('course_sections_availability',
4986 'coursesectionid IN (SELECT id from {course_sections} WHERE course=?)',
4987 array($course->id));
4988 $DB->delete_records_select('course_sections_avail_fields',
4989 'coursesectionid IN (SELECT id from {course_sections} WHERE course=?)',
4990 array($course->id));
4991 $DB->delete_records('course_sections', array('course'=>$course->id));
4993 // delete legacy, section and any other course files
4994 $fs->delete_area_files($coursecontext->id, 'course'); // files from summary and section
4996 // Delete all remaining stuff linked to context such as files, comments, ratings, etc.
4997 if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) {
4998 // Easy, do not delete the context itself...
4999 $coursecontext->delete_content();
5001 } else {
5002 // Hack alert!!!!
5003 // We can not drop all context stuff because it would bork enrolments and roles,
5004 // there might be also files used by enrol plugins...
5007 // Delete legacy files - just in case some files are still left there after conversion to new file api,
5008 // also some non-standard unsupported plugins may try to store something there
5009 fulldelete($CFG->dataroot.'/'.$course->id);
5011 // Finally trigger the event
5012 $course->context = $coursecontext; // you can not access context in cron event later after course is deleted
5013 $course->options = $options; // not empty if we used any crazy hack
5014 events_trigger('course_content_removed', $course);
5016 return true;
5020 * Change dates in module - used from course reset.
5022 * @global object
5023 * @global object
5024 * @param string $modname forum, assignment, etc
5025 * @param array $fields array of date fields from mod table
5026 * @param int $timeshift time difference
5027 * @param int $courseid
5028 * @return bool success
5030 function shift_course_mod_dates($modname, $fields, $timeshift, $courseid) {
5031 global $CFG, $DB;
5032 include_once($CFG->dirroot.'/mod/'.$modname.'/lib.php');
5034 $return = true;
5035 foreach ($fields as $field) {
5036 $updatesql = "UPDATE {".$modname."}
5037 SET $field = $field + ?
5038 WHERE course=? AND $field<>0";
5039 $return = $DB->execute($updatesql, array($timeshift, $courseid)) && $return;
5042 $refreshfunction = $modname.'_refresh_events';
5043 if (function_exists($refreshfunction)) {
5044 $refreshfunction($courseid);
5047 return $return;
5051 * This function will empty a course of user data.
5052 * It will retain the activities and the structure of the course.
5054 * @param object $data an object containing all the settings including courseid (without magic quotes)
5055 * @return array status array of array component, item, error
5057 function reset_course_userdata($data) {
5058 global $CFG, $USER, $DB;
5059 require_once($CFG->libdir.'/gradelib.php');
5060 require_once($CFG->libdir.'/completionlib.php');
5061 require_once($CFG->dirroot.'/group/lib.php');
5063 $data->courseid = $data->id;
5064 $context = context_course::instance($data->courseid);
5066 // calculate the time shift of dates
5067 if (!empty($data->reset_start_date)) {
5068 // time part of course startdate should be zero
5069 $data->timeshift = $data->reset_start_date - usergetmidnight($data->reset_start_date_old);
5070 } else {
5071 $data->timeshift = 0;
5074 // result array: component, item, error
5075 $status = array();
5077 // start the resetting
5078 $componentstr = get_string('general');
5080 // move the course start time
5081 if (!empty($data->reset_start_date) and $data->timeshift) {
5082 // change course start data
5083 $DB->set_field('course', 'startdate', $data->reset_start_date, array('id'=>$data->courseid));
5084 // update all course and group events - do not move activity events
5085 $updatesql = "UPDATE {event}
5086 SET timestart = timestart + ?
5087 WHERE courseid=? AND instance=0";
5088 $DB->execute($updatesql, array($data->timeshift, $data->courseid));
5090 $status[] = array('component'=>$componentstr, 'item'=>get_string('datechanged'), 'error'=>false);
5093 if (!empty($data->reset_logs)) {
5094 $DB->delete_records('log', array('course'=>$data->courseid));
5095 $status[] = array('component'=>$componentstr, 'item'=>get_string('deletelogs'), 'error'=>false);
5098 if (!empty($data->reset_events)) {
5099 $DB->delete_records('event', array('courseid'=>$data->courseid));
5100 $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteevents', 'calendar'), 'error'=>false);
5103 if (!empty($data->reset_notes)) {
5104 require_once($CFG->dirroot.'/notes/lib.php');
5105 note_delete_all($data->courseid);
5106 $status[] = array('component'=>$componentstr, 'item'=>get_string('deletenotes', 'notes'), 'error'=>false);
5109 if (!empty($data->delete_blog_associations)) {
5110 require_once($CFG->dirroot.'/blog/lib.php');
5111 blog_remove_associations_for_course($data->courseid);
5112 $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteblogassociations', 'blog'), 'error'=>false);
5115 if (!empty($data->reset_completion)) {
5116 // Delete course and activity completion information.
5117 $course = $DB->get_record('course', array('id'=>$data->courseid));
5118 $cc = new completion_info($course);
5119 $cc->delete_all_completion_data();
5120 $status[] = array('component' => $componentstr,
5121 'item' => get_string('deletecompletiondata', 'completion'), 'error' => false);
5124 $componentstr = get_string('roles');
5126 if (!empty($data->reset_roles_overrides)) {
5127 $children = get_child_contexts($context);
5128 foreach ($children as $child) {
5129 $DB->delete_records('role_capabilities', array('contextid'=>$child->id));
5131 $DB->delete_records('role_capabilities', array('contextid'=>$context->id));
5132 //force refresh for logged in users
5133 mark_context_dirty($context->path);
5134 $status[] = array('component'=>$componentstr, 'item'=>get_string('deletecourseoverrides', 'role'), 'error'=>false);
5137 if (!empty($data->reset_roles_local)) {
5138 $children = get_child_contexts($context);
5139 foreach ($children as $child) {
5140 role_unassign_all(array('contextid'=>$child->id));
5142 //force refresh for logged in users
5143 mark_context_dirty($context->path);
5144 $status[] = array('component'=>$componentstr, 'item'=>get_string('deletelocalroles', 'role'), 'error'=>false);
5147 // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc.
5148 $data->unenrolled = array();
5149 if (!empty($data->unenrol_users)) {
5150 $plugins = enrol_get_plugins(true);
5151 $instances = enrol_get_instances($data->courseid, true);
5152 foreach ($instances as $key=>$instance) {
5153 if (!isset($plugins[$instance->enrol])) {
5154 unset($instances[$key]);
5155 continue;
5159 foreach($data->unenrol_users as $withroleid) {
5160 if ($withroleid) {
5161 $sql = "SELECT ue.*
5162 FROM {user_enrolments} ue
5163 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5164 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5165 JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)";
5166 $params = array('courseid'=>$data->courseid, 'roleid'=>$withroleid, 'courselevel'=>CONTEXT_COURSE);
5168 } else {
5169 // without any role assigned at course context
5170 $sql = "SELECT ue.*
5171 FROM {user_enrolments} ue
5172 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5173 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5174 LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid)
5175 WHERE ra.id IS NULL";
5176 $params = array('courseid'=>$data->courseid, 'courselevel'=>CONTEXT_COURSE);
5179 $rs = $DB->get_recordset_sql($sql, $params);
5180 foreach ($rs as $ue) {
5181 if (!isset($instances[$ue->enrolid])) {
5182 continue;
5184 $instance = $instances[$ue->enrolid];
5185 $plugin = $plugins[$instance->enrol];
5186 if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) {
5187 continue;
5190 $plugin->unenrol_user($instance, $ue->userid);
5191 $data->unenrolled[$ue->userid] = $ue->userid;
5193 $rs->close();
5196 if (!empty($data->unenrolled)) {
5197 $status[] = array('component'=>$componentstr, 'item'=>get_string('unenrol', 'enrol').' ('.count($data->unenrolled).')', 'error'=>false);
5201 $componentstr = get_string('groups');
5203 // remove all group members
5204 if (!empty($data->reset_groups_members)) {
5205 groups_delete_group_members($data->courseid);
5206 $status[] = array('component'=>$componentstr, 'item'=>get_string('removegroupsmembers', 'group'), 'error'=>false);
5209 // remove all groups
5210 if (!empty($data->reset_groups_remove)) {
5211 groups_delete_groups($data->courseid, false);
5212 $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteallgroups', 'group'), 'error'=>false);
5215 // remove all grouping members
5216 if (!empty($data->reset_groupings_members)) {
5217 groups_delete_groupings_groups($data->courseid, false);
5218 $status[] = array('component'=>$componentstr, 'item'=>get_string('removegroupingsmembers', 'group'), 'error'=>false);
5221 // remove all groupings
5222 if (!empty($data->reset_groupings_remove)) {
5223 groups_delete_groupings($data->courseid, false);
5224 $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteallgroupings', 'group'), 'error'=>false);
5227 // Look in every instance of every module for data to delete
5228 $unsupported_mods = array();
5229 if ($allmods = $DB->get_records('modules') ) {
5230 foreach ($allmods as $mod) {
5231 $modname = $mod->name;
5232 $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php';
5233 $moddeleteuserdata = $modname.'_reset_userdata'; // Function to delete user data
5234 if (file_exists($modfile)) {
5235 if (!$DB->count_records($modname, array('course'=>$data->courseid))) {
5236 continue; // Skip mods with no instances
5238 include_once($modfile);
5239 if (function_exists($moddeleteuserdata)) {
5240 $modstatus = $moddeleteuserdata($data);
5241 if (is_array($modstatus)) {
5242 $status = array_merge($status, $modstatus);
5243 } else {
5244 debugging('Module '.$modname.' returned incorrect staus - must be an array!');
5246 } else {
5247 $unsupported_mods[] = $mod;
5249 } else {
5250 debugging('Missing lib.php in '.$modname.' module!');
5255 // mention unsupported mods
5256 if (!empty($unsupported_mods)) {
5257 foreach($unsupported_mods as $mod) {
5258 $status[] = array('component'=>get_string('modulenameplural', $mod->name), 'item'=>'', 'error'=>get_string('resetnotimplemented'));
5263 $componentstr = get_string('gradebook', 'grades');
5264 // reset gradebook
5265 if (!empty($data->reset_gradebook_items)) {
5266 remove_course_grades($data->courseid, false);
5267 grade_grab_course_grades($data->courseid);
5268 grade_regrade_final_grades($data->courseid);
5269 $status[] = array('component'=>$componentstr, 'item'=>get_string('removeallcourseitems', 'grades'), 'error'=>false);
5271 } else if (!empty($data->reset_gradebook_grades)) {
5272 grade_course_reset($data->courseid);
5273 $status[] = array('component'=>$componentstr, 'item'=>get_string('removeallcoursegrades', 'grades'), 'error'=>false);
5275 // reset comments
5276 if (!empty($data->reset_comments)) {
5277 require_once($CFG->dirroot.'/comment/lib.php');
5278 comment::reset_course_page_comments($context);
5281 return $status;
5285 * Generate an email processing address
5287 * @param int $modid
5288 * @param string $modargs
5289 * @return string Returns email processing address
5291 function generate_email_processing_address($modid,$modargs) {
5292 global $CFG;
5294 $header = $CFG->mailprefix . substr(base64_encode(pack('C',$modid)),0,2).$modargs;
5295 return $header . substr(md5($header.get_site_identifier()),0,16).'@'.$CFG->maildomain;
5301 * @todo Finish documenting this function
5303 * @global object
5304 * @param string $modargs
5305 * @param string $body Currently unused
5307 function moodle_process_email($modargs,$body) {
5308 global $DB;
5310 // the first char should be an unencoded letter. We'll take this as an action
5311 switch ($modargs{0}) {
5312 case 'B': { // bounce
5313 list(,$userid) = unpack('V',base64_decode(substr($modargs,1,8)));
5314 if ($user = $DB->get_record("user", array('id'=>$userid), "id,email")) {
5315 // check the half md5 of their email
5316 $md5check = substr(md5($user->email),0,16);
5317 if ($md5check == substr($modargs, -16)) {
5318 set_bounce_count($user);
5320 // else maybe they've already changed it?
5323 break;
5324 // maybe more later?
5328 /// CORRESPONDENCE ////////////////////////////////////////////////
5331 * Get mailer instance, enable buffering, flush buffer or disable buffering.
5333 * @param string $action 'get', 'buffer', 'close' or 'flush'
5334 * @return moodle_phpmailer|null mailer instance if 'get' used or nothing
5336 function get_mailer($action='get') {
5337 global $CFG;
5339 static $mailer = null;
5340 static $counter = 0;
5342 if (!isset($CFG->smtpmaxbulk)) {
5343 $CFG->smtpmaxbulk = 1;
5346 if ($action == 'get') {
5347 $prevkeepalive = false;
5349 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5350 if ($counter < $CFG->smtpmaxbulk and !$mailer->IsError()) {
5351 $counter++;
5352 // reset the mailer
5353 $mailer->Priority = 3;
5354 $mailer->CharSet = 'UTF-8'; // our default
5355 $mailer->ContentType = "text/plain";
5356 $mailer->Encoding = "8bit";
5357 $mailer->From = "root@localhost";
5358 $mailer->FromName = "Root User";
5359 $mailer->Sender = "";
5360 $mailer->Subject = "";
5361 $mailer->Body = "";
5362 $mailer->AltBody = "";
5363 $mailer->ConfirmReadingTo = "";
5365 $mailer->ClearAllRecipients();
5366 $mailer->ClearReplyTos();
5367 $mailer->ClearAttachments();
5368 $mailer->ClearCustomHeaders();
5369 return $mailer;
5372 $prevkeepalive = $mailer->SMTPKeepAlive;
5373 get_mailer('flush');
5376 include_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php');
5377 $mailer = new moodle_phpmailer();
5379 $counter = 1;
5381 $mailer->Version = 'Moodle '.$CFG->version; // mailer version
5382 $mailer->PluginDir = $CFG->libdir.'/phpmailer/'; // plugin directory (eg smtp plugin)
5383 $mailer->CharSet = 'UTF-8';
5385 // some MTAs may do double conversion of LF if CRLF used, CRLF is required line ending in RFC 822bis
5386 if (isset($CFG->mailnewline) and $CFG->mailnewline == 'CRLF') {
5387 $mailer->LE = "\r\n";
5388 } else {
5389 $mailer->LE = "\n";
5392 if ($CFG->smtphosts == 'qmail') {
5393 $mailer->IsQmail(); // use Qmail system
5395 } else if (empty($CFG->smtphosts)) {
5396 $mailer->IsMail(); // use PHP mail() = sendmail
5398 } else {
5399 $mailer->IsSMTP(); // use SMTP directly
5400 if (!empty($CFG->debugsmtp)) {
5401 $mailer->SMTPDebug = true;
5403 $mailer->Host = $CFG->smtphosts; // specify main and backup servers
5404 $mailer->SMTPSecure = $CFG->smtpsecure; // specify secure connection protocol
5405 $mailer->SMTPKeepAlive = $prevkeepalive; // use previous keepalive
5407 if ($CFG->smtpuser) { // Use SMTP authentication
5408 $mailer->SMTPAuth = true;
5409 $mailer->Username = $CFG->smtpuser;
5410 $mailer->Password = $CFG->smtppass;
5414 return $mailer;
5417 $nothing = null;
5419 // keep smtp session open after sending
5420 if ($action == 'buffer') {
5421 if (!empty($CFG->smtpmaxbulk)) {
5422 get_mailer('flush');
5423 $m = get_mailer();
5424 if ($m->Mailer == 'smtp') {
5425 $m->SMTPKeepAlive = true;
5428 return $nothing;
5431 // close smtp session, but continue buffering
5432 if ($action == 'flush') {
5433 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5434 if (!empty($mailer->SMTPDebug)) {
5435 echo '<pre>'."\n";
5437 $mailer->SmtpClose();
5438 if (!empty($mailer->SMTPDebug)) {
5439 echo '</pre>';
5442 return $nothing;
5445 // close smtp session, do not buffer anymore
5446 if ($action == 'close') {
5447 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5448 get_mailer('flush');
5449 $mailer->SMTPKeepAlive = false;
5451 $mailer = null; // better force new instance
5452 return $nothing;
5457 * Send an email to a specified user
5459 * @global object
5460 * @global string
5461 * @global string IdentityProvider(IDP) URL user hits to jump to mnet peer.
5462 * @uses SITEID
5463 * @param stdClass $user A {@link $USER} object
5464 * @param stdClass $from A {@link $USER} object
5465 * @param string $subject plain text subject line of the email
5466 * @param string $messagetext plain text version of the message
5467 * @param string $messagehtml complete html version of the message (optional)
5468 * @param string $attachment a file on the filesystem, relative to $CFG->dataroot
5469 * @param string $attachname the name of the file (extension indicates MIME)
5470 * @param bool $usetrueaddress determines whether $from email address should
5471 * be sent out. Will be overruled by user profile setting for maildisplay
5472 * @param string $replyto Email address to reply to
5473 * @param string $replytoname Name of reply to recipient
5474 * @param int $wordwrapwidth custom word wrap width, default 79
5475 * @return bool Returns true if mail was sent OK and false if there was an error.
5477 function email_to_user($user, $from, $subject, $messagetext, $messagehtml='', $attachment='', $attachname='', $usetrueaddress=true, $replyto='', $replytoname='', $wordwrapwidth=79) {
5479 global $CFG;
5481 if (empty($user) || empty($user->email)) {
5482 $nulluser = 'User is null or has no email';
5483 error_log($nulluser);
5484 if (CLI_SCRIPT) {
5485 mtrace('Error: lib/moodlelib.php email_to_user(): '.$nulluser);
5487 return false;
5490 if (!empty($user->deleted)) {
5491 // do not mail deleted users
5492 $userdeleted = 'User is deleted';
5493 error_log($userdeleted);
5494 if (CLI_SCRIPT) {
5495 mtrace('Error: lib/moodlelib.php email_to_user(): '.$userdeleted);
5497 return false;
5500 if (!empty($CFG->noemailever)) {
5501 // hidden setting for development sites, set in config.php if needed
5502 $noemail = 'Not sending email due to noemailever config setting';
5503 error_log($noemail);
5504 if (CLI_SCRIPT) {
5505 mtrace('Error: lib/moodlelib.php email_to_user(): '.$noemail);
5507 return true;
5510 if (!empty($CFG->divertallemailsto)) {
5511 $subject = "[DIVERTED {$user->email}] $subject";
5512 $user = clone($user);
5513 $user->email = $CFG->divertallemailsto;
5516 // skip mail to suspended users
5517 if ((isset($user->auth) && $user->auth=='nologin') or (isset($user->suspended) && $user->suspended)) {
5518 return true;
5521 if (!validate_email($user->email)) {
5522 // we can not send emails to invalid addresses - it might create security issue or confuse the mailer
5523 $invalidemail = "User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending.";
5524 error_log($invalidemail);
5525 if (CLI_SCRIPT) {
5526 mtrace('Error: lib/moodlelib.php email_to_user(): '.$invalidemail);
5528 return false;
5531 if (over_bounce_threshold($user)) {
5532 $bouncemsg = "User $user->id (".fullname($user).") is over bounce threshold! Not sending.";
5533 error_log($bouncemsg);
5534 if (CLI_SCRIPT) {
5535 mtrace('Error: lib/moodlelib.php email_to_user(): '.$bouncemsg);
5537 return false;
5540 // If the user is a remote mnet user, parse the email text for URL to the
5541 // wwwroot and modify the url to direct the user's browser to login at their
5542 // home site (identity provider - idp) before hitting the link itself
5543 if (is_mnet_remote_user($user)) {
5544 require_once($CFG->dirroot.'/mnet/lib.php');
5546 $jumpurl = mnet_get_idp_jump_url($user);
5547 $callback = partial('mnet_sso_apply_indirection', $jumpurl);
5549 $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%",
5550 $callback,
5551 $messagetext);
5552 $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%",
5553 $callback,
5554 $messagehtml);
5556 $mail = get_mailer();
5558 if (!empty($mail->SMTPDebug)) {
5559 echo '<pre>' . "\n";
5562 $temprecipients = array();
5563 $tempreplyto = array();
5565 $supportuser = generate_email_supportuser();
5567 // make up an email address for handling bounces
5568 if (!empty($CFG->handlebounces)) {
5569 $modargs = 'B'.base64_encode(pack('V',$user->id)).substr(md5($user->email),0,16);
5570 $mail->Sender = generate_email_processing_address(0,$modargs);
5571 } else {
5572 $mail->Sender = $supportuser->email;
5575 if (is_string($from)) { // So we can pass whatever we want if there is need
5576 $mail->From = $CFG->noreplyaddress;
5577 $mail->FromName = $from;
5578 } else if ($usetrueaddress and $from->maildisplay) {
5579 $mail->From = $from->email;
5580 $mail->FromName = fullname($from);
5581 } else {
5582 $mail->From = $CFG->noreplyaddress;
5583 $mail->FromName = fullname($from);
5584 if (empty($replyto)) {
5585 $tempreplyto[] = array($CFG->noreplyaddress, get_string('noreplyname'));
5589 if (!empty($replyto)) {
5590 $tempreplyto[] = array($replyto, $replytoname);
5593 $mail->Subject = substr($subject, 0, 900);
5595 $temprecipients[] = array($user->email, fullname($user));
5597 $mail->WordWrap = $wordwrapwidth; // set word wrap
5599 if (!empty($from->customheaders)) { // Add custom headers
5600 if (is_array($from->customheaders)) {
5601 foreach ($from->customheaders as $customheader) {
5602 $mail->AddCustomHeader($customheader);
5604 } else {
5605 $mail->AddCustomHeader($from->customheaders);
5609 if (!empty($from->priority)) {
5610 $mail->Priority = $from->priority;
5613 if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) { // Don't ever send HTML to users who don't want it
5614 $mail->IsHTML(true);
5615 $mail->Encoding = 'quoted-printable'; // Encoding to use
5616 $mail->Body = $messagehtml;
5617 $mail->AltBody = "\n$messagetext\n";
5618 } else {
5619 $mail->IsHTML(false);
5620 $mail->Body = "\n$messagetext\n";
5623 if ($attachment && $attachname) {
5624 if (preg_match( "~\\.\\.~" ,$attachment )) { // Security check for ".." in dir path
5625 $temprecipients[] = array($supportuser->email, fullname($supportuser, true));
5626 $mail->AddStringAttachment('Error in attachment. User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain');
5627 } else {
5628 require_once($CFG->libdir.'/filelib.php');
5629 $mimetype = mimeinfo('type', $attachname);
5630 $mail->AddAttachment($CFG->dataroot .'/'. $attachment, $attachname, 'base64', $mimetype);
5634 // Check if the email should be sent in an other charset then the default UTF-8
5635 if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) {
5637 // use the defined site mail charset or eventually the one preferred by the recipient
5638 $charset = $CFG->sitemailcharset;
5639 if (!empty($CFG->allowusermailcharset)) {
5640 if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) {
5641 $charset = $useremailcharset;
5645 // convert all the necessary strings if the charset is supported
5646 $charsets = get_list_of_charsets();
5647 unset($charsets['UTF-8']);
5648 if (in_array($charset, $charsets)) {
5649 $mail->CharSet = $charset;
5650 $mail->FromName = textlib::convert($mail->FromName, 'utf-8', strtolower($charset));
5651 $mail->Subject = textlib::convert($mail->Subject, 'utf-8', strtolower($charset));
5652 $mail->Body = textlib::convert($mail->Body, 'utf-8', strtolower($charset));
5653 $mail->AltBody = textlib::convert($mail->AltBody, 'utf-8', strtolower($charset));
5655 foreach ($temprecipients as $key => $values) {
5656 $temprecipients[$key][1] = textlib::convert($values[1], 'utf-8', strtolower($charset));
5658 foreach ($tempreplyto as $key => $values) {
5659 $tempreplyto[$key][1] = textlib::convert($values[1], 'utf-8', strtolower($charset));
5664 foreach ($temprecipients as $values) {
5665 $mail->AddAddress($values[0], $values[1]);
5667 foreach ($tempreplyto as $values) {
5668 $mail->AddReplyTo($values[0], $values[1]);
5671 if ($mail->Send()) {
5672 set_send_count($user);
5673 if (!empty($mail->SMTPDebug)) {
5674 echo '</pre>';
5676 return true;
5677 } else {
5678 add_to_log(SITEID, 'library', 'mailer', qualified_me(), 'ERROR: '. $mail->ErrorInfo);
5679 if (CLI_SCRIPT) {
5680 mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo);
5682 if (!empty($mail->SMTPDebug)) {
5683 echo '</pre>';
5685 return false;
5690 * Generate a signoff for emails based on support settings
5692 * @global object
5693 * @return string
5695 function generate_email_signoff() {
5696 global $CFG;
5698 $signoff = "\n";
5699 if (!empty($CFG->supportname)) {
5700 $signoff .= $CFG->supportname."\n";
5702 if (!empty($CFG->supportemail)) {
5703 $signoff .= $CFG->supportemail."\n";
5705 if (!empty($CFG->supportpage)) {
5706 $signoff .= $CFG->supportpage."\n";
5708 return $signoff;
5712 * Generate a fake user for emails based on support settings
5713 * @global object
5714 * @return object user info
5716 function generate_email_supportuser() {
5717 global $CFG;
5719 static $supportuser;
5721 if (!empty($supportuser)) {
5722 return $supportuser;
5725 $supportuser = new stdClass();
5726 $supportuser->email = $CFG->supportemail ? $CFG->supportemail : $CFG->noreplyaddress;
5727 $supportuser->firstname = $CFG->supportname ? $CFG->supportname : get_string('noreplyname');
5728 $supportuser->lastname = '';
5729 $supportuser->maildisplay = true;
5731 return $supportuser;
5736 * Sets specified user's password and send the new password to the user via email.
5738 * @global object
5739 * @global object
5740 * @param user $user A {@link $USER} object
5741 * @param boolean $fasthash If true, use a low cost factor when generating the hash for speed.
5742 * @return boolean|string Returns "true" if mail was sent OK and "false" if there was an error
5744 function setnew_password_and_mail($user, $fasthash = false) {
5745 global $CFG, $DB;
5747 // we try to send the mail in language the user understands,
5748 // unfortunately the filter_string() does not support alternative langs yet
5749 // so multilang will not work properly for site->fullname
5750 $lang = empty($user->lang) ? $CFG->lang : $user->lang;
5752 $site = get_site();
5754 $supportuser = generate_email_supportuser();
5756 $newpassword = generate_password();
5758 $hashedpassword = hash_internal_user_password($newpassword, $fasthash);
5759 $DB->set_field('user', 'password', $hashedpassword, array('id'=>$user->id));
5761 $a = new stdClass();
5762 $a->firstname = fullname($user, true);
5763 $a->sitename = format_string($site->fullname);
5764 $a->username = $user->username;
5765 $a->newpassword = $newpassword;
5766 $a->link = $CFG->wwwroot .'/login/';
5767 $a->signoff = generate_email_signoff();
5769 $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang);
5771 $subject = format_string($site->fullname) .': '. (string)new lang_string('newusernewpasswordsubj', '', $a, $lang);
5773 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5774 return email_to_user($user, $supportuser, $subject, $message);
5779 * Resets specified user's password and send the new password to the user via email.
5781 * @param stdClass $user A {@link $USER} object
5782 * @return bool Returns true if mail was sent OK and false if there was an error.
5784 function reset_password_and_mail($user) {
5785 global $CFG;
5787 $site = get_site();
5788 $supportuser = generate_email_supportuser();
5790 $userauth = get_auth_plugin($user->auth);
5791 if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) {
5792 trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth.");
5793 return false;
5796 $newpassword = generate_password();
5798 if (!$userauth->user_update_password($user, $newpassword)) {
5799 print_error("cannotsetpassword");
5802 $a = new stdClass();
5803 $a->firstname = $user->firstname;
5804 $a->lastname = $user->lastname;
5805 $a->sitename = format_string($site->fullname);
5806 $a->username = $user->username;
5807 $a->newpassword = $newpassword;
5808 $a->link = $CFG->httpswwwroot .'/login/change_password.php';
5809 $a->signoff = generate_email_signoff();
5811 $message = get_string('newpasswordtext', '', $a);
5813 $subject = format_string($site->fullname) .': '. get_string('changedpassword');
5815 unset_user_preference('create_password', $user); // prevent cron from generating the password
5817 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5818 return email_to_user($user, $supportuser, $subject, $message);
5823 * Send email to specified user with confirmation text and activation link.
5825 * @global object
5826 * @param user $user A {@link $USER} object
5827 * @return bool Returns true if mail was sent OK and false if there was an error.
5829 function send_confirmation_email($user) {
5830 global $CFG;
5832 $site = get_site();
5833 $supportuser = generate_email_supportuser();
5835 $data = new stdClass();
5836 $data->firstname = fullname($user);
5837 $data->sitename = format_string($site->fullname);
5838 $data->admin = generate_email_signoff();
5840 $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname));
5842 $username = urlencode($user->username);
5843 $username = str_replace('.', '%2E', $username); // prevent problems with trailing dots
5844 $data->link = $CFG->wwwroot .'/login/confirm.php?data='. $user->secret .'/'. $username;
5845 $message = get_string('emailconfirmation', '', $data);
5846 $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true);
5848 $user->mailformat = 1; // Always send HTML version as well
5850 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5851 return email_to_user($user, $supportuser, $subject, $message, $messagehtml);
5856 * send_password_change_confirmation_email.
5858 * @global object
5859 * @param user $user A {@link $USER} object
5860 * @return bool Returns true if mail was sent OK and false if there was an error.
5862 function send_password_change_confirmation_email($user) {
5863 global $CFG;
5865 $site = get_site();
5866 $supportuser = generate_email_supportuser();
5868 $data = new stdClass();
5869 $data->firstname = $user->firstname;
5870 $data->lastname = $user->lastname;
5871 $data->sitename = format_string($site->fullname);
5872 $data->link = $CFG->httpswwwroot .'/login/forgot_password.php?p='. $user->secret .'&s='. urlencode($user->username);
5873 $data->admin = generate_email_signoff();
5875 $message = get_string('emailpasswordconfirmation', '', $data);
5876 $subject = get_string('emailpasswordconfirmationsubject', '', format_string($site->fullname));
5878 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5879 return email_to_user($user, $supportuser, $subject, $message);
5884 * send_password_change_info.
5886 * @global object
5887 * @param user $user A {@link $USER} object
5888 * @return bool Returns true if mail was sent OK and false if there was an error.
5890 function send_password_change_info($user) {
5891 global $CFG;
5893 $site = get_site();
5894 $supportuser = generate_email_supportuser();
5895 $systemcontext = context_system::instance();
5897 $data = new stdClass();
5898 $data->firstname = $user->firstname;
5899 $data->lastname = $user->lastname;
5900 $data->sitename = format_string($site->fullname);
5901 $data->admin = generate_email_signoff();
5903 $userauth = get_auth_plugin($user->auth);
5905 if (!is_enabled_auth($user->auth) or $user->auth == 'nologin') {
5906 $message = get_string('emailpasswordchangeinfodisabled', '', $data);
5907 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
5908 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5909 return email_to_user($user, $supportuser, $subject, $message);
5912 if ($userauth->can_change_password() and $userauth->change_password_url()) {
5913 // we have some external url for password changing
5914 $data->link .= $userauth->change_password_url();
5916 } else {
5917 //no way to change password, sorry
5918 $data->link = '';
5921 if (!empty($data->link) and has_capability('moodle/user:changeownpassword', $systemcontext, $user->id)) {
5922 $message = get_string('emailpasswordchangeinfo', '', $data);
5923 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
5924 } else {
5925 $message = get_string('emailpasswordchangeinfofail', '', $data);
5926 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
5929 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5930 return email_to_user($user, $supportuser, $subject, $message);
5935 * Check that an email is allowed. It returns an error message if there
5936 * was a problem.
5938 * @global object
5939 * @param string $email Content of email
5940 * @return string|false
5942 function email_is_not_allowed($email) {
5943 global $CFG;
5945 if (!empty($CFG->allowemailaddresses)) {
5946 $allowed = explode(' ', $CFG->allowemailaddresses);
5947 foreach ($allowed as $allowedpattern) {
5948 $allowedpattern = trim($allowedpattern);
5949 if (!$allowedpattern) {
5950 continue;
5952 if (strpos($allowedpattern, '.') === 0) {
5953 if (strpos(strrev($email), strrev($allowedpattern)) === 0) {
5954 // subdomains are in a form ".example.com" - matches "xxx@anything.example.com"
5955 return false;
5958 } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) { // Match! (bug 5250)
5959 return false;
5962 return get_string('emailonlyallowed', '', $CFG->allowemailaddresses);
5964 } else if (!empty($CFG->denyemailaddresses)) {
5965 $denied = explode(' ', $CFG->denyemailaddresses);
5966 foreach ($denied as $deniedpattern) {
5967 $deniedpattern = trim($deniedpattern);
5968 if (!$deniedpattern) {
5969 continue;
5971 if (strpos($deniedpattern, '.') === 0) {
5972 if (strpos(strrev($email), strrev($deniedpattern)) === 0) {
5973 // subdomains are in a form ".example.com" - matches "xxx@anything.example.com"
5974 return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
5977 } else if (strpos(strrev($email), strrev('@'.$deniedpattern)) === 0) { // Match! (bug 5250)
5978 return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
5983 return false;
5986 /// FILE HANDLING /////////////////////////////////////////////
5989 * Returns local file storage instance
5991 * @return file_storage
5993 function get_file_storage() {
5994 global $CFG;
5996 static $fs = null;
5998 if ($fs) {
5999 return $fs;
6002 require_once("$CFG->libdir/filelib.php");
6004 if (isset($CFG->filedir)) {
6005 $filedir = $CFG->filedir;
6006 } else {
6007 $filedir = $CFG->dataroot.'/filedir';
6010 if (isset($CFG->trashdir)) {
6011 $trashdirdir = $CFG->trashdir;
6012 } else {
6013 $trashdirdir = $CFG->dataroot.'/trashdir';
6016 $fs = new file_storage($filedir, $trashdirdir, "$CFG->tempdir/filestorage", $CFG->directorypermissions, $CFG->filepermissions);
6018 return $fs;
6022 * Returns local file storage instance
6024 * @return file_browser
6026 function get_file_browser() {
6027 global $CFG;
6029 static $fb = null;
6031 if ($fb) {
6032 return $fb;
6035 require_once("$CFG->libdir/filelib.php");
6037 $fb = new file_browser();
6039 return $fb;
6043 * Returns file packer
6045 * @param string $mimetype default application/zip
6046 * @return file_packer
6048 function get_file_packer($mimetype='application/zip') {
6049 global $CFG;
6051 static $fp = array();
6053 if (isset($fp[$mimetype])) {
6054 return $fp[$mimetype];
6057 switch ($mimetype) {
6058 case 'application/zip':
6059 case 'application/vnd.moodle.backup':
6060 $classname = 'zip_packer';
6061 break;
6062 case 'application/x-tar':
6063 // $classname = 'tar_packer';
6064 // break;
6065 default:
6066 return false;
6069 require_once("$CFG->libdir/filestorage/$classname.php");
6070 $fp[$mimetype] = new $classname();
6072 return $fp[$mimetype];
6076 * Returns current name of file on disk if it exists.
6078 * @param string $newfile File to be verified
6079 * @return string Current name of file on disk if true
6081 function valid_uploaded_file($newfile) {
6082 if (empty($newfile)) {
6083 return '';
6085 if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) {
6086 return $newfile['tmp_name'];
6087 } else {
6088 return '';
6093 * Returns the maximum size for uploading files.
6095 * There are seven possible upload limits:
6096 * 1. in Apache using LimitRequestBody (no way of checking or changing this)
6097 * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
6098 * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
6099 * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
6100 * 5. by the Moodle admin in $CFG->maxbytes
6101 * 6. by the teacher in the current course $course->maxbytes
6102 * 7. by the teacher for the current module, eg $assignment->maxbytes
6104 * These last two are passed to this function as arguments (in bytes).
6105 * Anything defined as 0 is ignored.
6106 * The smallest of all the non-zero numbers is returned.
6108 * @todo Finish documenting this function
6110 * @param int $sizebytes Set maximum size
6111 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6112 * @param int $modulebytes Current module ->maxbytes (in bytes)
6113 * @return int The maximum size for uploading files.
6115 function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0) {
6117 if (! $filesize = ini_get('upload_max_filesize')) {
6118 $filesize = '5M';
6120 $minimumsize = get_real_size($filesize);
6122 if ($postsize = ini_get('post_max_size')) {
6123 $postsize = get_real_size($postsize);
6124 if ($postsize < $minimumsize) {
6125 $minimumsize = $postsize;
6129 if (($sitebytes > 0) and ($sitebytes < $minimumsize)) {
6130 $minimumsize = $sitebytes;
6133 if (($coursebytes > 0) and ($coursebytes < $minimumsize)) {
6134 $minimumsize = $coursebytes;
6137 if (($modulebytes > 0) and ($modulebytes < $minimumsize)) {
6138 $minimumsize = $modulebytes;
6141 return $minimumsize;
6145 * Returns the maximum size for uploading files for the current user
6147 * This function takes in account @see:get_max_upload_file_size() the user's capabilities
6149 * @param context $context The context in which to check user capabilities
6150 * @param int $sizebytes Set maximum size
6151 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6152 * @param int $modulebytes Current module ->maxbytes (in bytes)
6153 * @param stdClass The user
6154 * @return int The maximum size for uploading files.
6156 function get_user_max_upload_file_size($context, $sitebytes=0, $coursebytes=0, $modulebytes=0, $user=null) {
6157 global $USER;
6159 if (empty($user)) {
6160 $user = $USER;
6163 if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) {
6164 return USER_CAN_IGNORE_FILE_SIZE_LIMITS;
6167 return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes);
6171 * Returns an array of possible sizes in local language
6173 * Related to {@link get_max_upload_file_size()} - this function returns an
6174 * array of possible sizes in an array, translated to the
6175 * local language.
6177 * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes.
6179 * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)"
6180 * with the value set to 0. This option will be the first in the list.
6182 * @global object
6183 * @uses SORT_NUMERIC
6184 * @param int $sizebytes Set maximum size
6185 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6186 * @param int $modulebytes Current module ->maxbytes (in bytes)
6187 * @param int|array $custombytes custom upload size/s which will be added to list,
6188 * Only value/s smaller then maxsize will be added to list.
6189 * @return array
6191 function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null) {
6192 global $CFG;
6194 if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) {
6195 return array();
6198 if ($sitebytes == 0) {
6199 // Will get the minimum of upload_max_filesize or post_max_size.
6200 $sitebytes = get_max_upload_file_size();
6203 $filesize = array();
6204 $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152,
6205 5242880, 10485760, 20971520, 52428800, 104857600);
6207 // If custombytes is given and is valid then add it to the list.
6208 if (is_number($custombytes) and $custombytes > 0) {
6209 $custombytes = (int)$custombytes;
6210 if (!in_array($custombytes, $sizelist)) {
6211 $sizelist[] = $custombytes;
6213 } else if (is_array($custombytes)) {
6214 $sizelist = array_unique(array_merge($sizelist, $custombytes));
6217 // Allow maxbytes to be selected if it falls outside the above boundaries
6218 if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) {
6219 // note: get_real_size() is used in order to prevent problems with invalid values
6220 $sizelist[] = get_real_size($CFG->maxbytes);
6223 foreach ($sizelist as $sizebytes) {
6224 if ($sizebytes < $maxsize && $sizebytes > 0) {
6225 $filesize[(string)intval($sizebytes)] = display_size($sizebytes);
6229 $limitlevel = '';
6230 $displaysize = '';
6231 if ($modulebytes &&
6232 (($modulebytes < $coursebytes || $coursebytes == 0) &&
6233 ($modulebytes < $sitebytes || $sitebytes == 0))) {
6234 $limitlevel = get_string('activity', 'core');
6235 $displaysize = display_size($modulebytes);
6236 $filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list.
6238 } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) {
6239 $limitlevel = get_string('course', 'core');
6240 $displaysize = display_size($coursebytes);
6241 $filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list.
6243 } else if ($sitebytes) {
6244 $limitlevel = get_string('site', 'core');
6245 $displaysize = display_size($sitebytes);
6246 $filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list.
6249 krsort($filesize, SORT_NUMERIC);
6250 if ($limitlevel) {
6251 $params = (object) array('contextname'=>$limitlevel, 'displaysize'=>$displaysize);
6252 $filesize = array('0'=>get_string('uploadlimitwithsize', 'core', $params)) + $filesize;
6255 return $filesize;
6259 * Returns an array with all the filenames in all subdirectories, relative to the given rootdir.
6261 * If excludefiles is defined, then that file/directory is ignored
6262 * If getdirs is true, then (sub)directories are included in the output
6263 * If getfiles is true, then files are included in the output
6264 * (at least one of these must be true!)
6266 * @todo Finish documenting this function. Add examples of $excludefile usage.
6268 * @param string $rootdir A given root directory to start from
6269 * @param string|array $excludefile If defined then the specified file/directory is ignored
6270 * @param bool $descend If true then subdirectories are recursed as well
6271 * @param bool $getdirs If true then (sub)directories are included in the output
6272 * @param bool $getfiles If true then files are included in the output
6273 * @return array An array with all the filenames in
6274 * all subdirectories, relative to the given rootdir
6276 function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) {
6278 $dirs = array();
6280 if (!$getdirs and !$getfiles) { // Nothing to show
6281 return $dirs;
6284 if (!is_dir($rootdir)) { // Must be a directory
6285 return $dirs;
6288 if (!$dir = opendir($rootdir)) { // Can't open it for some reason
6289 return $dirs;
6292 if (!is_array($excludefiles)) {
6293 $excludefiles = array($excludefiles);
6296 while (false !== ($file = readdir($dir))) {
6297 $firstchar = substr($file, 0, 1);
6298 if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) {
6299 continue;
6301 $fullfile = $rootdir .'/'. $file;
6302 if (filetype($fullfile) == 'dir') {
6303 if ($getdirs) {
6304 $dirs[] = $file;
6306 if ($descend) {
6307 $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles);
6308 foreach ($subdirs as $subdir) {
6309 $dirs[] = $file .'/'. $subdir;
6312 } else if ($getfiles) {
6313 $dirs[] = $file;
6316 closedir($dir);
6318 asort($dirs);
6320 return $dirs;
6325 * Adds up all the files in a directory and works out the size.
6327 * @todo Finish documenting this function
6329 * @param string $rootdir The directory to start from
6330 * @param string $excludefile A file to exclude when summing directory size
6331 * @return int The summed size of all files and subfiles within the root directory
6333 function get_directory_size($rootdir, $excludefile='') {
6334 global $CFG;
6336 // do it this way if we can, it's much faster
6337 if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) {
6338 $command = trim($CFG->pathtodu).' -sk '.escapeshellarg($rootdir);
6339 $output = null;
6340 $return = null;
6341 exec($command,$output,$return);
6342 if (is_array($output)) {
6343 return get_real_size(intval($output[0]).'k'); // we told it to return k.
6347 if (!is_dir($rootdir)) { // Must be a directory
6348 return 0;
6351 if (!$dir = @opendir($rootdir)) { // Can't open it for some reason
6352 return 0;
6355 $size = 0;
6357 while (false !== ($file = readdir($dir))) {
6358 $firstchar = substr($file, 0, 1);
6359 if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) {
6360 continue;
6362 $fullfile = $rootdir .'/'. $file;
6363 if (filetype($fullfile) == 'dir') {
6364 $size += get_directory_size($fullfile, $excludefile);
6365 } else {
6366 $size += filesize($fullfile);
6369 closedir($dir);
6371 return $size;
6375 * Converts bytes into display form
6377 * @todo Finish documenting this function. Verify return type.
6379 * @staticvar string $gb Localized string for size in gigabytes
6380 * @staticvar string $mb Localized string for size in megabytes
6381 * @staticvar string $kb Localized string for size in kilobytes
6382 * @staticvar string $b Localized string for size in bytes
6383 * @param int $size The size to convert to human readable form
6384 * @return string
6386 function display_size($size) {
6388 static $gb, $mb, $kb, $b;
6390 if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) {
6391 return get_string('unlimited');
6394 if (empty($gb)) {
6395 $gb = get_string('sizegb');
6396 $mb = get_string('sizemb');
6397 $kb = get_string('sizekb');
6398 $b = get_string('sizeb');
6401 if ($size >= 1073741824) {
6402 $size = round($size / 1073741824 * 10) / 10 . $gb;
6403 } else if ($size >= 1048576) {
6404 $size = round($size / 1048576 * 10) / 10 . $mb;
6405 } else if ($size >= 1024) {
6406 $size = round($size / 1024 * 10) / 10 . $kb;
6407 } else {
6408 $size = intval($size) .' '. $b; // file sizes over 2GB can not work in 32bit PHP anyway
6410 return $size;
6414 * Cleans a given filename by removing suspicious or troublesome characters
6415 * @see clean_param()
6417 * @uses PARAM_FILE
6418 * @param string $string file name
6419 * @return string cleaned file name
6421 function clean_filename($string) {
6422 return clean_param($string, PARAM_FILE);
6426 /// STRING TRANSLATION ////////////////////////////////////////
6429 * Returns the code for the current language
6431 * @category string
6432 * @return string
6434 function current_language() {
6435 global $CFG, $USER, $SESSION, $COURSE;
6437 if (!empty($COURSE->id) and $COURSE->id != SITEID and !empty($COURSE->lang)) { // Course language can override all other settings for this page
6438 $return = $COURSE->lang;
6440 } else if (!empty($SESSION->lang)) { // Session language can override other settings
6441 $return = $SESSION->lang;
6443 } else if (!empty($USER->lang)) {
6444 $return = $USER->lang;
6446 } else if (isset($CFG->lang)) {
6447 $return = $CFG->lang;
6449 } else {
6450 $return = 'en';
6453 $return = str_replace('_utf8', '', $return); // Just in case this slipped in from somewhere by accident
6455 return $return;
6459 * Returns parent language of current active language if defined
6461 * @category string
6462 * @uses COURSE
6463 * @uses SESSION
6464 * @param string $lang null means current language
6465 * @return string
6467 function get_parent_language($lang=null) {
6468 global $COURSE, $SESSION;
6470 //let's hack around the current language
6471 if (!empty($lang)) {
6472 $old_course_lang = empty($COURSE->lang) ? '' : $COURSE->lang;
6473 $old_session_lang = empty($SESSION->lang) ? '' : $SESSION->lang;
6474 $COURSE->lang = '';
6475 $SESSION->lang = $lang;
6478 $parentlang = get_string('parentlanguage', 'langconfig');
6479 if ($parentlang === 'en') {
6480 $parentlang = '';
6483 //let's hack around the current language
6484 if (!empty($lang)) {
6485 $COURSE->lang = $old_course_lang;
6486 $SESSION->lang = $old_session_lang;
6489 return $parentlang;
6493 * Returns current string_manager instance.
6495 * The param $forcereload is needed for CLI installer only where the string_manager instance
6496 * must be replaced during the install.php script life time.
6498 * @category string
6499 * @param bool $forcereload shall the singleton be released and new instance created instead?
6500 * @return string_manager
6502 function get_string_manager($forcereload=false) {
6503 global $CFG;
6505 static $singleton = null;
6507 if ($forcereload) {
6508 $singleton = null;
6510 if ($singleton === null) {
6511 if (empty($CFG->early_install_lang)) {
6513 if (empty($CFG->langlist)) {
6514 $translist = array();
6515 } else {
6516 $translist = explode(',', $CFG->langlist);
6519 if (empty($CFG->langmenucachefile)) {
6520 $langmenucache = $CFG->cachedir . '/languages';
6521 } else {
6522 $langmenucache = $CFG->langmenucachefile;
6525 $singleton = new core_string_manager($CFG->langotherroot, $CFG->langlocalroot,
6526 !empty($CFG->langstringcache), $translist, $langmenucache);
6528 } else {
6529 $singleton = new install_string_manager();
6533 return $singleton;
6538 * Interface for string manager
6540 * Interface describing class which is responsible for getting
6541 * of localised strings from language packs.
6543 * @package core
6544 * @copyright 2010 Petr Skoda (http://skodak.org)
6545 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6547 interface string_manager {
6549 * Get String returns a requested string
6551 * @param string $identifier The identifier of the string to search for
6552 * @param string $component The module the string is associated with
6553 * @param string|object|array $a An object, string or number that can be used
6554 * within translation strings
6555 * @param string $lang moodle translation language, NULL means use current
6556 * @return string The String !
6558 public function get_string($identifier, $component = '', $a = NULL, $lang = NULL);
6561 * Does the string actually exist?
6563 * get_string() is throwing debug warnings, sometimes we do not want them
6564 * or we want to display better explanation of the problem.
6566 * Use with care!
6568 * @param string $identifier The identifier of the string to search for
6569 * @param string $component The module the string is associated with
6570 * @return boot true if exists
6572 public function string_exists($identifier, $component);
6575 * Returns a localised list of all country names, sorted by country keys.
6576 * @param bool $returnall return all or just enabled
6577 * @param string $lang moodle translation language, NULL means use current
6578 * @return array two-letter country code => translated name.
6580 public function get_list_of_countries($returnall = false, $lang = NULL);
6583 * Returns a localised list of languages, sorted by code keys.
6585 * @param string $lang moodle translation language, NULL means use current
6586 * @param string $standard language list standard
6587 * iso6392: three-letter language code (ISO 639-2/T) => translated name.
6588 * @return array language code => translated name
6590 public function get_list_of_languages($lang = NULL, $standard = 'iso6392');
6593 * Checks if the translation exists for the language
6595 * @param string $lang moodle translation language code
6596 * @param bool $includeall include also disabled translations
6597 * @return bool true if exists
6599 public function translation_exists($lang, $includeall = true);
6602 * Returns localised list of installed translations
6603 * @param bool $returnall return all or just enabled
6604 * @return array moodle translation code => localised translation name
6606 public function get_list_of_translations($returnall = false);
6609 * Returns localised list of currencies.
6611 * @param string $lang moodle translation language, NULL means use current
6612 * @return array currency code => localised currency name
6614 public function get_list_of_currencies($lang = NULL);
6617 * Load all strings for one component
6618 * @param string $component The module the string is associated with
6619 * @param string $lang
6620 * @param bool $disablecache Do not use caches, force fetching the strings from sources
6621 * @param bool $disablelocal Do not use customized strings in xx_local language packs
6622 * @return array of all string for given component and lang
6624 public function load_component_strings($component, $lang, $disablecache=false, $disablelocal=false);
6627 * Invalidates all caches, should the implementation use any
6628 * @param bool $phpunitreset true means called from our PHPUnit integration test reset
6630 public function reset_caches($phpunitreset = false);
6633 * Returns string revision counter, this is incremented after any
6634 * string cache reset.
6635 * @return int lang string revision counter, -1 if unknown
6637 public function get_revision();
6642 * Standard string_manager implementation
6644 * Implements string_manager with getting and printing localised strings
6646 * @package core
6647 * @category string
6648 * @copyright 2010 Petr Skoda (http://skodak.org)
6649 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6651 class core_string_manager implements string_manager {
6652 /** @var string location of all packs except 'en' */
6653 protected $otherroot;
6654 /** @var string location of all lang pack local modifications */
6655 protected $localroot;
6656 /** @var cache lang string cache - it will be optimised more later */
6657 protected $cache;
6658 /** @var int get_string() counter */
6659 protected $countgetstring = 0;
6660 /** @var bool use disk cache */
6661 protected $usecache;
6662 /** @var array limit list of translations */
6663 protected $translist;
6664 /** @var string location of a file that caches the list of available translations */
6665 protected $menucache;
6668 * Create new instance of string manager
6670 * @param string $otherroot location of downlaoded lang packs - usually $CFG->dataroot/lang
6671 * @param string $localroot usually the same as $otherroot
6672 * @param bool $usecache use disk cache
6673 * @param array $translist limit list of visible translations
6674 * @param string $menucache the location of a file that caches the list of available translations
6676 public function __construct($otherroot, $localroot, $usecache, $translist, $menucache) {
6677 $this->otherroot = $otherroot;
6678 $this->localroot = $localroot;
6679 $this->usecache = $usecache;
6680 $this->translist = $translist;
6681 $this->menucache = $menucache;
6683 if ($this->usecache) {
6684 // We can use a proper cache, establish the cache using the 'String cache' definition.
6685 $this->cache = cache::make('core', 'string');
6686 } else {
6687 // We only want a cache for the length of the request, create a static cache.
6688 $options = array(
6689 'simplekeys' => true,
6690 'simpledata' => true
6692 $this->cache = cache::make_from_params(cache_store::MODE_REQUEST, 'core', 'string', array(), $options);
6697 * Returns list of all explicit parent languages for the given language.
6699 * English (en) is considered as the top implicit parent of all language packs
6700 * and is not included in the returned list. The language itself is appended to the
6701 * end of the list. The method is aware of circular dependency risk.
6703 * @see self::populate_parent_languages()
6704 * @param string $lang the code of the language
6705 * @return array all explicit parent languages with the lang itself appended
6707 public function get_language_dependencies($lang) {
6708 return $this->populate_parent_languages($lang);
6712 * Load all strings for one component
6714 * @param string $component The module the string is associated with
6715 * @param string $lang
6716 * @param bool $disablecache Do not use caches, force fetching the strings from sources
6717 * @param bool $disablelocal Do not use customized strings in xx_local language packs
6718 * @return array of all string for given component and lang
6720 public function load_component_strings($component, $lang, $disablecache=false, $disablelocal=false) {
6721 global $CFG;
6723 list($plugintype, $pluginname) = normalize_component($component);
6724 if ($plugintype == 'core' and is_null($pluginname)) {
6725 $component = 'core';
6726 } else {
6727 $component = $plugintype . '_' . $pluginname;
6730 $cachekey = $lang.'_'.$component;
6732 if (!$disablecache and !$disablelocal) {
6733 $string = $this->cache->get($cachekey);
6734 if ($string) {
6735 return $string;
6739 // no cache found - let us merge all possible sources of the strings
6740 if ($plugintype === 'core') {
6741 $file = $pluginname;
6742 if ($file === null) {
6743 $file = 'moodle';
6745 $string = array();
6746 // first load english pack
6747 if (!file_exists("$CFG->dirroot/lang/en/$file.php")) {
6748 return array();
6750 include("$CFG->dirroot/lang/en/$file.php");
6751 $originalkeys = array_keys($string);
6752 $originalkeys = array_flip($originalkeys);
6754 // and then corresponding local if present and allowed
6755 if (!$disablelocal and file_exists("$this->localroot/en_local/$file.php")) {
6756 include("$this->localroot/en_local/$file.php");
6758 // now loop through all langs in correct order
6759 $deps = $this->get_language_dependencies($lang);
6760 foreach ($deps as $dep) {
6761 // the main lang string location
6762 if (file_exists("$this->otherroot/$dep/$file.php")) {
6763 include("$this->otherroot/$dep/$file.php");
6765 if (!$disablelocal and file_exists("$this->localroot/{$dep}_local/$file.php")) {
6766 include("$this->localroot/{$dep}_local/$file.php");
6770 } else {
6771 if (!$location = get_plugin_directory($plugintype, $pluginname) or !is_dir($location)) {
6772 return array();
6774 if ($plugintype === 'mod') {
6775 // bloody mod hack
6776 $file = $pluginname;
6777 } else {
6778 $file = $plugintype . '_' . $pluginname;
6780 $string = array();
6781 // first load English pack
6782 if (!file_exists("$location/lang/en/$file.php")) {
6783 //English pack does not exist, so do not try to load anything else
6784 return array();
6786 include("$location/lang/en/$file.php");
6787 $originalkeys = array_keys($string);
6788 $originalkeys = array_flip($originalkeys);
6789 // and then corresponding local english if present
6790 if (!$disablelocal and file_exists("$this->localroot/en_local/$file.php")) {
6791 include("$this->localroot/en_local/$file.php");
6794 // now loop through all langs in correct order
6795 $deps = $this->get_language_dependencies($lang);
6796 foreach ($deps as $dep) {
6797 // legacy location - used by contrib only
6798 if (file_exists("$location/lang/$dep/$file.php")) {
6799 include("$location/lang/$dep/$file.php");
6801 // the main lang string location
6802 if (file_exists("$this->otherroot/$dep/$file.php")) {
6803 include("$this->otherroot/$dep/$file.php");
6805 // local customisations
6806 if (!$disablelocal and file_exists("$this->localroot/{$dep}_local/$file.php")) {
6807 include("$this->localroot/{$dep}_local/$file.php");
6812 // we do not want any extra strings from other languages - everything must be in en lang pack
6813 $string = array_intersect_key($string, $originalkeys);
6815 if (!$disablelocal) {
6816 // now we have a list of strings from all possible sources. put it into both in-memory and on-disk
6817 // caches so we do not need to do all this merging and dependencies resolving again
6818 $this->cache->set($cachekey, $string);
6820 return $string;
6824 * Does the string actually exist?
6826 * get_string() is throwing debug warnings, sometimes we do not want them
6827 * or we want to display better explanation of the problem.
6828 * Note: Use with care!
6830 * @param string $identifier The identifier of the string to search for
6831 * @param string $component The module the string is associated with
6832 * @return boot true if exists
6834 public function string_exists($identifier, $component) {
6835 $lang = current_language();
6836 $string = $this->load_component_strings($component, $lang);
6837 return isset($string[$identifier]);
6841 * Get String returns a requested string
6843 * @param string $identifier The identifier of the string to search for
6844 * @param string $component The module the string is associated with
6845 * @param string|object|array $a An object, string or number that can be used
6846 * within translation strings
6847 * @param string $lang moodle translation language, NULL means use current
6848 * @return string The String !
6850 public function get_string($identifier, $component = '', $a = NULL, $lang = NULL) {
6851 $this->countgetstring++;
6852 // there are very many uses of these time formating strings without the 'langconfig' component,
6853 // it would not be reasonable to expect that all of them would be converted during 2.0 migration
6854 static $langconfigstrs = array(
6855 'strftimedate' => 1,
6856 'strftimedatefullshort' => 1,
6857 'strftimedateshort' => 1,
6858 'strftimedatetime' => 1,
6859 'strftimedatetimeshort' => 1,
6860 'strftimedaydate' => 1,
6861 'strftimedaydatetime' => 1,
6862 'strftimedayshort' => 1,
6863 'strftimedaytime' => 1,
6864 'strftimemonthyear' => 1,
6865 'strftimerecent' => 1,
6866 'strftimerecentfull' => 1,
6867 'strftimetime' => 1);
6869 if (empty($component)) {
6870 if (isset($langconfigstrs[$identifier])) {
6871 $component = 'langconfig';
6872 } else {
6873 $component = 'moodle';
6877 if ($lang === NULL) {
6878 $lang = current_language();
6881 $string = $this->load_component_strings($component, $lang);
6883 if (!isset($string[$identifier])) {
6884 if ($component === 'pix' or $component === 'core_pix') {
6885 // this component contains only alt tags for emoticons,
6886 // not all of them are supposed to be defined
6887 return '';
6889 if ($identifier === 'parentlanguage' and ($component === 'langconfig' or $component === 'core_langconfig')) {
6890 // parentlanguage is a special string, undefined means use English if not defined
6891 return 'en';
6893 if ($this->usecache) {
6894 // maybe the on-disk cache is dirty - let the last attempt be to find the string in original sources,
6895 // do NOT write the results to disk cache because it may end up in race conditions see MDL-31904
6896 $this->usecache = false;
6897 $string = $this->load_component_strings($component, $lang, true);
6898 $this->usecache = true;
6900 if (!isset($string[$identifier])) {
6901 // the string is still missing - should be fixed by developer
6902 list($plugintype, $pluginname) = normalize_component($component);
6903 if ($plugintype == 'core') {
6904 $file = "lang/en/{$component}.php";
6905 } else if ($plugintype == 'mod') {
6906 $file = "mod/{$pluginname}/lang/en/{$pluginname}.php";
6907 } else {
6908 $path = get_plugin_directory($plugintype, $pluginname);
6909 $file = "{$path}/lang/en/{$plugintype}_{$pluginname}.php";
6911 debugging("Invalid get_string() identifier: '{$identifier}' or component '{$component}'. " .
6912 "Perhaps you are missing \$string['{$identifier}'] = ''; in {$file}?", DEBUG_DEVELOPER);
6913 return "[[$identifier]]";
6917 $string = $string[$identifier];
6919 if ($a !== NULL) {
6920 // Process array's and objects (except lang_strings)
6921 if (is_array($a) or (is_object($a) && !($a instanceof lang_string))) {
6922 $a = (array)$a;
6923 $search = array();
6924 $replace = array();
6925 foreach ($a as $key=>$value) {
6926 if (is_int($key)) {
6927 // we do not support numeric keys - sorry!
6928 continue;
6930 if (is_array($value) or (is_object($value) && !($value instanceof lang_string))) {
6931 // we support just string or lang_string as value
6932 continue;
6934 $search[] = '{$a->'.$key.'}';
6935 $replace[] = (string)$value;
6937 if ($search) {
6938 $string = str_replace($search, $replace, $string);
6940 } else {
6941 $string = str_replace('{$a}', (string)$a, $string);
6945 return $string;
6949 * Returns information about the string_manager performance
6951 * @return array
6953 public function get_performance_summary() {
6954 return array(array(
6955 'langcountgetstring' => $this->countgetstring,
6956 ), array(
6957 'langcountgetstring' => 'get_string calls',
6962 * Returns a localised list of all country names, sorted by localised name.
6964 * @param bool $returnall return all or just enabled
6965 * @param string $lang moodle translation language, NULL means use current
6966 * @return array two-letter country code => translated name.
6968 public function get_list_of_countries($returnall = false, $lang = NULL) {
6969 global $CFG;
6971 if ($lang === NULL) {
6972 $lang = current_language();
6975 $countries = $this->load_component_strings('core_countries', $lang);
6976 collatorlib::asort($countries);
6977 if (!$returnall and !empty($CFG->allcountrycodes)) {
6978 $enabled = explode(',', $CFG->allcountrycodes);
6979 $return = array();
6980 foreach ($enabled as $c) {
6981 if (isset($countries[$c])) {
6982 $return[$c] = $countries[$c];
6985 return $return;
6988 return $countries;
6992 * Returns a localised list of languages, sorted by code keys.
6994 * @param string $lang moodle translation language, NULL means use current
6995 * @param string $standard language list standard
6996 * - iso6392: three-letter language code (ISO 639-2/T) => translated name
6997 * - iso6391: two-letter langauge code (ISO 639-1) => translated name
6998 * @return array language code => translated name
7000 public function get_list_of_languages($lang = NULL, $standard = 'iso6391') {
7001 if ($lang === NULL) {
7002 $lang = current_language();
7005 if ($standard === 'iso6392') {
7006 $langs = $this->load_component_strings('core_iso6392', $lang);
7007 ksort($langs);
7008 return $langs;
7010 } else if ($standard === 'iso6391') {
7011 $langs2 = $this->load_component_strings('core_iso6392', $lang);
7012 static $mapping = array('aar' => 'aa', 'abk' => 'ab', 'afr' => 'af', 'aka' => 'ak', 'sqi' => 'sq', 'amh' => 'am', 'ara' => 'ar', 'arg' => 'an', 'hye' => 'hy',
7013 'asm' => 'as', 'ava' => 'av', 'ave' => 'ae', 'aym' => 'ay', 'aze' => 'az', 'bak' => 'ba', 'bam' => 'bm', 'eus' => 'eu', 'bel' => 'be', 'ben' => 'bn', 'bih' => 'bh',
7014 'bis' => 'bi', 'bos' => 'bs', 'bre' => 'br', 'bul' => 'bg', 'mya' => 'my', 'cat' => 'ca', 'cha' => 'ch', 'che' => 'ce', 'zho' => 'zh', 'chu' => 'cu', 'chv' => 'cv',
7015 'cor' => 'kw', 'cos' => 'co', 'cre' => 'cr', 'ces' => 'cs', 'dan' => 'da', 'div' => 'dv', 'nld' => 'nl', 'dzo' => 'dz', 'eng' => 'en', 'epo' => 'eo', 'est' => 'et',
7016 'ewe' => 'ee', 'fao' => 'fo', 'fij' => 'fj', 'fin' => 'fi', 'fra' => 'fr', 'fry' => 'fy', 'ful' => 'ff', 'kat' => 'ka', 'deu' => 'de', 'gla' => 'gd', 'gle' => 'ga',
7017 'glg' => 'gl', 'glv' => 'gv', 'ell' => 'el', 'grn' => 'gn', 'guj' => 'gu', 'hat' => 'ht', 'hau' => 'ha', 'heb' => 'he', 'her' => 'hz', 'hin' => 'hi', 'hmo' => 'ho',
7018 'hrv' => 'hr', 'hun' => 'hu', 'ibo' => 'ig', 'isl' => 'is', 'ido' => 'io', 'iii' => 'ii', 'iku' => 'iu', 'ile' => 'ie', 'ina' => 'ia', 'ind' => 'id', 'ipk' => 'ik',
7019 'ita' => 'it', 'jav' => 'jv', 'jpn' => 'ja', 'kal' => 'kl', 'kan' => 'kn', 'kas' => 'ks', 'kau' => 'kr', 'kaz' => 'kk', 'khm' => 'km', 'kik' => 'ki', 'kin' => 'rw',
7020 'kir' => 'ky', 'kom' => 'kv', 'kon' => 'kg', 'kor' => 'ko', 'kua' => 'kj', 'kur' => 'ku', 'lao' => 'lo', 'lat' => 'la', 'lav' => 'lv', 'lim' => 'li', 'lin' => 'ln',
7021 'lit' => 'lt', 'ltz' => 'lb', 'lub' => 'lu', 'lug' => 'lg', 'mkd' => 'mk', 'mah' => 'mh', 'mal' => 'ml', 'mri' => 'mi', 'mar' => 'mr', 'msa' => 'ms', 'mlg' => 'mg',
7022 'mlt' => 'mt', 'mon' => 'mn', 'nau' => 'na', 'nav' => 'nv', 'nbl' => 'nr', 'nde' => 'nd', 'ndo' => 'ng', 'nep' => 'ne', 'nno' => 'nn', 'nob' => 'nb', 'nor' => 'no',
7023 'nya' => 'ny', 'oci' => 'oc', 'oji' => 'oj', 'ori' => 'or', 'orm' => 'om', 'oss' => 'os', 'pan' => 'pa', 'fas' => 'fa', 'pli' => 'pi', 'pol' => 'pl', 'por' => 'pt',
7024 'pus' => 'ps', 'que' => 'qu', 'roh' => 'rm', 'ron' => 'ro', 'run' => 'rn', 'rus' => 'ru', 'sag' => 'sg', 'san' => 'sa', 'sin' => 'si', 'slk' => 'sk', 'slv' => 'sl',
7025 'sme' => 'se', 'smo' => 'sm', 'sna' => 'sn', 'snd' => 'sd', 'som' => 'so', 'sot' => 'st', 'spa' => 'es', 'srd' => 'sc', 'srp' => 'sr', 'ssw' => 'ss', 'sun' => 'su',
7026 'swa' => 'sw', 'swe' => 'sv', 'tah' => 'ty', 'tam' => 'ta', 'tat' => 'tt', 'tel' => 'te', 'tgk' => 'tg', 'tgl' => 'tl', 'tha' => 'th', 'bod' => 'bo', 'tir' => 'ti',
7027 'ton' => 'to', 'tsn' => 'tn', 'tso' => 'ts', 'tuk' => 'tk', 'tur' => 'tr', 'twi' => 'tw', 'uig' => 'ug', 'ukr' => 'uk', 'urd' => 'ur', 'uzb' => 'uz', 'ven' => 've',
7028 'vie' => 'vi', 'vol' => 'vo', 'cym' => 'cy', 'wln' => 'wa', 'wol' => 'wo', 'xho' => 'xh', 'yid' => 'yi', 'yor' => 'yo', 'zha' => 'za', 'zul' => 'zu');
7029 $langs1 = array();
7030 foreach ($mapping as $c2=>$c1) {
7031 $langs1[$c1] = $langs2[$c2];
7033 ksort($langs1);
7034 return $langs1;
7036 } else {
7037 debugging('Unsupported $standard parameter in get_list_of_languages() method: '.$standard);
7040 return array();
7044 * Checks if the translation exists for the language
7046 * @param string $lang moodle translation language code
7047 * @param bool $includeall include also disabled translations
7048 * @return bool true if exists
7050 public function translation_exists($lang, $includeall = true) {
7052 if (strpos($lang, '_local') !== false) {
7053 // _local packs are not real translations
7054 return false;
7056 if (!$includeall and !empty($this->translist)) {
7057 if (!in_array($lang, $this->translist)) {
7058 return false;
7061 if ($lang === 'en') {
7062 // part of distribution
7063 return true;
7065 return file_exists("$this->otherroot/$lang/langconfig.php");
7069 * Returns localised list of installed translations
7071 * @param bool $returnall return all or just enabled
7072 * @return array moodle translation code => localised translation name
7074 public function get_list_of_translations($returnall = false) {
7075 global $CFG;
7077 $languages = array();
7079 if (!empty($CFG->langcache) and is_readable($this->menucache)) {
7080 // try to re-use the cached list of all available languages
7081 $cachedlist = json_decode(file_get_contents($this->menucache), true);
7083 if (is_array($cachedlist) and !empty($cachedlist)) {
7084 // the cache file is restored correctly
7086 if (!$returnall and !empty($this->translist)) {
7087 // return just enabled translations
7088 foreach ($cachedlist as $langcode => $langname) {
7089 if (in_array($langcode, $this->translist)) {
7090 $languages[$langcode] = $langname;
7093 return $languages;
7095 } else {
7096 // return all translations
7097 return $cachedlist;
7102 // the cached list of languages is not available, let us populate the list
7104 if (!$returnall and !empty($this->translist)) {
7105 // return only some translations
7106 foreach ($this->translist as $lang) {
7107 $lang = trim($lang); //Just trim spaces to be a bit more permissive
7108 if (strstr($lang, '_local') !== false) {
7109 continue;
7111 if (strstr($lang, '_utf8') !== false) {
7112 continue;
7114 if ($lang !== 'en' and !file_exists("$this->otherroot/$lang/langconfig.php")) {
7115 // some broken or missing lang - can not switch to it anyway
7116 continue;
7118 $string = $this->load_component_strings('langconfig', $lang);
7119 if (!empty($string['thislanguage'])) {
7120 $languages[$lang] = $string['thislanguage'].' ('. $lang .')';
7122 unset($string);
7125 } else {
7126 // return all languages available in system
7127 $langdirs = get_list_of_plugins('', '', $this->otherroot);
7129 $langdirs = array_merge($langdirs, array("$CFG->dirroot/lang/en"=>'en'));
7130 // Sort all
7132 // Loop through all langs and get info
7133 foreach ($langdirs as $lang) {
7134 if (strstr($lang, '_local') !== false) {
7135 continue;
7137 if (strstr($lang, '_utf8') !== false) {
7138 continue;
7140 $string = $this->load_component_strings('langconfig', $lang);
7141 if (!empty($string['thislanguage'])) {
7142 $languages[$lang] = $string['thislanguage'].' ('. $lang .')';
7144 unset($string);
7147 if (!empty($CFG->langcache) and !empty($this->menucache)) {
7148 // cache the list so that it can be used next time
7149 collatorlib::asort($languages);
7150 check_dir_exists(dirname($this->menucache), true, true);
7151 file_put_contents($this->menucache, json_encode($languages));
7155 collatorlib::asort($languages);
7157 return $languages;
7161 * Returns localised list of currencies.
7163 * @param string $lang moodle translation language, NULL means use current
7164 * @return array currency code => localised currency name
7166 public function get_list_of_currencies($lang = NULL) {
7167 if ($lang === NULL) {
7168 $lang = current_language();
7171 $currencies = $this->load_component_strings('core_currencies', $lang);
7172 asort($currencies);
7174 return $currencies;
7178 * Clears both in-memory and on-disk caches
7179 * @param bool $phpunitreset true means called from our PHPUnit integration test reset
7181 public function reset_caches($phpunitreset = false) {
7182 global $CFG;
7183 require_once("$CFG->libdir/filelib.php");
7185 // clear the on-disk disk with aggregated string files
7186 $this->cache->purge();
7188 if (!$phpunitreset) {
7189 // Increment the revision counter.
7190 $langrev = get_config('core', 'langrev');
7191 $next = time();
7192 if ($langrev !== false and $next <= $langrev and $langrev - $next < 60*60) {
7193 // This resolves problems when reset is requested repeatedly within 1s,
7194 // the < 1h condition prevents accidental switching to future dates
7195 // because we might not recover from it.
7196 $next = $langrev+1;
7198 set_config('langrev', $next);
7201 // clear the cache containing the list of available translations
7202 // and re-populate it again
7203 fulldelete($this->menucache);
7204 $this->get_list_of_translations(true);
7208 * Returns string revision counter, this is incremented after any
7209 * string cache reset.
7210 * @return int lang string revision counter, -1 if unknown
7212 public function get_revision() {
7213 global $CFG;
7214 if (isset($CFG->langrev)) {
7215 return (int)$CFG->langrev;
7216 } else {
7217 return -1;
7221 /// End of external API ////////////////////////////////////////////////////
7224 * Helper method that recursively loads all parents of the given language.
7226 * @see self::get_language_dependencies()
7227 * @param string $lang language code
7228 * @param array $stack list of parent languages already populated in previous recursive calls
7229 * @return array list of all parents of the given language with the $lang itself added as the last element
7231 protected function populate_parent_languages($lang, array $stack = array()) {
7233 // English does not have a parent language.
7234 if ($lang === 'en') {
7235 return $stack;
7238 // Prevent circular dependency (and thence the infinitive recursion loop).
7239 if (in_array($lang, $stack)) {
7240 return $stack;
7243 // Load language configuration and look for the explicit parent language.
7244 if (!file_exists("$this->otherroot/$lang/langconfig.php")) {
7245 return $stack;
7247 $string = array();
7248 include("$this->otherroot/$lang/langconfig.php");
7250 if (empty($string['parentlanguage']) or $string['parentlanguage'] === 'en') {
7251 unset($string);
7252 return array_merge(array($lang), $stack);
7254 } else {
7255 $parentlang = $string['parentlanguage'];
7256 unset($string);
7257 return $this->populate_parent_languages($parentlang, array_merge(array($lang), $stack));
7264 * Fetches minimum strings for installation
7266 * Minimalistic string fetching implementation
7267 * that is used in installer before we fetch the wanted
7268 * language pack from moodle.org lang download site.
7270 * @package core
7271 * @copyright 2010 Petr Skoda (http://skodak.org)
7272 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
7274 class install_string_manager implements string_manager {
7275 /** @var string location of pre-install packs for all langs */
7276 protected $installroot;
7279 * Crate new instance of install string manager
7281 public function __construct() {
7282 global $CFG;
7283 $this->installroot = "$CFG->dirroot/install/lang";
7287 * Load all strings for one component
7288 * @param string $component The module the string is associated with
7289 * @param string $lang
7290 * @param bool $disablecache Do not use caches, force fetching the strings from sources
7291 * @param bool $disablelocal Do not use customized strings in xx_local language packs
7292 * @return array of all string for given component and lang
7294 public function load_component_strings($component, $lang, $disablecache=false, $disablelocal=false) {
7295 // not needed in installer
7296 return array();
7300 * Does the string actually exist?
7302 * get_string() is throwing debug warnings, sometimes we do not want them
7303 * or we want to display better explanation of the problem.
7305 * Use with care!
7307 * @param string $identifier The identifier of the string to search for
7308 * @param string $component The module the string is associated with
7309 * @return boot true if exists
7311 public function string_exists($identifier, $component) {
7312 // simple old style hack ;)
7313 $str = get_string($identifier, $component);
7314 return (strpos($str, '[[') === false);
7318 * Get String returns a requested string
7320 * @param string $identifier The identifier of the string to search for
7321 * @param string $component The module the string is associated with
7322 * @param string|object|array $a An object, string or number that can be used
7323 * within translation strings
7324 * @param string $lang moodle translation language, NULL means use current
7325 * @return string The String !
7327 public function get_string($identifier, $component = '', $a = NULL, $lang = NULL) {
7328 if (!$component) {
7329 $component = 'moodle';
7332 if ($lang === NULL) {
7333 $lang = current_language();
7336 //get parent lang
7337 $parent = '';
7338 if ($lang !== 'en' and $identifier !== 'parentlanguage' and $component !== 'langconfig') {
7339 if (file_exists("$this->installroot/$lang/langconfig.php")) {
7340 $string = array();
7341 include("$this->installroot/$lang/langconfig.php");
7342 if (isset($string['parentlanguage'])) {
7343 $parent = $string['parentlanguage'];
7345 unset($string);
7349 // include en string first
7350 if (!file_exists("$this->installroot/en/$component.php")) {
7351 return "[[$identifier]]";
7353 $string = array();
7354 include("$this->installroot/en/$component.php");
7356 // now override en with parent if defined
7357 if ($parent and $parent !== 'en' and file_exists("$this->installroot/$parent/$component.php")) {
7358 include("$this->installroot/$parent/$component.php");
7361 // finally override with requested language
7362 if ($lang !== 'en' and file_exists("$this->installroot/$lang/$component.php")) {
7363 include("$this->installroot/$lang/$component.php");
7366 if (!isset($string[$identifier])) {
7367 return "[[$identifier]]";
7370 $string = $string[$identifier];
7372 if ($a !== NULL) {
7373 if (is_object($a) or is_array($a)) {
7374 $a = (array)$a;
7375 $search = array();
7376 $replace = array();
7377 foreach ($a as $key=>$value) {
7378 if (is_int($key)) {
7379 // we do not support numeric keys - sorry!
7380 continue;
7382 $search[] = '{$a->'.$key.'}';
7383 $replace[] = (string)$value;
7385 if ($search) {
7386 $string = str_replace($search, $replace, $string);
7388 } else {
7389 $string = str_replace('{$a}', (string)$a, $string);
7393 return $string;
7397 * Returns a localised list of all country names, sorted by country keys.
7399 * @param bool $returnall return all or just enabled
7400 * @param string $lang moodle translation language, NULL means use current
7401 * @return array two-letter country code => translated name.
7403 public function get_list_of_countries($returnall = false, $lang = NULL) {
7404 //not used in installer
7405 return array();
7409 * Returns a localised list of languages, sorted by code keys.
7411 * @param string $lang moodle translation language, NULL means use current
7412 * @param string $standard language list standard
7413 * iso6392: three-letter language code (ISO 639-2/T) => translated name.
7414 * @return array language code => translated name
7416 public function get_list_of_languages($lang = NULL, $standard = 'iso6392') {
7417 //not used in installer
7418 return array();
7422 * Checks if the translation exists for the language
7424 * @param string $lang moodle translation language code
7425 * @param bool $includeall include also disabled translations
7426 * @return bool true if exists
7428 public function translation_exists($lang, $includeall = true) {
7429 return file_exists($this->installroot.'/'.$lang.'/langconfig.php');
7433 * Returns localised list of installed translations
7434 * @param bool $returnall return all or just enabled
7435 * @return array moodle translation code => localised translation name
7437 public function get_list_of_translations($returnall = false) {
7438 // return all is ignored here - we need to know all langs in installer
7439 $languages = array();
7440 // Get raw list of lang directories
7441 $langdirs = get_list_of_plugins('install/lang');
7442 asort($langdirs);
7443 // Get some info from each lang
7444 foreach ($langdirs as $lang) {
7445 if (file_exists($this->installroot.'/'.$lang.'/langconfig.php')) {
7446 $string = array();
7447 include($this->installroot.'/'.$lang.'/langconfig.php');
7448 if (!empty($string['thislanguage'])) {
7449 $languages[$lang] = $string['thislanguage'].' ('.$lang.')';
7453 // Return array
7454 return $languages;
7458 * Returns localised list of currencies.
7460 * @param string $lang moodle translation language, NULL means use current
7461 * @return array currency code => localised currency name
7463 public function get_list_of_currencies($lang = NULL) {
7464 // not used in installer
7465 return array();
7469 * This implementation does not use any caches
7470 * @param bool $phpunitreset true means called from our PHPUnit integration test reset
7472 public function reset_caches($phpunitreset = false) {
7473 // Nothing to do.
7477 * Returns string revision counter, this is incremented after any
7478 * string cache reset.
7479 * @return int lang string revision counter, -1 if unknown
7481 public function get_revision() {
7482 return -1;
7488 * Returns a localized string.
7490 * Returns the translated string specified by $identifier as
7491 * for $module. Uses the same format files as STphp.
7492 * $a is an object, string or number that can be used
7493 * within translation strings
7495 * eg 'hello {$a->firstname} {$a->lastname}'
7496 * or 'hello {$a}'
7498 * If you would like to directly echo the localized string use
7499 * the function {@link print_string()}
7501 * Example usage of this function involves finding the string you would
7502 * like a local equivalent of and using its identifier and module information
7503 * to retrieve it.<br/>
7504 * If you open moodle/lang/en/moodle.php and look near line 278
7505 * you will find a string to prompt a user for their word for 'course'
7506 * <code>
7507 * $string['course'] = 'Course';
7508 * </code>
7509 * So if you want to display the string 'Course'
7510 * in any language that supports it on your site
7511 * you just need to use the identifier 'course'
7512 * <code>
7513 * $mystring = '<strong>'. get_string('course') .'</strong>';
7514 * or
7515 * </code>
7516 * If the string you want is in another file you'd take a slightly
7517 * different approach. Looking in moodle/lang/en/calendar.php you find
7518 * around line 75:
7519 * <code>
7520 * $string['typecourse'] = 'Course event';
7521 * </code>
7522 * If you want to display the string "Course event" in any language
7523 * supported you would use the identifier 'typecourse' and the module 'calendar'
7524 * (because it is in the file calendar.php):
7525 * <code>
7526 * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>';
7527 * </code>
7529 * As a last resort, should the identifier fail to map to a string
7530 * the returned string will be [[ $identifier ]]
7532 * In Moodle 2.3 there is a new argument to this function $lazyload.
7533 * Setting $lazyload to true causes get_string to return a lang_string object
7534 * rather than the string itself. The fetching of the string is then put off until
7535 * the string object is first used. The object can be used by calling it's out
7536 * method or by casting the object to a string, either directly e.g.
7537 * (string)$stringobject
7538 * or indirectly by using the string within another string or echoing it out e.g.
7539 * echo $stringobject
7540 * return "<p>{$stringobject}</p>";
7541 * It is worth noting that using $lazyload and attempting to use the string as an
7542 * array key will cause a fatal error as objects cannot be used as array keys.
7543 * But you should never do that anyway!
7544 * For more information {@see lang_string}
7546 * @category string
7547 * @param string $identifier The key identifier for the localized string
7548 * @param string $component The module where the key identifier is stored,
7549 * usually expressed as the filename in the language pack without the
7550 * .php on the end but can also be written as mod/forum or grade/export/xls.
7551 * If none is specified then moodle.php is used.
7552 * @param string|object|array $a An object, string or number that can be used
7553 * within translation strings
7554 * @param bool $lazyload If set to true a string object is returned instead of
7555 * the string itself. The string then isn't calculated until it is first used.
7556 * @return string The localized string.
7558 function get_string($identifier, $component = '', $a = NULL, $lazyload = false) {
7559 global $CFG;
7561 // If the lazy load argument has been supplied return a lang_string object
7562 // instead.
7563 // We need to make sure it is true (and a bool) as you will see below there
7564 // used to be a forth argument at one point.
7565 if ($lazyload === true) {
7566 return new lang_string($identifier, $component, $a);
7569 if (debugging('', DEBUG_DEVELOPER) && clean_param($identifier, PARAM_STRINGID) === '') {
7570 throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.');
7573 // There is now a forth argument again, this time it is a boolean however so
7574 // we can still check for the old extralocations parameter.
7575 if (!is_bool($lazyload) && !empty($lazyload)) {
7576 debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.');
7579 if (strpos($component, '/') !== false) {
7580 debugging('The module name you passed to get_string is the deprecated format ' .
7581 'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER);
7582 $componentpath = explode('/', $component);
7584 switch ($componentpath[0]) {
7585 case 'mod':
7586 $component = $componentpath[1];
7587 break;
7588 case 'blocks':
7589 case 'block':
7590 $component = 'block_'.$componentpath[1];
7591 break;
7592 case 'enrol':
7593 $component = 'enrol_'.$componentpath[1];
7594 break;
7595 case 'format':
7596 $component = 'format_'.$componentpath[1];
7597 break;
7598 case 'grade':
7599 $component = 'grade'.$componentpath[1].'_'.$componentpath[2];
7600 break;
7604 $result = get_string_manager()->get_string($identifier, $component, $a);
7606 // Debugging feature lets you display string identifier and component
7607 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
7608 $result .= ' {' . $identifier . '/' . $component . '}';
7610 return $result;
7614 * Converts an array of strings to their localized value.
7616 * @param array $array An array of strings
7617 * @param string $component The language module that these strings can be found in.
7618 * @return stdClass translated strings.
7620 function get_strings($array, $component = '') {
7621 $string = new stdClass;
7622 foreach ($array as $item) {
7623 $string->$item = get_string($item, $component);
7625 return $string;
7629 * Prints out a translated string.
7631 * Prints out a translated string using the return value from the {@link get_string()} function.
7633 * Example usage of this function when the string is in the moodle.php file:<br/>
7634 * <code>
7635 * echo '<strong>';
7636 * print_string('course');
7637 * echo '</strong>';
7638 * </code>
7640 * Example usage of this function when the string is not in the moodle.php file:<br/>
7641 * <code>
7642 * echo '<h1>';
7643 * print_string('typecourse', 'calendar');
7644 * echo '</h1>';
7645 * </code>
7647 * @category string
7648 * @param string $identifier The key identifier for the localized string
7649 * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used.
7650 * @param string|object|array $a An object, string or number that can be used within translation strings
7652 function print_string($identifier, $component = '', $a = NULL) {
7653 echo get_string($identifier, $component, $a);
7657 * Returns a list of charset codes
7659 * Returns a list of charset codes. It's hardcoded, so they should be added manually
7660 * (checking that such charset is supported by the texlib library!)
7662 * @return array And associative array with contents in the form of charset => charset
7664 function get_list_of_charsets() {
7666 $charsets = array(
7667 'EUC-JP' => 'EUC-JP',
7668 'ISO-2022-JP'=> 'ISO-2022-JP',
7669 'ISO-8859-1' => 'ISO-8859-1',
7670 'SHIFT-JIS' => 'SHIFT-JIS',
7671 'GB2312' => 'GB2312',
7672 'GB18030' => 'GB18030', // gb18030 not supported by typo and mbstring
7673 'UTF-8' => 'UTF-8');
7675 asort($charsets);
7677 return $charsets;
7681 * Returns a list of valid and compatible themes
7683 * @return array
7685 function get_list_of_themes() {
7686 global $CFG;
7688 $themes = array();
7690 if (!empty($CFG->themelist)) { // use admin's list of themes
7691 $themelist = explode(',', $CFG->themelist);
7692 } else {
7693 $themelist = array_keys(get_plugin_list("theme"));
7696 foreach ($themelist as $key => $themename) {
7697 $theme = theme_config::load($themename);
7698 $themes[$themename] = $theme;
7701 collatorlib::asort_objects_by_method($themes, 'get_theme_name');
7703 return $themes;
7707 * Returns a list of timezones in the current language
7709 * @global object
7710 * @global object
7711 * @return array
7713 function get_list_of_timezones() {
7714 global $CFG, $DB;
7716 static $timezones;
7718 if (!empty($timezones)) { // This function has been called recently
7719 return $timezones;
7722 $timezones = array();
7724 if ($rawtimezones = $DB->get_records_sql("SELECT MAX(id), name FROM {timezone} GROUP BY name")) {
7725 foreach($rawtimezones as $timezone) {
7726 if (!empty($timezone->name)) {
7727 if (get_string_manager()->string_exists(strtolower($timezone->name), 'timezones')) {
7728 $timezones[$timezone->name] = get_string(strtolower($timezone->name), 'timezones');
7729 } else {
7730 $timezones[$timezone->name] = $timezone->name;
7732 if (substr($timezones[$timezone->name], 0, 1) == '[') { // No translation found
7733 $timezones[$timezone->name] = $timezone->name;
7739 asort($timezones);
7741 for ($i = -13; $i <= 13; $i += .5) {
7742 $tzstring = 'UTC';
7743 if ($i < 0) {
7744 $timezones[sprintf("%.1f", $i)] = $tzstring . $i;
7745 } else if ($i > 0) {
7746 $timezones[sprintf("%.1f", $i)] = $tzstring . '+' . $i;
7747 } else {
7748 $timezones[sprintf("%.1f", $i)] = $tzstring;
7752 return $timezones;
7756 * Factory function for emoticon_manager
7758 * @return emoticon_manager singleton
7760 function get_emoticon_manager() {
7761 static $singleton = null;
7763 if (is_null($singleton)) {
7764 $singleton = new emoticon_manager();
7767 return $singleton;
7771 * Provides core support for plugins that have to deal with
7772 * emoticons (like HTML editor or emoticon filter).
7774 * Whenever this manager mentiones 'emoticon object', the following data
7775 * structure is expected: stdClass with properties text, imagename, imagecomponent,
7776 * altidentifier and altcomponent
7778 * @see admin_setting_emoticons
7780 class emoticon_manager {
7783 * Returns the currently enabled emoticons
7785 * @return array of emoticon objects
7787 public function get_emoticons() {
7788 global $CFG;
7790 if (empty($CFG->emoticons)) {
7791 return array();
7794 $emoticons = $this->decode_stored_config($CFG->emoticons);
7796 if (!is_array($emoticons)) {
7797 // something is wrong with the format of stored setting
7798 debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL);
7799 return array();
7802 return $emoticons;
7806 * Converts emoticon object into renderable pix_emoticon object
7808 * @param stdClass $emoticon emoticon object
7809 * @param array $attributes explicit HTML attributes to set
7810 * @return pix_emoticon
7812 public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array()) {
7813 $stringmanager = get_string_manager();
7814 if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) {
7815 $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent);
7816 } else {
7817 $alt = s($emoticon->text);
7819 return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes);
7823 * Encodes the array of emoticon objects into a string storable in config table
7825 * @see self::decode_stored_config()
7826 * @param array $emoticons array of emtocion objects
7827 * @return string
7829 public function encode_stored_config(array $emoticons) {
7830 return json_encode($emoticons);
7834 * Decodes the string into an array of emoticon objects
7836 * @see self::encode_stored_config()
7837 * @param string $encoded
7838 * @return string|null
7840 public function decode_stored_config($encoded) {
7841 $decoded = json_decode($encoded);
7842 if (!is_array($decoded)) {
7843 return null;
7845 return $decoded;
7849 * Returns default set of emoticons supported by Moodle
7851 * @return array of sdtClasses
7853 public function default_emoticons() {
7854 return array(
7855 $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'),
7856 $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'),
7857 $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'),
7858 $this->prepare_emoticon_object(";-)", 's/wink', 'wink'),
7859 $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'),
7860 $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'),
7861 $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'),
7862 $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'),
7863 $this->prepare_emoticon_object("B-)", 's/cool', 'cool'),
7864 $this->prepare_emoticon_object("^-)", 's/approve', 'approve'),
7865 $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'),
7866 $this->prepare_emoticon_object(":o)", 's/clown', 'clown'),
7867 $this->prepare_emoticon_object(":-(", 's/sad', 'sad'),
7868 $this->prepare_emoticon_object(":(", 's/sad', 'sad'),
7869 $this->prepare_emoticon_object("8-.", 's/shy', 'shy'),
7870 $this->prepare_emoticon_object(":-I", 's/blush', 'blush'),
7871 $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'),
7872 $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'),
7873 $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'),
7874 $this->prepare_emoticon_object("8-[", 's/angry', 'angry'),
7875 $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'),
7876 $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'),
7877 $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'),
7878 $this->prepare_emoticon_object("}-]", 's/evil', 'evil'),
7879 $this->prepare_emoticon_object("(h)", 's/heart', 'heart'),
7880 $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'),
7881 $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'),
7882 $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'),
7883 $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'),
7884 $this->prepare_emoticon_object("( )", 's/egg', 'egg'),
7889 * Helper method preparing the stdClass with the emoticon properties
7891 * @param string|array $text or array of strings
7892 * @param string $imagename to be used by {@see pix_emoticon}
7893 * @param string $altidentifier alternative string identifier, null for no alt
7894 * @param array $altcomponent where the alternative string is defined
7895 * @param string $imagecomponent to be used by {@see pix_emoticon}
7896 * @return stdClass
7898 protected function prepare_emoticon_object($text, $imagename, $altidentifier = null, $altcomponent = 'core_pix', $imagecomponent = 'core') {
7899 return (object)array(
7900 'text' => $text,
7901 'imagename' => $imagename,
7902 'imagecomponent' => $imagecomponent,
7903 'altidentifier' => $altidentifier,
7904 'altcomponent' => $altcomponent,
7909 /// ENCRYPTION ////////////////////////////////////////////////
7912 * rc4encrypt
7914 * Please note that in this version of moodle that the default for rc4encryption is
7915 * using the slightly more secure password key. There may be an issue when upgrading
7916 * from an older version of moodle.
7918 * @todo MDL-31836 Remove the old password key in version 2.4
7919 * Code also needs to be changed in sessionlib.php
7920 * @see get_moodle_cookie()
7921 * @see set_moodle_cookie()
7923 * @param string $data Data to encrypt.
7924 * @param bool $usesecurekey Lets us know if we are using the old or new secure password key.
7925 * @return string The now encrypted data.
7927 function rc4encrypt($data, $usesecurekey = true) {
7928 if (!$usesecurekey) {
7929 $passwordkey = 'nfgjeingjk';
7930 } else {
7931 $passwordkey = get_site_identifier();
7933 return endecrypt($passwordkey, $data, '');
7937 * rc4decrypt
7939 * Please note that in this version of moodle that the default for rc4encryption is
7940 * using the slightly more secure password key. There may be an issue when upgrading
7941 * from an older version of moodle.
7943 * @todo MDL-31836 Remove the old password key in version 2.4
7944 * Code also needs to be changed in sessionlib.php
7945 * @see get_moodle_cookie()
7946 * @see set_moodle_cookie()
7948 * @param string $data Data to decrypt.
7949 * @param bool $usesecurekey Lets us know if we are using the old or new secure password key.
7950 * @return string The now decrypted data.
7952 function rc4decrypt($data, $usesecurekey = true) {
7953 if (!$usesecurekey) {
7954 $passwordkey = 'nfgjeingjk';
7955 } else {
7956 $passwordkey = get_site_identifier();
7958 return endecrypt($passwordkey, $data, 'de');
7962 * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com]
7964 * @todo Finish documenting this function
7966 * @param string $pwd The password to use when encrypting or decrypting
7967 * @param string $data The data to be decrypted/encrypted
7968 * @param string $case Either 'de' for decrypt or '' for encrypt
7969 * @return string
7971 function endecrypt ($pwd, $data, $case) {
7973 if ($case == 'de') {
7974 $data = urldecode($data);
7977 $key[] = '';
7978 $box[] = '';
7979 $temp_swap = '';
7980 $pwd_length = 0;
7982 $pwd_length = strlen($pwd);
7984 for ($i = 0; $i <= 255; $i++) {
7985 $key[$i] = ord(substr($pwd, ($i % $pwd_length), 1));
7986 $box[$i] = $i;
7989 $x = 0;
7991 for ($i = 0; $i <= 255; $i++) {
7992 $x = ($x + $box[$i] + $key[$i]) % 256;
7993 $temp_swap = $box[$i];
7994 $box[$i] = $box[$x];
7995 $box[$x] = $temp_swap;
7998 $temp = '';
7999 $k = '';
8001 $cipherby = '';
8002 $cipher = '';
8004 $a = 0;
8005 $j = 0;
8007 for ($i = 0; $i < strlen($data); $i++) {
8008 $a = ($a + 1) % 256;
8009 $j = ($j + $box[$a]) % 256;
8010 $temp = $box[$a];
8011 $box[$a] = $box[$j];
8012 $box[$j] = $temp;
8013 $k = $box[(($box[$a] + $box[$j]) % 256)];
8014 $cipherby = ord(substr($data, $i, 1)) ^ $k;
8015 $cipher .= chr($cipherby);
8018 if ($case == 'de') {
8019 $cipher = urldecode(urlencode($cipher));
8020 } else {
8021 $cipher = urlencode($cipher);
8024 return $cipher;
8027 /// ENVIRONMENT CHECKING ////////////////////////////////////////////////////////////
8030 * Returns the exact absolute path to plugin directory.
8032 * @param string $plugintype type of plugin
8033 * @param string $name name of the plugin
8034 * @return string full path to plugin directory; NULL if not found
8036 function get_plugin_directory($plugintype, $name) {
8037 global $CFG;
8039 if ($plugintype === '') {
8040 $plugintype = 'mod';
8043 $types = get_plugin_types(true);
8044 if (!array_key_exists($plugintype, $types)) {
8045 return NULL;
8047 $name = clean_param($name, PARAM_SAFEDIR); // just in case ;-)
8049 if (!empty($CFG->themedir) and $plugintype === 'theme') {
8050 if (!is_dir($types['theme'] . '/' . $name)) {
8051 // ok, so the theme is supposed to be in the $CFG->themedir
8052 return $CFG->themedir . '/' . $name;
8056 return $types[$plugintype].'/'.$name;
8060 * Return exact absolute path to a plugin directory.
8062 * @param string $component name such as 'moodle', 'mod_forum'
8063 * @return string full path to component directory; NULL if not found
8065 function get_component_directory($component) {
8066 global $CFG;
8068 list($type, $plugin) = normalize_component($component);
8070 if ($type === 'core') {
8071 if ($plugin === NULL ) {
8072 $path = $CFG->libdir;
8073 } else {
8074 $subsystems = get_core_subsystems();
8075 if (isset($subsystems[$plugin])) {
8076 $path = $CFG->dirroot.'/'.$subsystems[$plugin];
8077 } else {
8078 $path = NULL;
8082 } else {
8083 $path = get_plugin_directory($type, $plugin);
8086 return $path;
8090 * Normalize the component name using the "frankenstyle" names.
8091 * @param string $component
8092 * @return array $type+$plugin elements
8094 function normalize_component($component) {
8095 if ($component === 'moodle' or $component === 'core') {
8096 $type = 'core';
8097 $plugin = NULL;
8099 } else if (strpos($component, '_') === false) {
8100 $subsystems = get_core_subsystems();
8101 if (array_key_exists($component, $subsystems)) {
8102 $type = 'core';
8103 $plugin = $component;
8104 } else {
8105 // everything else is a module
8106 $type = 'mod';
8107 $plugin = $component;
8110 } else {
8111 list($type, $plugin) = explode('_', $component, 2);
8112 $plugintypes = get_plugin_types(false);
8113 if ($type !== 'core' and !array_key_exists($type, $plugintypes)) {
8114 $type = 'mod';
8115 $plugin = $component;
8119 return array($type, $plugin);
8123 * List all core subsystems and their location
8125 * This is a whitelist of components that are part of the core and their
8126 * language strings are defined in /lang/en/<<subsystem>>.php. If a given
8127 * plugin is not listed here and it does not have proper plugintype prefix,
8128 * then it is considered as course activity module.
8130 * The location is dirroot relative path. NULL means there is no special
8131 * directory for this subsystem. If the location is set, the subsystem's
8132 * renderer.php is expected to be there.
8134 * @return array of (string)name => (string|null)location
8136 function get_core_subsystems() {
8137 global $CFG;
8139 static $info = null;
8141 if (!$info) {
8142 $info = array(
8143 'access' => NULL,
8144 'admin' => $CFG->admin,
8145 'auth' => 'auth',
8146 'backup' => 'backup/util/ui',
8147 'badges' => 'badges',
8148 'block' => 'blocks',
8149 'blog' => 'blog',
8150 'bulkusers' => NULL,
8151 'cache' => 'cache',
8152 'calendar' => 'calendar',
8153 'cohort' => 'cohort',
8154 'condition' => NULL,
8155 'completion' => NULL,
8156 'countries' => NULL,
8157 'course' => 'course',
8158 'currencies' => NULL,
8159 'dbtransfer' => NULL,
8160 'debug' => NULL,
8161 'dock' => NULL,
8162 'editor' => 'lib/editor',
8163 'edufields' => NULL,
8164 'enrol' => 'enrol',
8165 'error' => NULL,
8166 'filepicker' => NULL,
8167 'files' => 'files',
8168 'filters' => NULL,
8169 'fonts' => NULL,
8170 'form' => 'lib/form',
8171 'grades' => 'grade',
8172 'grading' => 'grade/grading',
8173 'group' => 'group',
8174 'help' => NULL,
8175 'hub' => NULL,
8176 'imscc' => NULL,
8177 'install' => NULL,
8178 'iso6392' => NULL,
8179 'langconfig' => NULL,
8180 'license' => NULL,
8181 'mathslib' => NULL,
8182 'media' => 'media',
8183 'message' => 'message',
8184 'mimetypes' => NULL,
8185 'mnet' => 'mnet',
8186 'moodle.org' => NULL, // the dot is nasty, watch out! should be renamed to moodleorg
8187 'my' => 'my',
8188 'notes' => 'notes',
8189 'pagetype' => NULL,
8190 'pix' => NULL,
8191 'plagiarism' => 'plagiarism',
8192 'plugin' => NULL,
8193 'portfolio' => 'portfolio',
8194 'publish' => 'course/publish',
8195 'question' => 'question',
8196 'rating' => 'rating',
8197 'register' => 'admin/registration', //TODO: this is wrong, unfortunately we would need to modify hub code to pass around the correct url
8198 'repository' => 'repository',
8199 'rss' => 'rss',
8200 'role' => $CFG->admin.'/role',
8201 'search' => 'search',
8202 'table' => NULL,
8203 'tag' => 'tag',
8204 'timezones' => NULL,
8205 'user' => 'user',
8206 'userkey' => NULL,
8207 'webservice' => 'webservice',
8211 return $info;
8215 * Lists all plugin types
8216 * @param bool $fullpaths false means relative paths from dirroot
8217 * @return array Array of strings - name=>location
8219 function get_plugin_types($fullpaths=true) {
8220 global $CFG;
8222 $cache = cache::make('core', 'plugintypes');
8224 if ($fullpaths) {
8225 // First confirm that dirroot and the stored dirroot match.
8226 if ($CFG->dirroot === $cache->get('dirroot')) {
8227 // They match we can use it.
8228 $cached = $cache->get(1);
8229 } else {
8230 // Oops they didn't match. The moodle directory has been moved on us.
8231 $cached = false;
8233 } else {
8234 $cached = $cache->get(0);
8237 if ($cached !== false) {
8238 return $cached;
8240 } else {
8241 $info = array('qtype' => 'question/type',
8242 'mod' => 'mod',
8243 'auth' => 'auth',
8244 'enrol' => 'enrol',
8245 'message' => 'message/output',
8246 'block' => 'blocks',
8247 'filter' => 'filter',
8248 'editor' => 'lib/editor',
8249 'format' => 'course/format',
8250 'profilefield' => 'user/profile/field',
8251 'report' => 'report',
8252 'coursereport' => 'course/report', // must be after system reports
8253 'gradeexport' => 'grade/export',
8254 'gradeimport' => 'grade/import',
8255 'gradereport' => 'grade/report',
8256 'gradingform' => 'grade/grading/form',
8257 'mnetservice' => 'mnet/service',
8258 'webservice' => 'webservice',
8259 'repository' => 'repository',
8260 'portfolio' => 'portfolio',
8261 'qbehaviour' => 'question/behaviour',
8262 'qformat' => 'question/format',
8263 'plagiarism' => 'plagiarism',
8264 'tool' => $CFG->admin.'/tool',
8265 'cachestore' => 'cache/stores',
8266 'cachelock' => 'cache/locks',
8267 'theme' => 'theme', // this is a bit hacky, themes may be in $CFG->themedir too
8270 $subpluginowners = array_merge(array_values(get_plugin_list('mod')),
8271 array_values(get_plugin_list('editor')));
8272 foreach ($subpluginowners as $ownerdir) {
8273 if (file_exists("$ownerdir/db/subplugins.php")) {
8274 $subplugins = array();
8275 include("$ownerdir/db/subplugins.php");
8276 foreach ($subplugins as $subtype=>$dir) {
8277 $info[$subtype] = $dir;
8282 // local is always last!
8283 $info['local'] = 'local';
8285 $fullinfo = array();
8286 foreach ($info as $type => $dir) {
8287 $fullinfo[$type] = $CFG->dirroot.'/'.$dir;
8290 $cache->set(0, $info);
8291 $cache->set(1, $fullinfo);
8292 // We cache the dirroot as well so that we can compare it when we
8293 // retrieve full info from the cache.
8294 $cache->set('dirroot', $CFG->dirroot);
8296 return ($fullpaths ? $fullinfo : $info);
8301 * This method validates a plug name. It is much faster than calling clean_param.
8302 * @param string $name a string that might be a plugin name.
8303 * @return bool if this string is a valid plugin name.
8305 function is_valid_plugin_name($name) {
8306 return (bool) preg_match('/^[a-z](?:[a-z0-9_](?!__))*[a-z0-9]$/', $name);
8310 * Simplified version of get_list_of_plugins()
8311 * @param string $plugintype type of plugin
8312 * @return array name=>fulllocation pairs of plugins of given type
8314 function get_plugin_list($plugintype) {
8315 global $CFG;
8317 // We use the dirroot as an identifier here because if it has changed the whole cache
8318 // can be considered invalid.
8319 $cache = cache::make('core', 'pluginlist', array('dirroot' => $CFG->dirroot));
8320 $cached = $cache->get($plugintype);
8321 if ($cached !== false) {
8322 return $cached;
8325 $ignored = array('CVS', '_vti_cnf', 'simpletest', 'db', 'yui', 'tests');
8326 if ($plugintype == 'auth') {
8327 // Historically we have had an auth plugin called 'db', so allow a special case.
8328 $key = array_search('db', $ignored);
8329 if ($key !== false) {
8330 unset($ignored[$key]);
8334 if ($plugintype === '') {
8335 $plugintype = 'mod';
8338 $fulldirs = array();
8340 if ($plugintype === 'mod') {
8341 // mod is an exception because we have to call this function from get_plugin_types()
8342 $fulldirs[] = $CFG->dirroot.'/mod';
8344 } else if ($plugintype === 'editor') {
8345 // Exception also needed for editor for same reason.
8346 $fulldirs[] = $CFG->dirroot . '/lib/editor';
8348 } else if ($plugintype === 'theme') {
8349 $fulldirs[] = $CFG->dirroot.'/theme';
8350 // themes are special because they may be stored also in separate directory
8351 if (!empty($CFG->themedir) and file_exists($CFG->themedir) and is_dir($CFG->themedir) ) {
8352 $fulldirs[] = $CFG->themedir;
8355 } else {
8356 $types = get_plugin_types(true);
8357 if (!array_key_exists($plugintype, $types)) {
8358 $cache->set($plugintype, array());
8359 return array();
8361 $fulldir = $types[$plugintype];
8362 if (!file_exists($fulldir)) {
8363 $cache->set($plugintype, array());
8364 return array();
8366 $fulldirs[] = $fulldir;
8368 $result = array();
8370 foreach ($fulldirs as $fulldir) {
8371 if (!is_dir($fulldir)) {
8372 continue;
8374 $items = new DirectoryIterator($fulldir);
8375 foreach ($items as $item) {
8376 if ($item->isDot() or !$item->isDir()) {
8377 continue;
8379 $pluginname = $item->getFilename();
8380 if (in_array($pluginname, $ignored)) {
8381 continue;
8383 if (!is_valid_plugin_name($pluginname)) {
8384 // Better ignore plugins with problematic names here.
8385 continue;
8387 $result[$pluginname] = $fulldir.'/'.$pluginname;
8388 unset($item);
8390 unset($items);
8393 //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!
8394 ksort($result);
8395 $cache->set($plugintype, $result);
8396 return $result;
8400 * Get a list of all the plugins of a given type that contain a particular file.
8401 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
8402 * @param string $file the name of file that must be present in the plugin.
8403 * (e.g. 'view.php', 'db/install.xml').
8404 * @param bool $include if true (default false), the file will be include_once-ed if found.
8405 * @return array with plugin name as keys (e.g. 'forum', 'courselist') and the path
8406 * to the file relative to dirroot as value (e.g. "$CFG->dirroot/mod/forum/view.php").
8408 function get_plugin_list_with_file($plugintype, $file, $include = false) {
8409 global $CFG; // Necessary in case it is referenced by include()d PHP scripts.
8411 $plugins = array();
8413 foreach(get_plugin_list($plugintype) as $plugin => $dir) {
8414 $path = $dir . '/' . $file;
8415 if (file_exists($path)) {
8416 if ($include) {
8417 include_once($path);
8419 $plugins[$plugin] = $path;
8423 return $plugins;
8427 * Get a list of all the plugins of a given type that define a certain API function
8428 * in a certain file. The plugin component names and function names are returned.
8430 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
8431 * @param string $function the part of the name of the function after the
8432 * frankenstyle prefix. e.g 'hook' if you are looking for functions with
8433 * names like report_courselist_hook.
8434 * @param string $file the name of file within the plugin that defines the
8435 * function. Defaults to lib.php.
8436 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
8437 * and the function names as values (e.g. 'report_courselist_hook', 'forum_hook').
8439 function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php') {
8440 $pluginfunctions = array();
8441 foreach (get_plugin_list_with_file($plugintype, $file, true) as $plugin => $notused) {
8442 $fullfunction = $plugintype . '_' . $plugin . '_' . $function;
8444 if (function_exists($fullfunction)) {
8445 // Function exists with standard name. Store, indexed by
8446 // frankenstyle name of plugin
8447 $pluginfunctions[$plugintype . '_' . $plugin] = $fullfunction;
8449 } else if ($plugintype === 'mod') {
8450 // For modules, we also allow plugin without full frankenstyle
8451 // but just starting with the module name
8452 $shortfunction = $plugin . '_' . $function;
8453 if (function_exists($shortfunction)) {
8454 $pluginfunctions[$plugintype . '_' . $plugin] = $shortfunction;
8458 return $pluginfunctions;
8462 * Get a list of all the plugins of a given type that define a certain class
8463 * in a certain file. The plugin component names and class names are returned.
8465 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
8466 * @param string $class the part of the name of the class after the
8467 * frankenstyle prefix. e.g 'thing' if you are looking for classes with
8468 * names like report_courselist_thing. If you are looking for classes with
8469 * the same name as the plugin name (e.g. qtype_multichoice) then pass ''.
8470 * @param string $file the name of file within the plugin that defines the class.
8471 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
8472 * and the class names as values (e.g. 'report_courselist_thing', 'qtype_multichoice').
8474 function get_plugin_list_with_class($plugintype, $class, $file) {
8475 if ($class) {
8476 $suffix = '_' . $class;
8477 } else {
8478 $suffix = '';
8481 $pluginclasses = array();
8482 foreach (get_plugin_list_with_file($plugintype, $file, true) as $plugin => $notused) {
8483 $classname = $plugintype . '_' . $plugin . $suffix;
8484 if (class_exists($classname)) {
8485 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
8489 return $pluginclasses;
8493 * Lists plugin-like directories within specified directory
8495 * This function was originally used for standard Moodle plugins, please use
8496 * new get_plugin_list() now.
8498 * This function is used for general directory listing and backwards compatility.
8500 * @param string $directory relative directory from root
8501 * @param string $exclude dir name to exclude from the list (defaults to none)
8502 * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot)
8503 * @return array Sorted array of directory names found under the requested parameters
8505 function get_list_of_plugins($directory='mod', $exclude='', $basedir='') {
8506 global $CFG;
8508 $plugins = array();
8510 if (empty($basedir)) {
8511 $basedir = $CFG->dirroot .'/'. $directory;
8513 } else {
8514 $basedir = $basedir .'/'. $directory;
8517 if (file_exists($basedir) && filetype($basedir) == 'dir') {
8518 if (!$dirhandle = opendir($basedir)) {
8519 debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER);
8520 return array();
8522 while (false !== ($dir = readdir($dirhandle))) {
8523 $firstchar = substr($dir, 0, 1);
8524 if ($firstchar === '.' or $dir === 'CVS' or $dir === '_vti_cnf' or $dir === 'simpletest' or $dir === 'yui' or $dir === 'phpunit' or $dir === $exclude) {
8525 continue;
8527 if (filetype($basedir .'/'. $dir) != 'dir') {
8528 continue;
8530 $plugins[] = $dir;
8532 closedir($dirhandle);
8534 if ($plugins) {
8535 asort($plugins);
8537 return $plugins;
8541 * Invoke plugin's callback functions
8543 * @param string $type plugin type e.g. 'mod'
8544 * @param string $name plugin name
8545 * @param string $feature feature name
8546 * @param string $action feature's action
8547 * @param array $params parameters of callback function, should be an array
8548 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
8549 * @return mixed
8551 * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743
8553 function plugin_callback($type, $name, $feature, $action, $params = null, $default = null) {
8554 return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default);
8558 * Invoke component's callback functions
8560 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
8561 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
8562 * @param array $params parameters of callback function
8563 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
8564 * @return mixed
8566 function component_callback($component, $function, array $params = array(), $default = null) {
8567 global $CFG; // this is needed for require_once() below
8569 $cleancomponent = clean_param($component, PARAM_COMPONENT);
8570 if (empty($cleancomponent)) {
8571 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
8573 $component = $cleancomponent;
8575 list($type, $name) = normalize_component($component);
8576 $component = $type . '_' . $name;
8578 $oldfunction = $name.'_'.$function;
8579 $function = $component.'_'.$function;
8581 $dir = get_component_directory($component);
8582 if (empty($dir)) {
8583 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
8586 // Load library and look for function
8587 if (file_exists($dir.'/lib.php')) {
8588 require_once($dir.'/lib.php');
8591 if (!function_exists($function) and function_exists($oldfunction)) {
8592 if ($type !== 'mod' and $type !== 'core') {
8593 debugging("Please use new function name $function instead of legacy $oldfunction");
8595 $function = $oldfunction;
8598 if (function_exists($function)) {
8599 // Function exists, so just return function result
8600 $ret = call_user_func_array($function, $params);
8601 if (is_null($ret)) {
8602 return $default;
8603 } else {
8604 return $ret;
8607 return $default;
8611 * Checks whether a plugin supports a specified feature.
8613 * @param string $type Plugin type e.g. 'mod'
8614 * @param string $name Plugin name e.g. 'forum'
8615 * @param string $feature Feature code (FEATURE_xx constant)
8616 * @param mixed $default default value if feature support unknown
8617 * @return mixed Feature result (false if not supported, null if feature is unknown,
8618 * otherwise usually true but may have other feature-specific value such as array)
8620 function plugin_supports($type, $name, $feature, $default = NULL) {
8621 global $CFG;
8623 if ($type === 'mod' and $name === 'NEWMODULE') {
8624 //somebody forgot to rename the module template
8625 return false;
8628 $component = clean_param($type . '_' . $name, PARAM_COMPONENT);
8629 if (empty($component)) {
8630 throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name);
8633 $function = null;
8635 if ($type === 'mod') {
8636 // we need this special case because we support subplugins in modules,
8637 // otherwise it would end up in infinite loop
8638 if (file_exists("$CFG->dirroot/mod/$name/lib.php")) {
8639 include_once("$CFG->dirroot/mod/$name/lib.php");
8640 $function = $component.'_supports';
8641 if (!function_exists($function)) {
8642 // legacy non-frankenstyle function name
8643 $function = $name.'_supports';
8645 } else {
8646 // invalid module
8649 } else {
8650 if (!$path = get_plugin_directory($type, $name)) {
8651 // non existent plugin type
8652 return false;
8654 if (file_exists("$path/lib.php")) {
8655 include_once("$path/lib.php");
8656 $function = $component.'_supports';
8660 if ($function and function_exists($function)) {
8661 $supports = $function($feature);
8662 if (is_null($supports)) {
8663 // plugin does not know - use default
8664 return $default;
8665 } else {
8666 return $supports;
8670 //plugin does not care, so use default
8671 return $default;
8675 * Returns true if the current version of PHP is greater that the specified one.
8677 * @todo Check PHP version being required here is it too low?
8679 * @param string $version The version of php being tested.
8680 * @return bool
8682 function check_php_version($version='5.2.4') {
8683 return (version_compare(phpversion(), $version) >= 0);
8687 * Checks to see if is the browser operating system matches the specified
8688 * brand.
8690 * Known brand: 'Windows','Linux','Macintosh','SGI','SunOS','HP-UX'
8692 * @uses $_SERVER
8693 * @param string $brand The operating system identifier being tested
8694 * @return bool true if the given brand below to the detected operating system
8696 function check_browser_operating_system($brand) {
8697 if (empty($_SERVER['HTTP_USER_AGENT'])) {
8698 return false;
8701 if (preg_match("/$brand/i", $_SERVER['HTTP_USER_AGENT'])) {
8702 return true;
8705 return false;
8709 * Checks to see if is a browser matches the specified
8710 * brand and is equal or better version.
8712 * @uses $_SERVER
8713 * @param string $brand The browser identifier being tested
8714 * @param int $version The version of the browser, if not specified any version (except 5.5 for IE for BC reasons)
8715 * @return bool true if the given version is below that of the detected browser
8717 function check_browser_version($brand, $version = null) {
8718 if (empty($_SERVER['HTTP_USER_AGENT'])) {
8719 return false;
8722 $agent = $_SERVER['HTTP_USER_AGENT'];
8724 switch ($brand) {
8726 case 'Camino': /// OSX browser using Gecke engine
8727 if (strpos($agent, 'Camino') === false) {
8728 return false;
8730 if (empty($version)) {
8731 return true; // no version specified
8733 if (preg_match("/Camino\/([0-9\.]+)/i", $agent, $match)) {
8734 if (version_compare($match[1], $version) >= 0) {
8735 return true;
8738 break;
8741 case 'Firefox': /// Mozilla Firefox browsers
8742 if (strpos($agent, 'Iceweasel') === false and strpos($agent, 'Firefox') === false) {
8743 return false;
8745 if (empty($version)) {
8746 return true; // no version specified
8748 if (preg_match("/(Iceweasel|Firefox)\/([0-9\.]+)/i", $agent, $match)) {
8749 if (version_compare($match[2], $version) >= 0) {
8750 return true;
8753 break;
8756 case 'Gecko': /// Gecko based browsers
8757 // Do not look for dates any more, we expect real Firefox version here.
8758 if (empty($version)) {
8759 $version = 1;
8760 } else if ($version > 20000000) {
8761 // This is just a guess, it is not supposed to be 100% accurate!
8762 if (preg_match('/^201/', $version)) {
8763 $version = 3.6;
8764 } else if (preg_match('/^200[7-9]/', $version)) {
8765 $version = 3;
8766 } else if (preg_match('/^2006/', $version)) {
8767 $version = 2;
8768 } else {
8769 $version = 1.5;
8772 if (preg_match("/(Iceweasel|Firefox)\/([0-9\.]+)/i", $agent, $match)) {
8773 // Use real Firefox version if specified in user agent string.
8774 if (version_compare($match[2], $version) >= 0) {
8775 return true;
8777 } else if (preg_match("/Gecko\/([0-9\.]+)/i", $agent, $match)) {
8778 // Gecko might contain date or Firefox revision, let's just guess the Firefox version from the date.
8779 $browserver = $match[1];
8780 if ($browserver > 20000000) {
8781 // This is just a guess, it is not supposed to be 100% accurate!
8782 if (preg_match('/^201/', $browserver)) {
8783 $browserver = 3.6;
8784 } else if (preg_match('/^200[7-9]/', $browserver)) {
8785 $browserver = 3;
8786 } else if (preg_match('/^2006/', $version)) {
8787 $browserver = 2;
8788 } else {
8789 $browserver = 1.5;
8792 if (version_compare($browserver, $version) >= 0) {
8793 return true;
8796 break;
8799 case 'MSIE': /// Internet Explorer
8800 if (strpos($agent, 'Opera') !== false) { // Reject Opera
8801 return false;
8803 // In case of IE we have to deal with BC of the version parameter.
8804 if (is_null($version)) {
8805 $version = 5.5; // Anything older is not considered a browser at all!
8807 // IE uses simple versions, let's cast it to float to simplify the logic here.
8808 $version = round($version, 1);
8809 // See: http://www.useragentstring.com/pages/Internet%20Explorer/
8810 if (preg_match("/MSIE ([0-9\.]+)/", $agent, $match)) {
8811 $browser = $match[1];
8812 } else {
8813 return false;
8815 // IE8 and later versions may pretend to be IE7 for intranet sites, use Trident version instead,
8816 // the Trident should always describe the capabilities of IE in any emulation mode.
8817 if ($browser === '7.0' and preg_match("/Trident\/([0-9\.]+)/", $agent, $match)) {
8818 $browser = $match[1] + 4; // NOTE: Hopefully this will work also for future IE versions.
8820 $browser = round($browser, 1);
8821 return ($browser >= $version);
8822 break;
8825 case 'Opera': /// Opera
8826 if (strpos($agent, 'Opera') === false) {
8827 return false;
8829 if (empty($version)) {
8830 return true; // no version specified
8832 // Recent Opera useragents have Version/ with the actual version, e.g.:
8833 // Opera/9.80 (Windows NT 6.1; WOW64; U; en) Presto/2.10.289 Version/12.01
8834 // That's Opera 12.01, not 9.8.
8835 if (preg_match("/Version\/([0-9\.]+)/i", $agent, $match)) {
8836 if (version_compare($match[1], $version) >= 0) {
8837 return true;
8839 } else if (preg_match("/Opera\/([0-9\.]+)/i", $agent, $match)) {
8840 if (version_compare($match[1], $version) >= 0) {
8841 return true;
8844 break;
8847 case 'WebKit': /// WebKit based browser - everything derived from it (Safari, Chrome, iOS, Android and other mobiles)
8848 if (strpos($agent, 'AppleWebKit') === false) {
8849 return false;
8851 if (empty($version)) {
8852 return true; // no version specified
8854 if (preg_match("/AppleWebKit\/([0-9.]+)/i", $agent, $match)) {
8855 if (version_compare($match[1], $version) >= 0) {
8856 return true;
8859 break;
8862 case 'Safari': /// Desktop version of Apple Safari browser - no mobile or touch devices
8863 if (strpos($agent, 'AppleWebKit') === false) {
8864 return false;
8866 // Look for AppleWebKit, excluding strings with OmniWeb, Shiira and SymbianOS and any other mobile devices
8867 if (strpos($agent, 'OmniWeb')) { // Reject OmniWeb
8868 return false;
8870 if (strpos($agent, 'Shiira')) { // Reject Shiira
8871 return false;
8873 if (strpos($agent, 'SymbianOS')) { // Reject SymbianOS
8874 return false;
8876 if (strpos($agent, 'Android')) { // Reject Androids too
8877 return false;
8879 if (strpos($agent, 'iPhone') or strpos($agent, 'iPad') or strpos($agent, 'iPod')) {
8880 // No Apple mobile devices here - editor does not work, course ajax is not touch compatible, etc.
8881 return false;
8883 if (strpos($agent, 'Chrome')) { // Reject chrome browsers - it needs to be tested explicitly
8884 return false;
8887 if (empty($version)) {
8888 return true; // no version specified
8890 if (preg_match("/AppleWebKit\/([0-9.]+)/i", $agent, $match)) {
8891 if (version_compare($match[1], $version) >= 0) {
8892 return true;
8895 break;
8898 case 'Chrome':
8899 if (strpos($agent, 'Chrome') === false) {
8900 return false;
8902 if (empty($version)) {
8903 return true; // no version specified
8905 if (preg_match("/Chrome\/(.*)[ ]+/i", $agent, $match)) {
8906 if (version_compare($match[1], $version) >= 0) {
8907 return true;
8910 break;
8913 case 'Safari iOS': /// Safari on iPhone, iPad and iPod touch
8914 if (strpos($agent, 'AppleWebKit') === false or strpos($agent, 'Safari') === false) {
8915 return false;
8917 if (!strpos($agent, 'iPhone') and !strpos($agent, 'iPad') and !strpos($agent, 'iPod')) {
8918 return false;
8920 if (empty($version)) {
8921 return true; // no version specified
8923 if (preg_match("/AppleWebKit\/([0-9]+)/i", $agent, $match)) {
8924 if (version_compare($match[1], $version) >= 0) {
8925 return true;
8928 break;
8931 case 'WebKit Android': /// WebKit browser on Android
8932 if (strpos($agent, 'Linux; U; Android') === false) {
8933 return false;
8935 if (empty($version)) {
8936 return true; // no version specified
8938 if (preg_match("/AppleWebKit\/([0-9]+)/i", $agent, $match)) {
8939 if (version_compare($match[1], $version) >= 0) {
8940 return true;
8943 break;
8947 return false;
8951 * Returns whether a device/browser combination is mobile, tablet, legacy, default or the result of
8952 * an optional admin specified regular expression. If enabledevicedetection is set to no or not set
8953 * it returns default
8955 * @return string device type
8957 function get_device_type() {
8958 global $CFG;
8960 if (empty($CFG->enabledevicedetection) || empty($_SERVER['HTTP_USER_AGENT'])) {
8961 return 'default';
8964 $useragent = $_SERVER['HTTP_USER_AGENT'];
8966 if (!empty($CFG->devicedetectregex)) {
8967 $regexes = json_decode($CFG->devicedetectregex);
8969 foreach ($regexes as $value=>$regex) {
8970 if (preg_match($regex, $useragent)) {
8971 return $value;
8976 //mobile detection PHP direct copy from open source detectmobilebrowser.com
8977 $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';
8978 $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';
8979 if (preg_match($phonesregex,$useragent) || preg_match($modelsregex,substr($useragent, 0, 4))){
8980 return 'mobile';
8983 $tabletregex = '/Tablet browser|android|iPad|iProd|GT-P1000|GT-I9000|SHW-M180S|SGH-T849|SCH-I800|Build\/ERE27|sholest/i';
8984 if (preg_match($tabletregex, $useragent)) {
8985 return 'tablet';
8988 // Safe way to check for IE6 and not get false positives for some IE 7/8 users
8989 if (substr($_SERVER['HTTP_USER_AGENT'], 0, 34) === 'Mozilla/4.0 (compatible; MSIE 6.0;') {
8990 return 'legacy';
8993 return 'default';
8997 * Returns a list of the device types supporting by Moodle
8999 * @param boolean $incusertypes includes types specified using the devicedetectregex admin setting
9000 * @return array $types
9002 function get_device_type_list($incusertypes = true) {
9003 global $CFG;
9005 $types = array('default', 'legacy', 'mobile', 'tablet');
9007 if ($incusertypes && !empty($CFG->devicedetectregex)) {
9008 $regexes = json_decode($CFG->devicedetectregex);
9010 foreach ($regexes as $value => $regex) {
9011 $types[] = $value;
9015 return $types;
9019 * Returns the theme selected for a particular device or false if none selected.
9021 * @param string $devicetype
9022 * @return string|false The name of the theme to use for the device or the false if not set
9024 function get_selected_theme_for_device_type($devicetype = null) {
9025 global $CFG;
9027 if (empty($devicetype)) {
9028 $devicetype = get_user_device_type();
9031 $themevarname = get_device_cfg_var_name($devicetype);
9032 if (empty($CFG->$themevarname)) {
9033 return false;
9036 return $CFG->$themevarname;
9040 * Returns the name of the device type theme var in $CFG (because there is not a standard convention to allow backwards compatability
9042 * @param string $devicetype
9043 * @return string The config variable to use to determine the theme
9045 function get_device_cfg_var_name($devicetype = null) {
9046 if ($devicetype == 'default' || empty($devicetype)) {
9047 return 'theme';
9050 return 'theme' . $devicetype;
9054 * Allows the user to switch the device they are seeing the theme for.
9055 * This allows mobile users to switch back to the default theme, or theme for any other device.
9057 * @param string $newdevice The device the user is currently using.
9058 * @return string The device the user has switched to
9060 function set_user_device_type($newdevice) {
9061 global $USER;
9063 $devicetype = get_device_type();
9064 $devicetypes = get_device_type_list();
9066 if ($newdevice == $devicetype) {
9067 unset_user_preference('switchdevice'.$devicetype);
9068 } else if (in_array($newdevice, $devicetypes)) {
9069 set_user_preference('switchdevice'.$devicetype, $newdevice);
9074 * Returns the device the user is currently using, or if the user has chosen to switch devices
9075 * for the current device type the type they have switched to.
9077 * @return string The device the user is currently using or wishes to use
9079 function get_user_device_type() {
9080 $device = get_device_type();
9081 $switched = get_user_preferences('switchdevice'.$device, false);
9082 if ($switched != false) {
9083 return $switched;
9085 return $device;
9089 * Returns one or several CSS class names that match the user's browser. These can be put
9090 * in the body tag of the page to apply browser-specific rules without relying on CSS hacks
9092 * @return array An array of browser version classes
9094 function get_browser_version_classes() {
9095 $classes = array();
9097 if (check_browser_version("MSIE", "0")) {
9098 $classes[] = 'ie';
9099 for($i=12; $i>=6; $i--) {
9100 if (check_browser_version("MSIE", $i)) {
9101 $classes[] = 'ie'.$i;
9102 break;
9106 } else if (check_browser_version("Firefox") || check_browser_version("Gecko") || check_browser_version("Camino")) {
9107 $classes[] = 'gecko';
9108 if (preg_match('/rv\:([1-2])\.([0-9])/', $_SERVER['HTTP_USER_AGENT'], $matches)) {
9109 $classes[] = "gecko{$matches[1]}{$matches[2]}";
9112 } else if (check_browser_version("WebKit")) {
9113 $classes[] = 'safari';
9114 if (check_browser_version("Safari iOS")) {
9115 $classes[] = 'ios';
9117 } else if (check_browser_version("WebKit Android")) {
9118 $classes[] = 'android';
9121 } else if (check_browser_version("Opera")) {
9122 $classes[] = 'opera';
9126 return $classes;
9130 * Can handle rotated text. Whether it is safe to use the trickery in textrotate.js.
9132 * @return bool True for yes, false for no
9134 function can_use_rotated_text() {
9135 return check_browser_version('MSIE', 9) || check_browser_version('Firefox', 2) ||
9136 check_browser_version('Chrome', 21) || check_browser_version('Safari', 536.25) ||
9137 check_browser_version('Opera', 12) || check_browser_version('Safari iOS', 533);
9141 * Determine if moodle installation requires update
9143 * Checks version numbers of main code and all modules to see
9144 * if there are any mismatches
9146 * @global moodle_database $DB
9147 * @return bool
9149 function moodle_needs_upgrading() {
9150 global $CFG, $DB, $OUTPUT;
9152 if (empty($CFG->version)) {
9153 return true;
9156 // We have to purge plugin related caches now to be sure we have fresh data
9157 // and new plugins can be detected.
9158 cache::make('core', 'plugintypes')->purge();
9159 cache::make('core', 'pluginlist')->purge();
9160 cache::make('core', 'plugininfo_base')->purge();
9161 cache::make('core', 'plugininfo_mod')->purge();
9162 cache::make('core', 'plugininfo_block')->purge();
9163 cache::make('core', 'plugininfo_filter')->purge();
9164 cache::make('core', 'plugininfo_repository')->purge();
9165 cache::make('core', 'plugininfo_portfolio')->purge();
9167 // Check the main version first.
9168 $version = null;
9169 include($CFG->dirroot.'/version.php'); // defines $version and upgrades
9170 if ($version > $CFG->version) {
9171 return true;
9174 // modules
9175 $mods = get_plugin_list('mod');
9176 $installed = $DB->get_records('modules', array(), '', 'name, version');
9177 foreach ($mods as $mod => $fullmod) {
9178 if ($mod === 'NEWMODULE') { // Someone has unzipped the template, ignore it
9179 continue;
9181 $module = new stdClass();
9182 $plugin = new stdClass();
9183 if (!is_readable($fullmod.'/version.php')) {
9184 continue;
9186 include($fullmod.'/version.php'); // defines $module with version etc
9187 if (!isset($module->version) and isset($plugin->version)) {
9188 $module = $plugin;
9190 if (empty($installed[$mod])) {
9191 return true;
9192 } else if ($module->version > $installed[$mod]->version) {
9193 return true;
9196 unset($installed);
9198 // blocks
9199 $blocks = get_plugin_list('block');
9200 $installed = $DB->get_records('block', array(), '', 'name, version');
9201 require_once($CFG->dirroot.'/blocks/moodleblock.class.php');
9202 foreach ($blocks as $blockname=>$fullblock) {
9203 if ($blockname === 'NEWBLOCK') { // Someone has unzipped the template, ignore it
9204 continue;
9206 if (!is_readable($fullblock.'/version.php')) {
9207 continue;
9209 $plugin = new stdClass();
9210 $plugin->version = NULL;
9211 include($fullblock.'/version.php');
9212 if (empty($installed[$blockname])) {
9213 return true;
9214 } else if ($plugin->version > $installed[$blockname]->version) {
9215 return true;
9218 unset($installed);
9220 // now the rest of plugins
9221 $plugintypes = get_plugin_types();
9222 unset($plugintypes['mod']);
9223 unset($plugintypes['block']);
9225 $versions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin, value');
9226 foreach ($plugintypes as $type=>$unused) {
9227 $plugs = get_plugin_list($type);
9228 foreach ($plugs as $plug=>$fullplug) {
9229 $component = $type.'_'.$plug;
9230 if (!is_readable($fullplug.'/version.php')) {
9231 continue;
9233 $plugin = new stdClass();
9234 include($fullplug.'/version.php'); // defines $plugin with version etc
9235 if (array_key_exists($component, $versions)) {
9236 $installedversion = $versions[$component];
9237 } else {
9238 $installedversion = get_config($component, 'version');
9240 if (empty($installedversion)) { // new installation
9241 return true;
9242 } else if ($installedversion < $plugin->version) { // upgrade
9243 return true;
9248 return false;
9252 * Returns the major version of this site
9254 * Moodle version numbers consist of three numbers separated by a dot, for
9255 * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so
9256 * called major version. This function extracts the major version from either
9257 * $CFG->release (default) or eventually from the $release variable defined in
9258 * the main version.php.
9260 * @param bool $fromdisk should the version if source code files be used
9261 * @return string|false the major version like '2.3', false if could not be determined
9263 function moodle_major_version($fromdisk = false) {
9264 global $CFG;
9266 if ($fromdisk) {
9267 $release = null;
9268 require($CFG->dirroot.'/version.php');
9269 if (empty($release)) {
9270 return false;
9273 } else {
9274 if (empty($CFG->release)) {
9275 return false;
9277 $release = $CFG->release;
9280 if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) {
9281 return $matches[0];
9282 } else {
9283 return false;
9287 /// MISCELLANEOUS ////////////////////////////////////////////////////////////////////
9290 * Sets the system locale
9292 * @category string
9293 * @param string $locale Can be used to force a locale
9295 function moodle_setlocale($locale='') {
9296 global $CFG;
9298 static $currentlocale = ''; // last locale caching
9300 $oldlocale = $currentlocale;
9302 /// Fetch the correct locale based on ostype
9303 if ($CFG->ostype == 'WINDOWS') {
9304 $stringtofetch = 'localewin';
9305 } else {
9306 $stringtofetch = 'locale';
9309 /// the priority is the same as in get_string() - parameter, config, course, session, user, global language
9310 if (!empty($locale)) {
9311 $currentlocale = $locale;
9312 } else if (!empty($CFG->locale)) { // override locale for all language packs
9313 $currentlocale = $CFG->locale;
9314 } else {
9315 $currentlocale = get_string($stringtofetch, 'langconfig');
9318 /// do nothing if locale already set up
9319 if ($oldlocale == $currentlocale) {
9320 return;
9323 /// Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values,
9324 /// set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7
9325 /// Some day, numeric, monetary and other categories should be set too, I think. :-/
9327 /// Get current values
9328 $monetary= setlocale (LC_MONETARY, 0);
9329 $numeric = setlocale (LC_NUMERIC, 0);
9330 $ctype = setlocale (LC_CTYPE, 0);
9331 if ($CFG->ostype != 'WINDOWS') {
9332 $messages= setlocale (LC_MESSAGES, 0);
9334 /// Set locale to all
9335 setlocale (LC_ALL, $currentlocale);
9336 /// Set old values
9337 setlocale (LC_MONETARY, $monetary);
9338 setlocale (LC_NUMERIC, $numeric);
9339 if ($CFG->ostype != 'WINDOWS') {
9340 setlocale (LC_MESSAGES, $messages);
9342 if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') { // To workaround a well-known PHP problem with Turkish letter Ii
9343 setlocale (LC_CTYPE, $ctype);
9348 * Count words in a string.
9350 * Words are defined as things between whitespace.
9352 * @category string
9353 * @param string $string The text to be searched for words.
9354 * @return int The count of words in the specified string
9356 function count_words($string) {
9357 $string = strip_tags($string);
9358 return count(preg_split("/\w\b/", $string)) - 1;
9361 /** Count letters in a string.
9363 * Letters are defined as chars not in tags and different from whitespace.
9365 * @category string
9366 * @param string $string The text to be searched for letters.
9367 * @return int The count of letters in the specified text.
9369 function count_letters($string) {
9370 /// Loading the textlib singleton instance. We are going to need it.
9371 $string = strip_tags($string); // Tags are out now
9372 $string = preg_replace('/[[:space:]]*/','',$string); //Whitespace are out now
9374 return textlib::strlen($string);
9378 * Generate and return a random string of the specified length.
9380 * @param int $length The length of the string to be created.
9381 * @return string
9383 function random_string ($length=15) {
9384 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
9385 $pool .= 'abcdefghijklmnopqrstuvwxyz';
9386 $pool .= '0123456789';
9387 $poollen = strlen($pool);
9388 mt_srand ((double) microtime() * 1000000);
9389 $string = '';
9390 for ($i = 0; $i < $length; $i++) {
9391 $string .= substr($pool, (mt_rand()%($poollen)), 1);
9393 return $string;
9397 * Generate a complex random string (useful for md5 salts)
9399 * This function is based on the above {@link random_string()} however it uses a
9400 * larger pool of characters and generates a string between 24 and 32 characters
9402 * @param int $length Optional if set generates a string to exactly this length
9403 * @return string
9405 function complex_random_string($length=null) {
9406 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
9407 $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
9408 $poollen = strlen($pool);
9409 mt_srand ((double) microtime() * 1000000);
9410 if ($length===null) {
9411 $length = floor(rand(24,32));
9413 $string = '';
9414 for ($i = 0; $i < $length; $i++) {
9415 $string .= $pool[(mt_rand()%$poollen)];
9417 return $string;
9421 * Given some text (which may contain HTML) and an ideal length,
9422 * this function truncates the text neatly on a word boundary if possible
9424 * @category string
9425 * @global stdClass $CFG
9426 * @param string $text text to be shortened
9427 * @param int $ideal ideal string length
9428 * @param boolean $exact if false, $text will not be cut mid-word
9429 * @param string $ending The string to append if the passed string is truncated
9430 * @return string $truncate shortened string
9432 function shorten_text($text, $ideal=30, $exact = false, $ending='...') {
9434 global $CFG;
9436 // If the plain text is shorter than the maximum length, return the whole text.
9437 if (textlib::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) {
9438 return $text;
9441 // Splits on HTML tags. Each open/close/empty tag will be the first thing
9442 // and only tag in its 'line'.
9443 preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
9445 $total_length = textlib::strlen($ending);
9446 $truncate = '';
9448 // This array stores information about open and close tags and their position
9449 // in the truncated string. Each item in the array is an object with fields
9450 // ->open (true if open), ->tag (tag name in lower case), and ->pos
9451 // (byte position in truncated text).
9452 $tagdetails = array();
9454 foreach ($lines as $line_matchings) {
9455 // If there is any html-tag in this line, handle it and add it (uncounted) to the output.
9456 if (!empty($line_matchings[1])) {
9457 // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>).
9458 if (preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $line_matchings[1])) {
9459 // Do nothing.
9461 } else if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $line_matchings[1], $tag_matchings)) {
9462 // Record closing tag.
9463 $tagdetails[] = (object) array(
9464 'open' => false,
9465 'tag' => textlib::strtolower($tag_matchings[1]),
9466 'pos' => textlib::strlen($truncate),
9469 } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $line_matchings[1], $tag_matchings)) {
9470 // Record opening tag.
9471 $tagdetails[] = (object) array(
9472 'open' => true,
9473 'tag' => textlib::strtolower($tag_matchings[1]),
9474 'pos' => textlib::strlen($truncate),
9477 // Add html-tag to $truncate'd text.
9478 $truncate .= $line_matchings[1];
9481 // Calculate the length of the plain text part of the line; handle entities as one character.
9482 $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]));
9483 if ($total_length + $content_length > $ideal) {
9484 // The number of characters which are left.
9485 $left = $ideal - $total_length;
9486 $entities_length = 0;
9487 // Search for html entities.
9488 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)) {
9489 // calculate the real length of all entities in the legal range
9490 foreach ($entities[0] as $entity) {
9491 if ($entity[1]+1-$entities_length <= $left) {
9492 $left--;
9493 $entities_length += textlib::strlen($entity[0]);
9494 } else {
9495 // no more characters left
9496 break;
9500 $breakpos = $left + $entities_length;
9502 // if the words shouldn't be cut in the middle...
9503 if (!$exact) {
9504 // ...search the last occurence of a space...
9505 for (; $breakpos > 0; $breakpos--) {
9506 if ($char = textlib::substr($line_matchings[2], $breakpos, 1)) {
9507 if ($char === '.' or $char === ' ') {
9508 $breakpos += 1;
9509 break;
9510 } else if (strlen($char) > 2) { // Chinese/Japanese/Korean text
9511 $breakpos += 1; // can be truncated at any UTF-8
9512 break; // character boundary.
9517 if ($breakpos == 0) {
9518 // This deals with the test_shorten_text_no_spaces case.
9519 $breakpos = $left + $entities_length;
9520 } else if ($breakpos > $left + $entities_length) {
9521 // This deals with the previous for loop breaking on the first char.
9522 $breakpos = $left + $entities_length;
9525 $truncate .= textlib::substr($line_matchings[2], 0, $breakpos);
9526 // maximum length is reached, so get off the loop
9527 break;
9528 } else {
9529 $truncate .= $line_matchings[2];
9530 $total_length += $content_length;
9533 // If the maximum length is reached, get off the loop.
9534 if($total_length >= $ideal) {
9535 break;
9539 // Add the defined ending to the text.
9540 $truncate .= $ending;
9542 // Now calculate the list of open html tags based on the truncate position.
9543 $open_tags = array();
9544 foreach ($tagdetails as $taginfo) {
9545 if ($taginfo->open) {
9546 // Add tag to the beginning of $open_tags list.
9547 array_unshift($open_tags, $taginfo->tag);
9548 } else {
9549 // Can have multiple exact same open tags, close the last one.
9550 $pos = array_search($taginfo->tag, array_reverse($open_tags, true));
9551 if ($pos !== false) {
9552 unset($open_tags[$pos]);
9557 // Close all unclosed html-tags.
9558 foreach ($open_tags as $tag) {
9559 $truncate .= '</' . $tag . '>';
9562 return $truncate;
9567 * Given dates in seconds, how many weeks is the date from startdate
9568 * The first week is 1, the second 2 etc ...
9570 * @todo Finish documenting this function
9572 * @uses WEEKSECS
9573 * @param int $startdate Timestamp for the start date
9574 * @param int $thedate Timestamp for the end date
9575 * @return string
9577 function getweek ($startdate, $thedate) {
9578 if ($thedate < $startdate) { // error
9579 return 0;
9582 return floor(($thedate - $startdate) / WEEKSECS) + 1;
9586 * returns a randomly generated password of length $maxlen. inspired by
9588 * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and
9589 * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254}
9591 * @global stdClass $CFG
9592 * @param int $maxlen The maximum size of the password being generated.
9593 * @return string
9595 function generate_password($maxlen=10) {
9596 global $CFG;
9598 if (empty($CFG->passwordpolicy)) {
9599 $fillers = PASSWORD_DIGITS;
9600 $wordlist = file($CFG->wordlist);
9601 $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]);
9602 $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]);
9603 $filler1 = $fillers[rand(0, strlen($fillers) - 1)];
9604 $password = $word1 . $filler1 . $word2;
9605 } else {
9606 $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0;
9607 $digits = $CFG->minpassworddigits;
9608 $lower = $CFG->minpasswordlower;
9609 $upper = $CFG->minpasswordupper;
9610 $nonalphanum = $CFG->minpasswordnonalphanum;
9611 $total = $lower + $upper + $digits + $nonalphanum;
9612 // minlength should be the greater one of the two ( $minlen and $total )
9613 $minlen = $minlen < $total ? $total : $minlen;
9614 // maxlen can never be smaller than minlen
9615 $maxlen = $minlen > $maxlen ? $minlen : $maxlen;
9616 $additional = $maxlen - $total;
9618 // Make sure we have enough characters to fulfill
9619 // complexity requirements
9620 $passworddigits = PASSWORD_DIGITS;
9621 while ($digits > strlen($passworddigits)) {
9622 $passworddigits .= PASSWORD_DIGITS;
9624 $passwordlower = PASSWORD_LOWER;
9625 while ($lower > strlen($passwordlower)) {
9626 $passwordlower .= PASSWORD_LOWER;
9628 $passwordupper = PASSWORD_UPPER;
9629 while ($upper > strlen($passwordupper)) {
9630 $passwordupper .= PASSWORD_UPPER;
9632 $passwordnonalphanum = PASSWORD_NONALPHANUM;
9633 while ($nonalphanum > strlen($passwordnonalphanum)) {
9634 $passwordnonalphanum .= PASSWORD_NONALPHANUM;
9637 // Now mix and shuffle it all
9638 $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) .
9639 substr(str_shuffle ($passwordupper), 0, $upper) .
9640 substr(str_shuffle ($passworddigits), 0, $digits) .
9641 substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) .
9642 substr(str_shuffle ($passwordlower .
9643 $passwordupper .
9644 $passworddigits .
9645 $passwordnonalphanum), 0 , $additional));
9648 return substr ($password, 0, $maxlen);
9652 * Given a float, prints it nicely.
9653 * Localized floats must not be used in calculations!
9655 * The stripzeros feature is intended for making numbers look nicer in small
9656 * areas where it is not necessary to indicate the degree of accuracy by showing
9657 * ending zeros. If you turn it on with $decimalpoints set to 3, for example,
9658 * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'.
9660 * @param float $float The float to print
9661 * @param int $decimalpoints The number of decimal places to print.
9662 * @param bool $localized use localized decimal separator
9663 * @param bool $stripzeros If true, removes final zeros after decimal point
9664 * @return string locale float
9666 function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=false) {
9667 if (is_null($float)) {
9668 return '';
9670 if ($localized) {
9671 $separator = get_string('decsep', 'langconfig');
9672 } else {
9673 $separator = '.';
9675 $result = number_format($float, $decimalpoints, $separator, '');
9676 if ($stripzeros) {
9677 // Remove zeros and final dot if not needed
9678 $result = preg_replace('~(' . preg_quote($separator) . ')?0+$~', '', $result);
9680 return $result;
9684 * Converts locale specific floating point/comma number back to standard PHP float value
9685 * Do NOT try to do any math operations before this conversion on any user submitted floats!
9687 * @param string $locale_float locale aware float representation
9688 * @param bool $strict If true, then check the input and return false if it is not a valid number.
9689 * @return mixed float|bool - false or the parsed float.
9691 function unformat_float($locale_float, $strict = false) {
9692 $locale_float = trim($locale_float);
9694 if ($locale_float == '') {
9695 return null;
9698 $locale_float = str_replace(' ', '', $locale_float); // no spaces - those might be used as thousand separators
9699 $locale_float = str_replace(get_string('decsep', 'langconfig'), '.', $locale_float);
9701 if ($strict && !is_numeric($locale_float)) {
9702 return false;
9705 return (float)$locale_float;
9709 * Given a simple array, this shuffles it up just like shuffle()
9710 * Unlike PHP's shuffle() this function works on any machine.
9712 * @param array $array The array to be rearranged
9713 * @return array
9715 function swapshuffle($array) {
9717 srand ((double) microtime() * 10000000);
9718 $last = count($array) - 1;
9719 for ($i=0;$i<=$last;$i++) {
9720 $from = rand(0,$last);
9721 $curr = $array[$i];
9722 $array[$i] = $array[$from];
9723 $array[$from] = $curr;
9725 return $array;
9729 * Like {@link swapshuffle()}, but works on associative arrays
9731 * @param array $array The associative array to be rearranged
9732 * @return array
9734 function swapshuffle_assoc($array) {
9736 $newarray = array();
9737 $newkeys = swapshuffle(array_keys($array));
9739 foreach ($newkeys as $newkey) {
9740 $newarray[$newkey] = $array[$newkey];
9742 return $newarray;
9746 * Given an arbitrary array, and a number of draws,
9747 * this function returns an array with that amount
9748 * of items. The indexes are retained.
9750 * @todo Finish documenting this function
9752 * @param array $array
9753 * @param int $draws
9754 * @return array
9756 function draw_rand_array($array, $draws) {
9757 srand ((double) microtime() * 10000000);
9759 $return = array();
9761 $last = count($array);
9763 if ($draws > $last) {
9764 $draws = $last;
9767 while ($draws > 0) {
9768 $last--;
9770 $keys = array_keys($array);
9771 $rand = rand(0, $last);
9773 $return[$keys[$rand]] = $array[$keys[$rand]];
9774 unset($array[$keys[$rand]]);
9776 $draws--;
9779 return $return;
9783 * Calculate the difference between two microtimes
9785 * @param string $a The first Microtime
9786 * @param string $b The second Microtime
9787 * @return string
9789 function microtime_diff($a, $b) {
9790 list($a_dec, $a_sec) = explode(' ', $a);
9791 list($b_dec, $b_sec) = explode(' ', $b);
9792 return $b_sec - $a_sec + $b_dec - $a_dec;
9796 * Given a list (eg a,b,c,d,e) this function returns
9797 * an array of 1->a, 2->b, 3->c etc
9799 * @param string $list The string to explode into array bits
9800 * @param string $separator The separator used within the list string
9801 * @return array The now assembled array
9803 function make_menu_from_list($list, $separator=',') {
9805 $array = array_reverse(explode($separator, $list), true);
9806 foreach ($array as $key => $item) {
9807 $outarray[$key+1] = trim($item);
9809 return $outarray;
9813 * Creates an array that represents all the current grades that
9814 * can be chosen using the given grading type.
9816 * Negative numbers
9817 * are scales, zero is no grade, and positive numbers are maximum
9818 * grades.
9820 * @todo Finish documenting this function or better deprecated this completely!
9822 * @param int $gradingtype
9823 * @return array
9825 function make_grades_menu($gradingtype) {
9826 global $DB;
9828 $grades = array();
9829 if ($gradingtype < 0) {
9830 if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) {
9831 return make_menu_from_list($scale->scale);
9833 } else if ($gradingtype > 0) {
9834 for ($i=$gradingtype; $i>=0; $i--) {
9835 $grades[$i] = $i .' / '. $gradingtype;
9837 return $grades;
9839 return $grades;
9843 * This function returns the number of activities
9844 * using scaleid in a courseid
9846 * @todo Finish documenting this function
9848 * @global object
9849 * @global object
9850 * @param int $courseid ?
9851 * @param int $scaleid ?
9852 * @return int
9854 function course_scale_used($courseid, $scaleid) {
9855 global $CFG, $DB;
9857 $return = 0;
9859 if (!empty($scaleid)) {
9860 if ($cms = get_course_mods($courseid)) {
9861 foreach ($cms as $cm) {
9862 //Check cm->name/lib.php exists
9863 if (file_exists($CFG->dirroot.'/mod/'.$cm->modname.'/lib.php')) {
9864 include_once($CFG->dirroot.'/mod/'.$cm->modname.'/lib.php');
9865 $function_name = $cm->modname.'_scale_used';
9866 if (function_exists($function_name)) {
9867 if ($function_name($cm->instance,$scaleid)) {
9868 $return++;
9875 // check if any course grade item makes use of the scale
9876 $return += $DB->count_records('grade_items', array('courseid'=>$courseid, 'scaleid'=>$scaleid));
9878 // check if any outcome in the course makes use of the scale
9879 $return += $DB->count_records_sql("SELECT COUNT('x')
9880 FROM {grade_outcomes_courses} goc,
9881 {grade_outcomes} go
9882 WHERE go.id = goc.outcomeid
9883 AND go.scaleid = ? AND goc.courseid = ?",
9884 array($scaleid, $courseid));
9886 return $return;
9890 * This function returns the number of activities
9891 * using scaleid in the entire site
9893 * @param int $scaleid
9894 * @param array $courses
9895 * @return int
9897 function site_scale_used($scaleid, &$courses) {
9898 $return = 0;
9900 if (!is_array($courses) || count($courses) == 0) {
9901 $courses = get_courses("all",false,"c.id,c.shortname");
9904 if (!empty($scaleid)) {
9905 if (is_array($courses) && count($courses) > 0) {
9906 foreach ($courses as $course) {
9907 $return += course_scale_used($course->id,$scaleid);
9911 return $return;
9915 * make_unique_id_code
9917 * @todo Finish documenting this function
9919 * @uses $_SERVER
9920 * @param string $extra Extra string to append to the end of the code
9921 * @return string
9923 function make_unique_id_code($extra='') {
9925 $hostname = 'unknownhost';
9926 if (!empty($_SERVER['HTTP_HOST'])) {
9927 $hostname = $_SERVER['HTTP_HOST'];
9928 } else if (!empty($_ENV['HTTP_HOST'])) {
9929 $hostname = $_ENV['HTTP_HOST'];
9930 } else if (!empty($_SERVER['SERVER_NAME'])) {
9931 $hostname = $_SERVER['SERVER_NAME'];
9932 } else if (!empty($_ENV['SERVER_NAME'])) {
9933 $hostname = $_ENV['SERVER_NAME'];
9936 $date = gmdate("ymdHis");
9938 $random = random_string(6);
9940 if ($extra) {
9941 return $hostname .'+'. $date .'+'. $random .'+'. $extra;
9942 } else {
9943 return $hostname .'+'. $date .'+'. $random;
9949 * Function to check the passed address is within the passed subnet
9951 * The parameter is a comma separated string of subnet definitions.
9952 * Subnet strings can be in one of three formats:
9953 * 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn (number of bits in net mask)
9954 * 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)
9955 * 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. (incomplete address, a bit non-technical ;-)
9956 * Code for type 1 modified from user posted comments by mediator at
9957 * {@link http://au.php.net/manual/en/function.ip2long.php}
9959 * @param string $addr The address you are checking
9960 * @param string $subnetstr The string of subnet addresses
9961 * @return bool
9963 function address_in_subnet($addr, $subnetstr) {
9965 if ($addr == '0.0.0.0') {
9966 return false;
9968 $subnets = explode(',', $subnetstr);
9969 $found = false;
9970 $addr = trim($addr);
9971 $addr = cleanremoteaddr($addr, false); // normalise
9972 if ($addr === null) {
9973 return false;
9975 $addrparts = explode(':', $addr);
9977 $ipv6 = strpos($addr, ':');
9979 foreach ($subnets as $subnet) {
9980 $subnet = trim($subnet);
9981 if ($subnet === '') {
9982 continue;
9985 if (strpos($subnet, '/') !== false) {
9986 ///1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn
9987 list($ip, $mask) = explode('/', $subnet);
9988 $mask = trim($mask);
9989 if (!is_number($mask)) {
9990 continue; // incorect mask number, eh?
9992 $ip = cleanremoteaddr($ip, false); // normalise
9993 if ($ip === null) {
9994 continue;
9996 if (strpos($ip, ':') !== false) {
9997 // IPv6
9998 if (!$ipv6) {
9999 continue;
10001 if ($mask > 128 or $mask < 0) {
10002 continue; // nonsense
10004 if ($mask == 0) {
10005 return true; // any address
10007 if ($mask == 128) {
10008 if ($ip === $addr) {
10009 return true;
10011 continue;
10013 $ipparts = explode(':', $ip);
10014 $modulo = $mask % 16;
10015 $ipnet = array_slice($ipparts, 0, ($mask-$modulo)/16);
10016 $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16);
10017 if (implode(':', $ipnet) === implode(':', $addrnet)) {
10018 if ($modulo == 0) {
10019 return true;
10021 $pos = ($mask-$modulo)/16;
10022 $ipnet = hexdec($ipparts[$pos]);
10023 $addrnet = hexdec($addrparts[$pos]);
10024 $mask = 0xffff << (16 - $modulo);
10025 if (($addrnet & $mask) == ($ipnet & $mask)) {
10026 return true;
10030 } else {
10031 // IPv4
10032 if ($ipv6) {
10033 continue;
10035 if ($mask > 32 or $mask < 0) {
10036 continue; // nonsense
10038 if ($mask == 0) {
10039 return true;
10041 if ($mask == 32) {
10042 if ($ip === $addr) {
10043 return true;
10045 continue;
10047 $mask = 0xffffffff << (32 - $mask);
10048 if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
10049 return true;
10053 } else if (strpos($subnet, '-') !== false) {
10054 /// 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.
10055 $parts = explode('-', $subnet);
10056 if (count($parts) != 2) {
10057 continue;
10060 if (strpos($subnet, ':') !== false) {
10061 // IPv6
10062 if (!$ipv6) {
10063 continue;
10065 $ipstart = cleanremoteaddr(trim($parts[0]), false); // normalise
10066 if ($ipstart === null) {
10067 continue;
10069 $ipparts = explode(':', $ipstart);
10070 $start = hexdec(array_pop($ipparts));
10071 $ipparts[] = trim($parts[1]);
10072 $ipend = cleanremoteaddr(implode(':', $ipparts), false); // normalise
10073 if ($ipend === null) {
10074 continue;
10076 $ipparts[7] = '';
10077 $ipnet = implode(':', $ipparts);
10078 if (strpos($addr, $ipnet) !== 0) {
10079 continue;
10081 $ipparts = explode(':', $ipend);
10082 $end = hexdec($ipparts[7]);
10084 $addrend = hexdec($addrparts[7]);
10086 if (($addrend >= $start) and ($addrend <= $end)) {
10087 return true;
10090 } else {
10091 // IPv4
10092 if ($ipv6) {
10093 continue;
10095 $ipstart = cleanremoteaddr(trim($parts[0]), false); // normalise
10096 if ($ipstart === null) {
10097 continue;
10099 $ipparts = explode('.', $ipstart);
10100 $ipparts[3] = trim($parts[1]);
10101 $ipend = cleanremoteaddr(implode('.', $ipparts), false); // normalise
10102 if ($ipend === null) {
10103 continue;
10106 if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
10107 return true;
10111 } else {
10112 /// 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
10113 if (strpos($subnet, ':') !== false) {
10114 // IPv6
10115 if (!$ipv6) {
10116 continue;
10118 $parts = explode(':', $subnet);
10119 $count = count($parts);
10120 if ($parts[$count-1] === '') {
10121 unset($parts[$count-1]); // trim trailing :
10122 $count--;
10123 $subnet = implode('.', $parts);
10125 $isip = cleanremoteaddr($subnet, false); // normalise
10126 if ($isip !== null) {
10127 if ($isip === $addr) {
10128 return true;
10130 continue;
10131 } else if ($count > 8) {
10132 continue;
10134 $zeros = array_fill(0, 8-$count, '0');
10135 $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16);
10136 if (address_in_subnet($addr, $subnet)) {
10137 return true;
10140 } else {
10141 // IPv4
10142 if ($ipv6) {
10143 continue;
10145 $parts = explode('.', $subnet);
10146 $count = count($parts);
10147 if ($parts[$count-1] === '') {
10148 unset($parts[$count-1]); // trim trailing .
10149 $count--;
10150 $subnet = implode('.', $parts);
10152 if ($count == 4) {
10153 $subnet = cleanremoteaddr($subnet, false); // normalise
10154 if ($subnet === $addr) {
10155 return true;
10157 continue;
10158 } else if ($count > 4) {
10159 continue;
10161 $zeros = array_fill(0, 4-$count, '0');
10162 $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8);
10163 if (address_in_subnet($addr, $subnet)) {
10164 return true;
10170 return false;
10174 * For outputting debugging info
10176 * @uses STDOUT
10177 * @param string $string The string to write
10178 * @param string $eol The end of line char(s) to use
10179 * @param string $sleep Period to make the application sleep
10180 * This ensures any messages have time to display before redirect
10182 function mtrace($string, $eol="\n", $sleep=0) {
10184 if (defined('STDOUT') and !PHPUNIT_TEST) {
10185 fwrite(STDOUT, $string.$eol);
10186 } else {
10187 echo $string . $eol;
10190 flush();
10192 //delay to keep message on user's screen in case of subsequent redirect
10193 if ($sleep) {
10194 sleep($sleep);
10199 * Replace 1 or more slashes or backslashes to 1 slash
10201 * @param string $path The path to strip
10202 * @return string the path with double slashes removed
10204 function cleardoubleslashes ($path) {
10205 return preg_replace('/(\/|\\\){1,}/','/',$path);
10209 * Is current ip in give list?
10211 * @param string $list
10212 * @return bool
10214 function remoteip_in_list($list){
10215 $inlist = false;
10216 $client_ip = getremoteaddr(null);
10218 if(!$client_ip){
10219 // ensure access on cli
10220 return true;
10223 $list = explode("\n", $list);
10224 foreach($list as $subnet) {
10225 $subnet = trim($subnet);
10226 if (address_in_subnet($client_ip, $subnet)) {
10227 $inlist = true;
10228 break;
10231 return $inlist;
10235 * Returns most reliable client address
10237 * @global object
10238 * @param string $default If an address can't be determined, then return this
10239 * @return string The remote IP address
10241 function getremoteaddr($default='0.0.0.0') {
10242 global $CFG;
10244 if (empty($CFG->getremoteaddrconf)) {
10245 // This will happen, for example, before just after the upgrade, as the
10246 // user is redirected to the admin screen.
10247 $variablestoskip = 0;
10248 } else {
10249 $variablestoskip = $CFG->getremoteaddrconf;
10251 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) {
10252 if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
10253 $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']);
10254 return $address ? $address : $default;
10257 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
10258 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
10259 $address = cleanremoteaddr($_SERVER['HTTP_X_FORWARDED_FOR']);
10260 return $address ? $address : $default;
10263 if (!empty($_SERVER['REMOTE_ADDR'])) {
10264 $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']);
10265 return $address ? $address : $default;
10266 } else {
10267 return $default;
10272 * Cleans an ip address. Internal addresses are now allowed.
10273 * (Originally local addresses were not allowed.)
10275 * @param string $addr IPv4 or IPv6 address
10276 * @param bool $compress use IPv6 address compression
10277 * @return string normalised ip address string, null if error
10279 function cleanremoteaddr($addr, $compress=false) {
10280 $addr = trim($addr);
10282 //TODO: maybe add a separate function is_addr_public() or something like this
10284 if (strpos($addr, ':') !== false) {
10285 // can be only IPv6
10286 $parts = explode(':', $addr);
10287 $count = count($parts);
10289 if (strpos($parts[$count-1], '.') !== false) {
10290 //legacy ipv4 notation
10291 $last = array_pop($parts);
10292 $ipv4 = cleanremoteaddr($last, true);
10293 if ($ipv4 === null) {
10294 return null;
10296 $bits = explode('.', $ipv4);
10297 $parts[] = dechex($bits[0]).dechex($bits[1]);
10298 $parts[] = dechex($bits[2]).dechex($bits[3]);
10299 $count = count($parts);
10300 $addr = implode(':', $parts);
10303 if ($count < 3 or $count > 8) {
10304 return null; // severly malformed
10307 if ($count != 8) {
10308 if (strpos($addr, '::') === false) {
10309 return null; // malformed
10311 // uncompress ::
10312 $insertat = array_search('', $parts, true);
10313 $missing = array_fill(0, 1 + 8 - $count, '0');
10314 array_splice($parts, $insertat, 1, $missing);
10315 foreach ($parts as $key=>$part) {
10316 if ($part === '') {
10317 $parts[$key] = '0';
10322 $adr = implode(':', $parts);
10323 if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) {
10324 return null; // incorrect format - sorry
10327 // normalise 0s and case
10328 $parts = array_map('hexdec', $parts);
10329 $parts = array_map('dechex', $parts);
10331 $result = implode(':', $parts);
10333 if (!$compress) {
10334 return $result;
10337 if ($result === '0:0:0:0:0:0:0:0') {
10338 return '::'; // all addresses
10341 $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1);
10342 if ($compressed !== $result) {
10343 return $compressed;
10346 $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
10347 if ($compressed !== $result) {
10348 return $compressed;
10351 $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
10352 if ($compressed !== $result) {
10353 return $compressed;
10356 return $result;
10359 // first get all things that look like IPv4 addresses
10360 $parts = array();
10361 if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
10362 return null;
10364 unset($parts[0]);
10366 foreach ($parts as $key=>$match) {
10367 if ($match > 255) {
10368 return null;
10370 $parts[$key] = (int)$match; // normalise 0s
10373 return implode('.', $parts);
10377 * This function will make a complete copy of anything it's given,
10378 * regardless of whether it's an object or not.
10380 * @param mixed $thing Something you want cloned
10381 * @return mixed What ever it is you passed it
10383 function fullclone($thing) {
10384 return unserialize(serialize($thing));
10389 * This function expects to called during shutdown
10390 * should be set via register_shutdown_function()
10391 * in lib/setup.php .
10393 * @return void
10395 function moodle_request_shutdown() {
10396 global $CFG;
10398 // help apache server if possible
10399 $apachereleasemem = false;
10400 if (function_exists('apache_child_terminate') && function_exists('memory_get_usage')
10401 && ini_get_bool('child_terminate')) {
10403 $limit = (empty($CFG->apachemaxmem) ? 64*1024*1024 : $CFG->apachemaxmem); //64MB default
10404 if (memory_get_usage() > get_real_size($limit)) {
10405 $apachereleasemem = $limit;
10406 @apache_child_terminate();
10410 // deal with perf logging
10411 if (defined('MDL_PERF') || (!empty($CFG->perfdebug) and $CFG->perfdebug > 7)) {
10412 if ($apachereleasemem) {
10413 error_log('Mem usage over '.$apachereleasemem.': marking Apache child for reaping.');
10415 if (defined('MDL_PERFTOLOG')) {
10416 $perf = get_performance_info();
10417 error_log("PERF: " . $perf['txt']);
10419 if (defined('MDL_PERFINC')) {
10420 $inc = get_included_files();
10421 $ts = 0;
10422 foreach($inc as $f) {
10423 if (preg_match(':^/:', $f)) {
10424 $fs = filesize($f);
10425 $ts += $fs;
10426 $hfs = display_size($fs);
10427 error_log(substr($f,strlen($CFG->dirroot)) . " size: $fs ($hfs)"
10428 , NULL, NULL, 0);
10429 } else {
10430 error_log($f , NULL, NULL, 0);
10433 if ($ts > 0 ) {
10434 $hts = display_size($ts);
10435 error_log("Total size of files included: $ts ($hts)");
10442 * If new messages are waiting for the current user, then insert
10443 * JavaScript to pop up the messaging window into the page
10445 * @global moodle_page $PAGE
10446 * @return void
10448 function message_popup_window() {
10449 global $USER, $DB, $PAGE, $CFG, $SITE;
10451 if (!$PAGE->get_popup_notification_allowed() || empty($CFG->messaging)) {
10452 return;
10455 if (!isloggedin() || isguestuser()) {
10456 return;
10459 if (!isset($USER->message_lastpopup)) {
10460 $USER->message_lastpopup = 0;
10461 } else if ($USER->message_lastpopup > (time()-120)) {
10462 //dont run the query to check whether to display a popup if its been run in the last 2 minutes
10463 return;
10466 //a quick query to check whether the user has new messages
10467 $messagecount = $DB->count_records('message', array('useridto' => $USER->id));
10468 if ($messagecount<1) {
10469 return;
10472 //got unread messages so now do another query that joins with the user table
10473 $messagesql = "SELECT m.id, m.smallmessage, m.fullmessageformat, m.notification, u.firstname, u.lastname
10474 FROM {message} m
10475 JOIN {message_working} mw ON m.id=mw.unreadmessageid
10476 JOIN {message_processors} p ON mw.processorid=p.id
10477 JOIN {user} u ON m.useridfrom=u.id
10478 WHERE m.useridto = :userid
10479 AND p.name='popup'";
10481 //if the user was last notified over an hour ago we can renotify them of old messages
10482 //so don't worry about when the new message was sent
10483 $lastnotifiedlongago = $USER->message_lastpopup < (time()-3600);
10484 if (!$lastnotifiedlongago) {
10485 $messagesql .= 'AND m.timecreated > :lastpopuptime';
10488 $message_users = $DB->get_records_sql($messagesql, array('userid'=>$USER->id, 'lastpopuptime'=>$USER->message_lastpopup));
10490 //if we have new messages to notify the user about
10491 if (!empty($message_users)) {
10493 $strmessages = '';
10494 if (count($message_users)>1) {
10495 $strmessages = get_string('unreadnewmessages', 'message', count($message_users));
10496 } else {
10497 $message_users = reset($message_users);
10499 //show who the message is from if its not a notification
10500 if (!$message_users->notification) {
10501 $strmessages = get_string('unreadnewmessage', 'message', fullname($message_users) );
10504 //try to display the small version of the message
10505 $smallmessage = null;
10506 if (!empty($message_users->smallmessage)) {
10507 //display the first 200 chars of the message in the popup
10508 $smallmessage = null;
10509 if (textlib::strlen($message_users->smallmessage) > 200) {
10510 $smallmessage = textlib::substr($message_users->smallmessage,0,200).'...';
10511 } else {
10512 $smallmessage = $message_users->smallmessage;
10515 //prevent html symbols being displayed
10516 if ($message_users->fullmessageformat == FORMAT_HTML) {
10517 $smallmessage = html_to_text($smallmessage);
10518 } else {
10519 $smallmessage = s($smallmessage);
10521 } else if ($message_users->notification) {
10522 //its a notification with no smallmessage so just say they have a notification
10523 $smallmessage = get_string('unreadnewnotification', 'message');
10525 if (!empty($smallmessage)) {
10526 $strmessages .= '<div id="usermessage">'.s($smallmessage).'</div>';
10530 $strgomessage = get_string('gotomessages', 'message');
10531 $strstaymessage = get_string('ignore','admin');
10533 $url = $CFG->wwwroot.'/message/index.php';
10534 $content = html_writer::start_tag('div', array('id'=>'newmessageoverlay','class'=>'mdl-align')).
10535 html_writer::start_tag('div', array('id'=>'newmessagetext')).
10536 $strmessages.
10537 html_writer::end_tag('div').
10539 html_writer::start_tag('div', array('id'=>'newmessagelinks')).
10540 html_writer::link($url, $strgomessage, array('id'=>'notificationyes')).'&nbsp;&nbsp;&nbsp;'.
10541 html_writer::link('', $strstaymessage, array('id'=>'notificationno')).
10542 html_writer::end_tag('div');
10543 html_writer::end_tag('div');
10545 $PAGE->requires->js_init_call('M.core_message.init_notification', array('', $content, $url));
10547 $USER->message_lastpopup = time();
10552 * Used to make sure that $min <= $value <= $max
10554 * Make sure that value is between min, and max
10556 * @param int $min The minimum value
10557 * @param int $value The value to check
10558 * @param int $max The maximum value
10560 function bounded_number($min, $value, $max) {
10561 if($value < $min) {
10562 return $min;
10564 if($value > $max) {
10565 return $max;
10567 return $value;
10571 * Check if there is a nested array within the passed array
10573 * @param array $array
10574 * @return bool true if there is a nested array false otherwise
10576 function array_is_nested($array) {
10577 foreach ($array as $value) {
10578 if (is_array($value)) {
10579 return true;
10582 return false;
10586 * get_performance_info() pairs up with init_performance_info()
10587 * loaded in setup.php. Returns an array with 'html' and 'txt'
10588 * values ready for use, and each of the individual stats provided
10589 * separately as well.
10591 * @global object
10592 * @global object
10593 * @global object
10594 * @return array
10596 function get_performance_info() {
10597 global $CFG, $PERF, $DB, $PAGE;
10599 $info = array();
10600 $info['html'] = ''; // holds userfriendly HTML representation
10601 $info['txt'] = me() . ' '; // holds log-friendly representation
10603 $info['realtime'] = microtime_diff($PERF->starttime, microtime());
10605 $info['html'] .= '<span class="timeused">'.$info['realtime'].' secs</span> ';
10606 $info['txt'] .= 'time: '.$info['realtime'].'s ';
10608 if (function_exists('memory_get_usage')) {
10609 $info['memory_total'] = memory_get_usage();
10610 $info['memory_growth'] = memory_get_usage() - $PERF->startmemory;
10611 $info['html'] .= '<span class="memoryused">RAM: '.display_size($info['memory_total']).'</span> ';
10612 $info['txt'] .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '.$info['memory_growth'].'B ('.display_size($info['memory_growth']).') ';
10615 if (function_exists('memory_get_peak_usage')) {
10616 $info['memory_peak'] = memory_get_peak_usage();
10617 $info['html'] .= '<span class="memoryused">RAM peak: '.display_size($info['memory_peak']).'</span> ';
10618 $info['txt'] .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') ';
10621 $inc = get_included_files();
10622 //error_log(print_r($inc,1));
10623 $info['includecount'] = count($inc);
10624 $info['html'] .= '<span class="included">Included '.$info['includecount'].' files</span> ';
10625 $info['txt'] .= 'includecount: '.$info['includecount'].' ';
10627 if (!empty($CFG->early_install_lang) or empty($PAGE)) {
10628 // We can not track more performance before installation or before PAGE init, sorry.
10629 return $info;
10632 $filtermanager = filter_manager::instance();
10633 if (method_exists($filtermanager, 'get_performance_summary')) {
10634 list($filterinfo, $nicenames) = $filtermanager->get_performance_summary();
10635 $info = array_merge($filterinfo, $info);
10636 foreach ($filterinfo as $key => $value) {
10637 $info['html'] .= "<span class='$key'>$nicenames[$key]: $value </span> ";
10638 $info['txt'] .= "$key: $value ";
10642 $stringmanager = get_string_manager();
10643 if (method_exists($stringmanager, 'get_performance_summary')) {
10644 list($filterinfo, $nicenames) = $stringmanager->get_performance_summary();
10645 $info = array_merge($filterinfo, $info);
10646 foreach ($filterinfo as $key => $value) {
10647 $info['html'] .= "<span class='$key'>$nicenames[$key]: $value </span> ";
10648 $info['txt'] .= "$key: $value ";
10652 $jsmodules = $PAGE->requires->get_loaded_modules();
10653 if ($jsmodules) {
10654 $yuicount = 0;
10655 $othercount = 0;
10656 $details = '';
10657 foreach ($jsmodules as $module => $backtraces) {
10658 if (strpos($module, 'yui') === 0) {
10659 $yuicount += 1;
10660 } else {
10661 $othercount += 1;
10663 if (!empty($CFG->yuimoduledebug)) {
10664 // hidden feature for developers working on YUI module infrastructure
10665 $details .= "<div class='yui-module'><p>$module</p>";
10666 foreach ($backtraces as $backtrace) {
10667 $details .= "<div class='backtrace'>$backtrace</div>";
10669 $details .= '</div>';
10672 $info['html'] .= "<span class='includedyuimodules'>Included YUI modules: $yuicount</span> ";
10673 $info['txt'] .= "includedyuimodules: $yuicount ";
10674 $info['html'] .= "<span class='includedjsmodules'>Other JavaScript modules: $othercount</span> ";
10675 $info['txt'] .= "includedjsmodules: $othercount ";
10676 if ($details) {
10677 $info['html'] .= '<div id="yui-module-debug" class="notifytiny">'.$details.'</div>';
10681 if (!empty($PERF->logwrites)) {
10682 $info['logwrites'] = $PERF->logwrites;
10683 $info['html'] .= '<span class="logwrites">Log DB writes '.$info['logwrites'].'</span> ';
10684 $info['txt'] .= 'logwrites: '.$info['logwrites'].' ';
10687 $info['dbqueries'] = $DB->perf_get_reads().'/'.($DB->perf_get_writes() - $PERF->logwrites);
10688 $info['html'] .= '<span class="dbqueries">DB reads/writes: '.$info['dbqueries'].'</span> ';
10689 $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' ';
10691 if (function_exists('posix_times')) {
10692 $ptimes = posix_times();
10693 if (is_array($ptimes)) {
10694 foreach ($ptimes as $key => $val) {
10695 $info[$key] = $ptimes[$key] - $PERF->startposixtimes[$key];
10697 $info['html'] .= "<span class=\"posixtimes\">ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</span> ";
10698 $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] ";
10702 // Grab the load average for the last minute
10703 // /proc will only work under some linux configurations
10704 // while uptime is there under MacOSX/Darwin and other unices
10705 if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) {
10706 list($server_load) = explode(' ', $loadavg[0]);
10707 unset($loadavg);
10708 } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) {
10709 if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) {
10710 $server_load = $matches[1];
10711 } else {
10712 trigger_error('Could not parse uptime output!');
10715 if (!empty($server_load)) {
10716 $info['serverload'] = $server_load;
10717 $info['html'] .= '<span class="serverload">Load average: '.$info['serverload'].'</span> ';
10718 $info['txt'] .= "serverload: {$info['serverload']} ";
10721 // Display size of session if session started
10722 if (session_id()) {
10723 $info['sessionsize'] = display_size(strlen(session_encode()));
10724 $info['html'] .= '<span class="sessionsize">Session: ' . $info['sessionsize'] . '</span> ';
10725 $info['txt'] .= "Session: {$info['sessionsize']} ";
10728 if ($stats = cache_helper::get_stats()) {
10729 $html = '<span class="cachesused">';
10730 $html .= '<span class="cache-stats-heading">Caches used (hits/misses/sets)</span>';
10731 $text = 'Caches used (hits/misses/sets): ';
10732 $hits = 0;
10733 $misses = 0;
10734 $sets = 0;
10735 foreach ($stats as $definition => $stores) {
10736 $html .= '<span class="cache-definition-stats">';
10737 $html .= '<span class="cache-definition-stats-heading">'.$definition.'</span>';
10738 $text .= "$definition {";
10739 foreach ($stores as $store => $data) {
10740 $hits += $data['hits'];
10741 $misses += $data['misses'];
10742 $sets += $data['sets'];
10743 if ($data['hits'] == 0 and $data['misses'] > 0) {
10744 $cachestoreclass = 'nohits';
10745 } else if ($data['hits'] < $data['misses']) {
10746 $cachestoreclass = 'lowhits';
10747 } else {
10748 $cachestoreclass = 'hihits';
10750 $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
10751 $html .= "<span class=\"cache-store-stats $cachestoreclass\">$store: $data[hits] / $data[misses] / $data[sets]</span>";
10753 $html .= '</span>';
10754 $text .= '} ';
10756 $html .= "<span class='cache-total-stats'>Total: $hits / $misses / $sets</span>";
10757 $html .= '</span> ';
10758 $info['cachesused'] = "$hits / $misses / $sets";
10759 $info['html'] .= $html;
10760 $info['txt'] .= $text.'. ';
10761 } else {
10762 $info['cachesused'] = '0 / 0 / 0';
10763 $info['html'] .= '<span class="cachesused">Caches used (hits/misses/sets): 0/0/0</span>';
10764 $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 ';
10767 $info['html'] = '<div class="performanceinfo siteinfo">'.$info['html'].'</div>';
10768 return $info;
10772 * @todo Document this function linux people
10774 function apd_get_profiling() {
10775 return shell_exec('pprofp -u ' . ini_get('apd.dumpdir') . '/pprof.' . getmypid() . '.*');
10779 * Delete directory or only its content
10781 * @param string $dir directory path
10782 * @param bool $content_only
10783 * @return bool success, true also if dir does not exist
10785 function remove_dir($dir, $content_only=false) {
10786 if (!file_exists($dir)) {
10787 // nothing to do
10788 return true;
10790 if (!$handle = opendir($dir)) {
10791 return false;
10793 $result = true;
10794 while (false!==($item = readdir($handle))) {
10795 if($item != '.' && $item != '..') {
10796 if(is_dir($dir.'/'.$item)) {
10797 $result = remove_dir($dir.'/'.$item) && $result;
10798 }else{
10799 $result = unlink($dir.'/'.$item) && $result;
10803 closedir($handle);
10804 if ($content_only) {
10805 clearstatcache(); // make sure file stat cache is properly invalidated
10806 return $result;
10808 $result = rmdir($dir); // if anything left the result will be false, no need for && $result
10809 clearstatcache(); // make sure file stat cache is properly invalidated
10810 return $result;
10814 * Detect if an object or a class contains a given property
10815 * will take an actual object or the name of a class
10817 * @param mix $obj Name of class or real object to test
10818 * @param string $property name of property to find
10819 * @return bool true if property exists
10821 function object_property_exists( $obj, $property ) {
10822 if (is_string( $obj )) {
10823 $properties = get_class_vars( $obj );
10825 else {
10826 $properties = get_object_vars( $obj );
10828 return array_key_exists( $property, $properties );
10832 * Converts an object into an associative array
10834 * This function converts an object into an associative array by iterating
10835 * over its public properties. Because this function uses the foreach
10836 * construct, Iterators are respected. It works recursively on arrays of objects.
10837 * Arrays and simple values are returned as is.
10839 * If class has magic properties, it can implement IteratorAggregate
10840 * and return all available properties in getIterator()
10842 * @param mixed $var
10843 * @return array
10845 function convert_to_array($var) {
10846 $result = array();
10848 // loop over elements/properties
10849 foreach ($var as $key => $value) {
10850 // recursively convert objects
10851 if (is_object($value) || is_array($value)) {
10852 $result[$key] = convert_to_array($value);
10853 } else {
10854 // simple values are untouched
10855 $result[$key] = $value;
10858 return $result;
10862 * Detect a custom script replacement in the data directory that will
10863 * replace an existing moodle script
10865 * @return string|bool full path name if a custom script exists, false if no custom script exists
10867 function custom_script_path() {
10868 global $CFG, $SCRIPT;
10870 if ($SCRIPT === null) {
10871 // Probably some weird external script
10872 return false;
10875 $scriptpath = $CFG->customscripts . $SCRIPT;
10877 // check the custom script exists
10878 if (file_exists($scriptpath) and is_file($scriptpath)) {
10879 return $scriptpath;
10880 } else {
10881 return false;
10886 * Returns whether or not the user object is a remote MNET user. This function
10887 * is in moodlelib because it does not rely on loading any of the MNET code.
10889 * @global object
10890 * @param object $user A valid user object
10891 * @return bool True if the user is from a remote Moodle.
10893 function is_mnet_remote_user($user) {
10894 global $CFG;
10896 if (!isset($CFG->mnet_localhost_id)) {
10897 include_once $CFG->dirroot . '/mnet/lib.php';
10898 $env = new mnet_environment();
10899 $env->init();
10900 unset($env);
10903 return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id);
10907 * This function will search for browser prefereed languages, setting Moodle
10908 * to use the best one available if $SESSION->lang is undefined
10910 * @global object
10911 * @global object
10912 * @global object
10914 function setup_lang_from_browser() {
10916 global $CFG, $SESSION, $USER;
10918 if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) {
10919 // Lang is defined in session or user profile, nothing to do
10920 return;
10923 if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do
10924 return;
10927 /// Extract and clean langs from headers
10928 $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
10929 $rawlangs = str_replace('-', '_', $rawlangs); // we are using underscores
10930 $rawlangs = explode(',', $rawlangs); // Convert to array
10931 $langs = array();
10933 $order = 1.0;
10934 foreach ($rawlangs as $lang) {
10935 if (strpos($lang, ';') === false) {
10936 $langs[(string)$order] = $lang;
10937 $order = $order-0.01;
10938 } else {
10939 $parts = explode(';', $lang);
10940 $pos = strpos($parts[1], '=');
10941 $langs[substr($parts[1], $pos+1)] = $parts[0];
10944 krsort($langs, SORT_NUMERIC);
10946 /// Look for such langs under standard locations
10947 foreach ($langs as $lang) {
10948 $lang = strtolower(clean_param($lang, PARAM_SAFEDIR)); // clean it properly for include
10949 if (get_string_manager()->translation_exists($lang, false)) {
10950 $SESSION->lang = $lang; /// Lang exists, set it in session
10951 break; /// We have finished. Go out
10954 return;
10958 * check if $url matches anything in proxybypass list
10960 * any errors just result in the proxy being used (least bad)
10962 * @global object
10963 * @param string $url url to check
10964 * @return boolean true if we should bypass the proxy
10966 function is_proxybypass( $url ) {
10967 global $CFG;
10969 // sanity check
10970 if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) {
10971 return false;
10974 // get the host part out of the url
10975 if (!$host = parse_url( $url, PHP_URL_HOST )) {
10976 return false;
10979 // get the possible bypass hosts into an array
10980 $matches = explode( ',', $CFG->proxybypass );
10982 // check for a match
10983 // (IPs need to match the left hand side and hosts the right of the url,
10984 // but we can recklessly check both as there can't be a false +ve)
10985 $bypass = false;
10986 foreach ($matches as $match) {
10987 $match = trim($match);
10989 // try for IP match (Left side)
10990 $lhs = substr($host,0,strlen($match));
10991 if (strcasecmp($match,$lhs)==0) {
10992 return true;
10995 // try for host match (Right side)
10996 $rhs = substr($host,-strlen($match));
10997 if (strcasecmp($match,$rhs)==0) {
10998 return true;
11002 // nothing matched.
11003 return false;
11007 ////////////////////////////////////////////////////////////////////////////////
11010 * Check if the passed navigation is of the new style
11012 * @param mixed $navigation
11013 * @return bool true for yes false for no
11015 function is_newnav($navigation) {
11016 if (is_array($navigation) && !empty($navigation['newnav'])) {
11017 return true;
11018 } else {
11019 return false;
11024 * Checks whether the given variable name is defined as a variable within the given object.
11026 * This will NOT work with stdClass objects, which have no class variables.
11028 * @param string $var The variable name
11029 * @param object $object The object to check
11030 * @return boolean
11032 function in_object_vars($var, $object) {
11033 $class_vars = get_class_vars(get_class($object));
11034 $class_vars = array_keys($class_vars);
11035 return in_array($var, $class_vars);
11039 * Returns an array without repeated objects.
11040 * This function is similar to array_unique, but for arrays that have objects as values
11042 * @param array $array
11043 * @param bool $keep_key_assoc
11044 * @return array
11046 function object_array_unique($array, $keep_key_assoc = true) {
11047 $duplicate_keys = array();
11048 $tmp = array();
11050 foreach ($array as $key=>$val) {
11051 // convert objects to arrays, in_array() does not support objects
11052 if (is_object($val)) {
11053 $val = (array)$val;
11056 if (!in_array($val, $tmp)) {
11057 $tmp[] = $val;
11058 } else {
11059 $duplicate_keys[] = $key;
11063 foreach ($duplicate_keys as $key) {
11064 unset($array[$key]);
11067 return $keep_key_assoc ? $array : array_values($array);
11071 * Is a userid the primary administrator?
11073 * @param int $userid int id of user to check
11074 * @return boolean
11076 function is_primary_admin($userid){
11077 $primaryadmin = get_admin();
11079 if($userid == $primaryadmin->id){
11080 return true;
11081 }else{
11082 return false;
11087 * Returns the site identifier
11089 * @global object
11090 * @return string $CFG->siteidentifier, first making sure it is properly initialised.
11092 function get_site_identifier() {
11093 global $CFG;
11094 // Check to see if it is missing. If so, initialise it.
11095 if (empty($CFG->siteidentifier)) {
11096 set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']);
11098 // Return it.
11099 return $CFG->siteidentifier;
11103 * Check whether the given password has no more than the specified
11104 * number of consecutive identical characters.
11106 * @param string $password password to be checked against the password policy
11107 * @param integer $maxchars maximum number of consecutive identical characters
11109 function check_consecutive_identical_characters($password, $maxchars) {
11111 if ($maxchars < 1) {
11112 return true; // 0 is to disable this check
11114 if (strlen($password) <= $maxchars) {
11115 return true; // too short to fail this test
11118 $previouschar = '';
11119 $consecutivecount = 1;
11120 foreach (str_split($password) as $char) {
11121 if ($char != $previouschar) {
11122 $consecutivecount = 1;
11124 else {
11125 $consecutivecount++;
11126 if ($consecutivecount > $maxchars) {
11127 return false; // check failed already
11131 $previouschar = $char;
11134 return true;
11138 * helper function to do partial function binding
11139 * so we can use it for preg_replace_callback, for example
11140 * this works with php functions, user functions, static methods and class methods
11141 * it returns you a callback that you can pass on like so:
11143 * $callback = partial('somefunction', $arg1, $arg2);
11144 * or
11145 * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
11146 * or even
11147 * $obj = new someclass();
11148 * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2);
11150 * and then the arguments that are passed through at calltime are appended to the argument list.
11152 * @param mixed $function a php callback
11153 * $param mixed $arg1.. $argv arguments to partially bind with
11155 * @return callback
11157 function partial() {
11158 if (!class_exists('partial')) {
11159 class partial{
11160 var $values = array();
11161 var $func;
11163 function __construct($func, $args) {
11164 $this->values = $args;
11165 $this->func = $func;
11168 function method() {
11169 $args = func_get_args();
11170 return call_user_func_array($this->func, array_merge($this->values, $args));
11174 $args = func_get_args();
11175 $func = array_shift($args);
11176 $p = new partial($func, $args);
11177 return array($p, 'method');
11181 * helper function to load up and initialise the mnet environment
11182 * this must be called before you use mnet functions.
11184 * @return mnet_environment the equivalent of old $MNET global
11186 function get_mnet_environment() {
11187 global $CFG;
11188 require_once($CFG->dirroot . '/mnet/lib.php');
11189 static $instance = null;
11190 if (empty($instance)) {
11191 $instance = new mnet_environment();
11192 $instance->init();
11194 return $instance;
11198 * during xmlrpc server code execution, any code wishing to access
11199 * information about the remote peer must use this to get it.
11201 * @return mnet_remote_client the equivalent of old $MNET_REMOTE_CLIENT global
11203 function get_mnet_remote_client() {
11204 if (!defined('MNET_SERVER')) {
11205 debugging(get_string('notinxmlrpcserver', 'mnet'));
11206 return false;
11208 global $MNET_REMOTE_CLIENT;
11209 if (isset($MNET_REMOTE_CLIENT)) {
11210 return $MNET_REMOTE_CLIENT;
11212 return false;
11216 * during the xmlrpc server code execution, this will be called
11217 * to setup the object returned by {@see get_mnet_remote_client}
11219 * @param mnet_remote_client $client the client to set up
11221 function set_mnet_remote_client($client) {
11222 if (!defined('MNET_SERVER')) {
11223 throw new moodle_exception('notinxmlrpcserver', 'mnet');
11225 global $MNET_REMOTE_CLIENT;
11226 $MNET_REMOTE_CLIENT = $client;
11230 * return the jump url for a given remote user
11231 * this is used for rewriting forum post links in emails, etc
11233 * @param stdclass $user the user to get the idp url for
11235 function mnet_get_idp_jump_url($user) {
11236 global $CFG;
11238 static $mnetjumps = array();
11239 if (!array_key_exists($user->mnethostid, $mnetjumps)) {
11240 $idp = mnet_get_peer_host($user->mnethostid);
11241 $idpjumppath = mnet_get_app_jumppath($idp->applicationid);
11242 $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl=';
11244 return $mnetjumps[$user->mnethostid];
11248 * Gets the homepage to use for the current user
11250 * @return int One of HOMEPAGE_*
11252 function get_home_page() {
11253 global $CFG;
11255 if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) {
11256 if ($CFG->defaulthomepage == HOMEPAGE_MY) {
11257 return HOMEPAGE_MY;
11258 } else {
11259 return (int)get_user_preferences('user_home_page_preference', HOMEPAGE_MY);
11262 return HOMEPAGE_SITE;
11266 * Gets the name of a course to be displayed when showing a list of courses.
11267 * By default this is just $course->fullname but user can configure it. The
11268 * result of this function should be passed through print_string.
11269 * @param stdClass|course_in_list $course Moodle course object
11270 * @return string Display name of course (either fullname or short + fullname)
11272 function get_course_display_name_for_list($course) {
11273 global $CFG;
11274 if (!empty($CFG->courselistshortnames)) {
11275 if (!($course instanceof stdClass)) {
11276 $course = (object)convert_to_array($course);
11278 return get_string('courseextendednamedisplay', '', $course);
11279 } else {
11280 return $course->fullname;
11285 * The lang_string class
11287 * This special class is used to create an object representation of a string request.
11288 * It is special because processing doesn't occur until the object is first used.
11289 * The class was created especially to aid performance in areas where strings were
11290 * required to be generated but were not necessarily used.
11291 * As an example the admin tree when generated uses over 1500 strings, of which
11292 * normally only 1/3 are ever actually printed at any time.
11293 * The performance advantage is achieved by not actually processing strings that
11294 * arn't being used, as such reducing the processing required for the page.
11296 * How to use the lang_string class?
11297 * There are two methods of using the lang_string class, first through the
11298 * forth argument of the get_string function, and secondly directly.
11299 * The following are examples of both.
11300 * 1. Through get_string calls e.g.
11301 * $string = get_string($identifier, $component, $a, true);
11302 * $string = get_string('yes', 'moodle', null, true);
11303 * 2. Direct instantiation
11304 * $string = new lang_string($identifier, $component, $a, $lang);
11305 * $string = new lang_string('yes');
11307 * How do I use a lang_string object?
11308 * The lang_string object makes use of a magic __toString method so that you
11309 * are able to use the object exactly as you would use a string in most cases.
11310 * This means you are able to collect it into a variable and then directly
11311 * echo it, or concatenate it into another string, or similar.
11312 * The other thing you can do is manually get the string by calling the
11313 * lang_strings out method e.g.
11314 * $string = new lang_string('yes');
11315 * $string->out();
11316 * Also worth noting is that the out method can take one argument, $lang which
11317 * allows the developer to change the language on the fly.
11319 * When should I use a lang_string object?
11320 * The lang_string object is designed to be used in any situation where a
11321 * string may not be needed, but needs to be generated.
11322 * The admin tree is a good example of where lang_string objects should be
11323 * used.
11324 * A more practical example would be any class that requries strings that may
11325 * not be printed (after all classes get renderer by renderers and who knows
11326 * what they will do ;))
11328 * When should I not use a lang_string object?
11329 * Don't use lang_strings when you are going to use a string immediately.
11330 * There is no need as it will be processed immediately and there will be no
11331 * advantage, and in fact perhaps a negative hit as a class has to be
11332 * instantiated for a lang_string object, however get_string won't require
11333 * that.
11335 * Limitations:
11336 * 1. You cannot use a lang_string object as an array offset. Doing so will
11337 * result in PHP throwing an error. (You can use it as an object property!)
11339 * @package core
11340 * @category string
11341 * @copyright 2011 Sam Hemelryk
11342 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
11344 class lang_string {
11346 /** @var string The strings identifier */
11347 protected $identifier;
11348 /** @var string The strings component. Default '' */
11349 protected $component = '';
11350 /** @var array|stdClass Any arguments required for the string. Default null */
11351 protected $a = null;
11352 /** @var string The language to use when processing the string. Default null */
11353 protected $lang = null;
11355 /** @var string The processed string (once processed) */
11356 protected $string = null;
11359 * A special boolean. If set to true then the object has been woken up and
11360 * cannot be regenerated. If this is set then $this->string MUST be used.
11361 * @var bool
11363 protected $forcedstring = false;
11366 * Constructs a lang_string object
11368 * This function should do as little processing as possible to ensure the best
11369 * performance for strings that won't be used.
11371 * @param string $identifier The strings identifier
11372 * @param string $component The strings component
11373 * @param stdClass|array $a Any arguments the string requires
11374 * @param string $lang The language to use when processing the string.
11376 public function __construct($identifier, $component = '', $a = null, $lang = null) {
11377 if (empty($component)) {
11378 $component = 'moodle';
11381 $this->identifier = $identifier;
11382 $this->component = $component;
11383 $this->lang = $lang;
11385 // We MUST duplicate $a to ensure that it if it changes by reference those
11386 // changes are not carried across.
11387 // To do this we always ensure $a or its properties/values are strings
11388 // and that any properties/values that arn't convertable are forgotten.
11389 if (!empty($a)) {
11390 if (is_scalar($a)) {
11391 $this->a = $a;
11392 } else if ($a instanceof lang_string) {
11393 $this->a = $a->out();
11394 } else if (is_object($a) or is_array($a)) {
11395 $a = (array)$a;
11396 $this->a = array();
11397 foreach ($a as $key => $value) {
11398 // Make sure conversion errors don't get displayed (results in '')
11399 if (is_array($value)) {
11400 $this->a[$key] = '';
11401 } else if (is_object($value)) {
11402 if (method_exists($value, '__toString')) {
11403 $this->a[$key] = $value->__toString();
11404 } else {
11405 $this->a[$key] = '';
11407 } else {
11408 $this->a[$key] = (string)$value;
11414 if (debugging(false, DEBUG_DEVELOPER)) {
11415 if (clean_param($this->identifier, PARAM_STRINGID) == '') {
11416 throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition');
11418 if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') {
11419 throw new coding_exception('Invalid string compontent. Please check your string definition');
11421 if (!get_string_manager()->string_exists($this->identifier, $this->component)) {
11422 debugging('String does not exist. Please check your string definition for '.$this->identifier.'/'.$this->component, DEBUG_DEVELOPER);
11428 * Processes the string.
11430 * This function actually processes the string, stores it in the string property
11431 * and then returns it.
11432 * You will notice that this function is VERY similar to the get_string method.
11433 * That is because it is pretty much doing the same thing.
11434 * However as this function is an upgrade it isn't as tolerant to backwards
11435 * compatability.
11437 * @return string
11439 protected function get_string() {
11440 global $CFG;
11442 // Check if we need to process the string
11443 if ($this->string === null) {
11444 // Check the quality of the identifier.
11445 if (debugging('', DEBUG_DEVELOPER) && clean_param($this->identifier, PARAM_STRINGID) === '') {
11446 throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition');
11449 // Process the string
11450 $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang);
11451 // Debugging feature lets you display string identifier and component
11452 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
11453 $this->string .= ' {' . $this->identifier . '/' . $this->component . '}';
11456 // Return the string
11457 return $this->string;
11461 * Returns the string
11463 * @param string $lang The langauge to use when processing the string
11464 * @return string
11466 public function out($lang = null) {
11467 if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) {
11468 if ($this->forcedstring) {
11469 debugging('lang_string objects that have been serialised and unserialised cannot be printed in another language. ('.$this->lang.' used)', DEBUG_DEVELOPER);
11470 return $this->get_string();
11472 $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang);
11473 return $translatedstring->out();
11475 return $this->get_string();
11479 * Magic __toString method for printing a string
11481 * @return string
11483 public function __toString() {
11484 return $this->get_string();
11488 * Magic __set_state method used for var_export
11490 * @return string
11492 public function __set_state() {
11493 return $this->get_string();
11497 * Prepares the lang_string for sleep and stores only the forcedstring and
11498 * string properties... the string cannot be regenerated so we need to ensure
11499 * it is generated for this.
11501 * @return string
11503 public function __sleep() {
11504 $this->get_string();
11505 $this->forcedstring = true;
11506 return array('forcedstring', 'string', 'lang');