MDL-40392 Navigation -> my courses listing tests
[moodle.git] / lib / moodlelib.php
blob1b4581fe3b1b1897e4a9a8af4eb3167cab73dbfc
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 case 'application/vnd.moodle.profiling':
6061 $classname = 'zip_packer';
6062 break;
6063 case 'application/x-tar':
6064 // $classname = 'tar_packer';
6065 // break;
6066 default:
6067 return false;
6070 require_once("$CFG->libdir/filestorage/$classname.php");
6071 $fp[$mimetype] = new $classname();
6073 return $fp[$mimetype];
6077 * Returns current name of file on disk if it exists.
6079 * @param string $newfile File to be verified
6080 * @return string Current name of file on disk if true
6082 function valid_uploaded_file($newfile) {
6083 if (empty($newfile)) {
6084 return '';
6086 if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) {
6087 return $newfile['tmp_name'];
6088 } else {
6089 return '';
6094 * Returns the maximum size for uploading files.
6096 * There are seven possible upload limits:
6097 * 1. in Apache using LimitRequestBody (no way of checking or changing this)
6098 * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
6099 * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
6100 * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
6101 * 5. by the Moodle admin in $CFG->maxbytes
6102 * 6. by the teacher in the current course $course->maxbytes
6103 * 7. by the teacher for the current module, eg $assignment->maxbytes
6105 * These last two are passed to this function as arguments (in bytes).
6106 * Anything defined as 0 is ignored.
6107 * The smallest of all the non-zero numbers is returned.
6109 * @todo Finish documenting this function
6111 * @param int $sizebytes Set maximum size
6112 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6113 * @param int $modulebytes Current module ->maxbytes (in bytes)
6114 * @return int The maximum size for uploading files.
6116 function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0) {
6118 if (! $filesize = ini_get('upload_max_filesize')) {
6119 $filesize = '5M';
6121 $minimumsize = get_real_size($filesize);
6123 if ($postsize = ini_get('post_max_size')) {
6124 $postsize = get_real_size($postsize);
6125 if ($postsize < $minimumsize) {
6126 $minimumsize = $postsize;
6130 if (($sitebytes > 0) and ($sitebytes < $minimumsize)) {
6131 $minimumsize = $sitebytes;
6134 if (($coursebytes > 0) and ($coursebytes < $minimumsize)) {
6135 $minimumsize = $coursebytes;
6138 if (($modulebytes > 0) and ($modulebytes < $minimumsize)) {
6139 $minimumsize = $modulebytes;
6142 return $minimumsize;
6146 * Returns the maximum size for uploading files for the current user
6148 * This function takes in account @see:get_max_upload_file_size() the user's capabilities
6150 * @param context $context The context in which to check user capabilities
6151 * @param int $sizebytes Set maximum size
6152 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6153 * @param int $modulebytes Current module ->maxbytes (in bytes)
6154 * @param stdClass The user
6155 * @return int The maximum size for uploading files.
6157 function get_user_max_upload_file_size($context, $sitebytes=0, $coursebytes=0, $modulebytes=0, $user=null) {
6158 global $USER;
6160 if (empty($user)) {
6161 $user = $USER;
6164 if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) {
6165 return USER_CAN_IGNORE_FILE_SIZE_LIMITS;
6168 return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes);
6172 * Returns an array of possible sizes in local language
6174 * Related to {@link get_max_upload_file_size()} - this function returns an
6175 * array of possible sizes in an array, translated to the
6176 * local language.
6178 * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes.
6180 * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)"
6181 * with the value set to 0. This option will be the first in the list.
6183 * @global object
6184 * @uses SORT_NUMERIC
6185 * @param int $sizebytes Set maximum size
6186 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6187 * @param int $modulebytes Current module ->maxbytes (in bytes)
6188 * @param int|array $custombytes custom upload size/s which will be added to list,
6189 * Only value/s smaller then maxsize will be added to list.
6190 * @return array
6192 function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null) {
6193 global $CFG;
6195 if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) {
6196 return array();
6199 if ($sitebytes == 0) {
6200 // Will get the minimum of upload_max_filesize or post_max_size.
6201 $sitebytes = get_max_upload_file_size();
6204 $filesize = array();
6205 $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152,
6206 5242880, 10485760, 20971520, 52428800, 104857600);
6208 // If custombytes is given and is valid then add it to the list.
6209 if (is_number($custombytes) and $custombytes > 0) {
6210 $custombytes = (int)$custombytes;
6211 if (!in_array($custombytes, $sizelist)) {
6212 $sizelist[] = $custombytes;
6214 } else if (is_array($custombytes)) {
6215 $sizelist = array_unique(array_merge($sizelist, $custombytes));
6218 // Allow maxbytes to be selected if it falls outside the above boundaries
6219 if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) {
6220 // note: get_real_size() is used in order to prevent problems with invalid values
6221 $sizelist[] = get_real_size($CFG->maxbytes);
6224 foreach ($sizelist as $sizebytes) {
6225 if ($sizebytes < $maxsize && $sizebytes > 0) {
6226 $filesize[(string)intval($sizebytes)] = display_size($sizebytes);
6230 $limitlevel = '';
6231 $displaysize = '';
6232 if ($modulebytes &&
6233 (($modulebytes < $coursebytes || $coursebytes == 0) &&
6234 ($modulebytes < $sitebytes || $sitebytes == 0))) {
6235 $limitlevel = get_string('activity', 'core');
6236 $displaysize = display_size($modulebytes);
6237 $filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list.
6239 } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) {
6240 $limitlevel = get_string('course', 'core');
6241 $displaysize = display_size($coursebytes);
6242 $filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list.
6244 } else if ($sitebytes) {
6245 $limitlevel = get_string('site', 'core');
6246 $displaysize = display_size($sitebytes);
6247 $filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list.
6250 krsort($filesize, SORT_NUMERIC);
6251 if ($limitlevel) {
6252 $params = (object) array('contextname'=>$limitlevel, 'displaysize'=>$displaysize);
6253 $filesize = array('0'=>get_string('uploadlimitwithsize', 'core', $params)) + $filesize;
6256 return $filesize;
6260 * Returns an array with all the filenames in all subdirectories, relative to the given rootdir.
6262 * If excludefiles is defined, then that file/directory is ignored
6263 * If getdirs is true, then (sub)directories are included in the output
6264 * If getfiles is true, then files are included in the output
6265 * (at least one of these must be true!)
6267 * @todo Finish documenting this function. Add examples of $excludefile usage.
6269 * @param string $rootdir A given root directory to start from
6270 * @param string|array $excludefile If defined then the specified file/directory is ignored
6271 * @param bool $descend If true then subdirectories are recursed as well
6272 * @param bool $getdirs If true then (sub)directories are included in the output
6273 * @param bool $getfiles If true then files are included in the output
6274 * @return array An array with all the filenames in
6275 * all subdirectories, relative to the given rootdir
6277 function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) {
6279 $dirs = array();
6281 if (!$getdirs and !$getfiles) { // Nothing to show
6282 return $dirs;
6285 if (!is_dir($rootdir)) { // Must be a directory
6286 return $dirs;
6289 if (!$dir = opendir($rootdir)) { // Can't open it for some reason
6290 return $dirs;
6293 if (!is_array($excludefiles)) {
6294 $excludefiles = array($excludefiles);
6297 while (false !== ($file = readdir($dir))) {
6298 $firstchar = substr($file, 0, 1);
6299 if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) {
6300 continue;
6302 $fullfile = $rootdir .'/'. $file;
6303 if (filetype($fullfile) == 'dir') {
6304 if ($getdirs) {
6305 $dirs[] = $file;
6307 if ($descend) {
6308 $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles);
6309 foreach ($subdirs as $subdir) {
6310 $dirs[] = $file .'/'. $subdir;
6313 } else if ($getfiles) {
6314 $dirs[] = $file;
6317 closedir($dir);
6319 asort($dirs);
6321 return $dirs;
6326 * Adds up all the files in a directory and works out the size.
6328 * @todo Finish documenting this function
6330 * @param string $rootdir The directory to start from
6331 * @param string $excludefile A file to exclude when summing directory size
6332 * @return int The summed size of all files and subfiles within the root directory
6334 function get_directory_size($rootdir, $excludefile='') {
6335 global $CFG;
6337 // do it this way if we can, it's much faster
6338 if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) {
6339 $command = trim($CFG->pathtodu).' -sk '.escapeshellarg($rootdir);
6340 $output = null;
6341 $return = null;
6342 exec($command,$output,$return);
6343 if (is_array($output)) {
6344 return get_real_size(intval($output[0]).'k'); // we told it to return k.
6348 if (!is_dir($rootdir)) { // Must be a directory
6349 return 0;
6352 if (!$dir = @opendir($rootdir)) { // Can't open it for some reason
6353 return 0;
6356 $size = 0;
6358 while (false !== ($file = readdir($dir))) {
6359 $firstchar = substr($file, 0, 1);
6360 if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) {
6361 continue;
6363 $fullfile = $rootdir .'/'. $file;
6364 if (filetype($fullfile) == 'dir') {
6365 $size += get_directory_size($fullfile, $excludefile);
6366 } else {
6367 $size += filesize($fullfile);
6370 closedir($dir);
6372 return $size;
6376 * Converts bytes into display form
6378 * @todo Finish documenting this function. Verify return type.
6380 * @staticvar string $gb Localized string for size in gigabytes
6381 * @staticvar string $mb Localized string for size in megabytes
6382 * @staticvar string $kb Localized string for size in kilobytes
6383 * @staticvar string $b Localized string for size in bytes
6384 * @param int $size The size to convert to human readable form
6385 * @return string
6387 function display_size($size) {
6389 static $gb, $mb, $kb, $b;
6391 if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) {
6392 return get_string('unlimited');
6395 if (empty($gb)) {
6396 $gb = get_string('sizegb');
6397 $mb = get_string('sizemb');
6398 $kb = get_string('sizekb');
6399 $b = get_string('sizeb');
6402 if ($size >= 1073741824) {
6403 $size = round($size / 1073741824 * 10) / 10 . $gb;
6404 } else if ($size >= 1048576) {
6405 $size = round($size / 1048576 * 10) / 10 . $mb;
6406 } else if ($size >= 1024) {
6407 $size = round($size / 1024 * 10) / 10 . $kb;
6408 } else {
6409 $size = intval($size) .' '. $b; // file sizes over 2GB can not work in 32bit PHP anyway
6411 return $size;
6415 * Cleans a given filename by removing suspicious or troublesome characters
6416 * @see clean_param()
6418 * @uses PARAM_FILE
6419 * @param string $string file name
6420 * @return string cleaned file name
6422 function clean_filename($string) {
6423 return clean_param($string, PARAM_FILE);
6427 /// STRING TRANSLATION ////////////////////////////////////////
6430 * Returns the code for the current language
6432 * @category string
6433 * @return string
6435 function current_language() {
6436 global $CFG, $USER, $SESSION, $COURSE;
6438 if (!empty($COURSE->id) and $COURSE->id != SITEID and !empty($COURSE->lang)) { // Course language can override all other settings for this page
6439 $return = $COURSE->lang;
6441 } else if (!empty($SESSION->lang)) { // Session language can override other settings
6442 $return = $SESSION->lang;
6444 } else if (!empty($USER->lang)) {
6445 $return = $USER->lang;
6447 } else if (isset($CFG->lang)) {
6448 $return = $CFG->lang;
6450 } else {
6451 $return = 'en';
6454 $return = str_replace('_utf8', '', $return); // Just in case this slipped in from somewhere by accident
6456 return $return;
6460 * Returns parent language of current active language if defined
6462 * @category string
6463 * @uses COURSE
6464 * @uses SESSION
6465 * @param string $lang null means current language
6466 * @return string
6468 function get_parent_language($lang=null) {
6469 global $COURSE, $SESSION;
6471 //let's hack around the current language
6472 if (!empty($lang)) {
6473 $old_course_lang = empty($COURSE->lang) ? '' : $COURSE->lang;
6474 $old_session_lang = empty($SESSION->lang) ? '' : $SESSION->lang;
6475 $COURSE->lang = '';
6476 $SESSION->lang = $lang;
6479 $parentlang = get_string('parentlanguage', 'langconfig');
6480 if ($parentlang === 'en') {
6481 $parentlang = '';
6484 //let's hack around the current language
6485 if (!empty($lang)) {
6486 $COURSE->lang = $old_course_lang;
6487 $SESSION->lang = $old_session_lang;
6490 return $parentlang;
6494 * Returns current string_manager instance.
6496 * The param $forcereload is needed for CLI installer only where the string_manager instance
6497 * must be replaced during the install.php script life time.
6499 * @category string
6500 * @param bool $forcereload shall the singleton be released and new instance created instead?
6501 * @return string_manager
6503 function get_string_manager($forcereload=false) {
6504 global $CFG;
6506 static $singleton = null;
6508 if ($forcereload) {
6509 $singleton = null;
6511 if ($singleton === null) {
6512 if (empty($CFG->early_install_lang)) {
6514 if (empty($CFG->langlist)) {
6515 $translist = array();
6516 } else {
6517 $translist = explode(',', $CFG->langlist);
6520 if (empty($CFG->langmenucachefile)) {
6521 $langmenucache = $CFG->cachedir . '/languages';
6522 } else {
6523 $langmenucache = $CFG->langmenucachefile;
6526 $singleton = new core_string_manager($CFG->langotherroot, $CFG->langlocalroot,
6527 !empty($CFG->langstringcache), $translist, $langmenucache);
6529 } else {
6530 $singleton = new install_string_manager();
6534 return $singleton;
6539 * Interface for string manager
6541 * Interface describing class which is responsible for getting
6542 * of localised strings from language packs.
6544 * @package core
6545 * @copyright 2010 Petr Skoda (http://skodak.org)
6546 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6548 interface string_manager {
6550 * Get String returns a requested string
6552 * @param string $identifier The identifier of the string to search for
6553 * @param string $component The module the string is associated with
6554 * @param string|object|array $a An object, string or number that can be used
6555 * within translation strings
6556 * @param string $lang moodle translation language, NULL means use current
6557 * @return string The String !
6559 public function get_string($identifier, $component = '', $a = NULL, $lang = NULL);
6562 * Does the string actually exist?
6564 * get_string() is throwing debug warnings, sometimes we do not want them
6565 * or we want to display better explanation of the problem.
6567 * Use with care!
6569 * @param string $identifier The identifier of the string to search for
6570 * @param string $component The module the string is associated with
6571 * @return boot true if exists
6573 public function string_exists($identifier, $component);
6576 * Returns a localised list of all country names, sorted by country keys.
6577 * @param bool $returnall return all or just enabled
6578 * @param string $lang moodle translation language, NULL means use current
6579 * @return array two-letter country code => translated name.
6581 public function get_list_of_countries($returnall = false, $lang = NULL);
6584 * Returns a localised list of languages, sorted by code keys.
6586 * @param string $lang moodle translation language, NULL means use current
6587 * @param string $standard language list standard
6588 * iso6392: three-letter language code (ISO 639-2/T) => translated name.
6589 * @return array language code => translated name
6591 public function get_list_of_languages($lang = NULL, $standard = 'iso6392');
6594 * Checks if the translation exists for the language
6596 * @param string $lang moodle translation language code
6597 * @param bool $includeall include also disabled translations
6598 * @return bool true if exists
6600 public function translation_exists($lang, $includeall = true);
6603 * Returns localised list of installed translations
6604 * @param bool $returnall return all or just enabled
6605 * @return array moodle translation code => localised translation name
6607 public function get_list_of_translations($returnall = false);
6610 * Returns localised list of currencies.
6612 * @param string $lang moodle translation language, NULL means use current
6613 * @return array currency code => localised currency name
6615 public function get_list_of_currencies($lang = NULL);
6618 * Load all strings for one component
6619 * @param string $component The module the string is associated with
6620 * @param string $lang
6621 * @param bool $disablecache Do not use caches, force fetching the strings from sources
6622 * @param bool $disablelocal Do not use customized strings in xx_local language packs
6623 * @return array of all string for given component and lang
6625 public function load_component_strings($component, $lang, $disablecache=false, $disablelocal=false);
6628 * Invalidates all caches, should the implementation use any
6629 * @param bool $phpunitreset true means called from our PHPUnit integration test reset
6631 public function reset_caches($phpunitreset = false);
6634 * Returns string revision counter, this is incremented after any
6635 * string cache reset.
6636 * @return int lang string revision counter, -1 if unknown
6638 public function get_revision();
6643 * Standard string_manager implementation
6645 * Implements string_manager with getting and printing localised strings
6647 * @package core
6648 * @category string
6649 * @copyright 2010 Petr Skoda (http://skodak.org)
6650 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6652 class core_string_manager implements string_manager {
6653 /** @var string location of all packs except 'en' */
6654 protected $otherroot;
6655 /** @var string location of all lang pack local modifications */
6656 protected $localroot;
6657 /** @var cache lang string cache - it will be optimised more later */
6658 protected $cache;
6659 /** @var int get_string() counter */
6660 protected $countgetstring = 0;
6661 /** @var bool use disk cache */
6662 protected $usecache;
6663 /** @var array limit list of translations */
6664 protected $translist;
6665 /** @var string location of a file that caches the list of available translations */
6666 protected $menucache;
6669 * Create new instance of string manager
6671 * @param string $otherroot location of downlaoded lang packs - usually $CFG->dataroot/lang
6672 * @param string $localroot usually the same as $otherroot
6673 * @param bool $usecache use disk cache
6674 * @param array $translist limit list of visible translations
6675 * @param string $menucache the location of a file that caches the list of available translations
6677 public function __construct($otherroot, $localroot, $usecache, $translist, $menucache) {
6678 $this->otherroot = $otherroot;
6679 $this->localroot = $localroot;
6680 $this->usecache = $usecache;
6681 $this->translist = $translist;
6682 $this->menucache = $menucache;
6684 if ($this->usecache) {
6685 // We can use a proper cache, establish the cache using the 'String cache' definition.
6686 $this->cache = cache::make('core', 'string');
6687 } else {
6688 // We only want a cache for the length of the request, create a static cache.
6689 $options = array(
6690 'simplekeys' => true,
6691 'simpledata' => true
6693 $this->cache = cache::make_from_params(cache_store::MODE_REQUEST, 'core', 'string', array(), $options);
6698 * Returns list of all explicit parent languages for the given language.
6700 * English (en) is considered as the top implicit parent of all language packs
6701 * and is not included in the returned list. The language itself is appended to the
6702 * end of the list. The method is aware of circular dependency risk.
6704 * @see self::populate_parent_languages()
6705 * @param string $lang the code of the language
6706 * @return array all explicit parent languages with the lang itself appended
6708 public function get_language_dependencies($lang) {
6709 return $this->populate_parent_languages($lang);
6713 * Load all strings for one component
6715 * @param string $component The module the string is associated with
6716 * @param string $lang
6717 * @param bool $disablecache Do not use caches, force fetching the strings from sources
6718 * @param bool $disablelocal Do not use customized strings in xx_local language packs
6719 * @return array of all string for given component and lang
6721 public function load_component_strings($component, $lang, $disablecache=false, $disablelocal=false) {
6722 global $CFG;
6724 list($plugintype, $pluginname) = normalize_component($component);
6725 if ($plugintype == 'core' and is_null($pluginname)) {
6726 $component = 'core';
6727 } else {
6728 $component = $plugintype . '_' . $pluginname;
6731 $cachekey = $lang.'_'.$component;
6733 if (!$disablecache and !$disablelocal) {
6734 $string = $this->cache->get($cachekey);
6735 if ($string) {
6736 return $string;
6740 // no cache found - let us merge all possible sources of the strings
6741 if ($plugintype === 'core') {
6742 $file = $pluginname;
6743 if ($file === null) {
6744 $file = 'moodle';
6746 $string = array();
6747 // first load english pack
6748 if (!file_exists("$CFG->dirroot/lang/en/$file.php")) {
6749 return array();
6751 include("$CFG->dirroot/lang/en/$file.php");
6752 $originalkeys = array_keys($string);
6753 $originalkeys = array_flip($originalkeys);
6755 // and then corresponding local if present and allowed
6756 if (!$disablelocal and file_exists("$this->localroot/en_local/$file.php")) {
6757 include("$this->localroot/en_local/$file.php");
6759 // now loop through all langs in correct order
6760 $deps = $this->get_language_dependencies($lang);
6761 foreach ($deps as $dep) {
6762 // the main lang string location
6763 if (file_exists("$this->otherroot/$dep/$file.php")) {
6764 include("$this->otherroot/$dep/$file.php");
6766 if (!$disablelocal and file_exists("$this->localroot/{$dep}_local/$file.php")) {
6767 include("$this->localroot/{$dep}_local/$file.php");
6771 } else {
6772 if (!$location = get_plugin_directory($plugintype, $pluginname) or !is_dir($location)) {
6773 return array();
6775 if ($plugintype === 'mod') {
6776 // bloody mod hack
6777 $file = $pluginname;
6778 } else {
6779 $file = $plugintype . '_' . $pluginname;
6781 $string = array();
6782 // first load English pack
6783 if (!file_exists("$location/lang/en/$file.php")) {
6784 //English pack does not exist, so do not try to load anything else
6785 return array();
6787 include("$location/lang/en/$file.php");
6788 $originalkeys = array_keys($string);
6789 $originalkeys = array_flip($originalkeys);
6790 // and then corresponding local english if present
6791 if (!$disablelocal and file_exists("$this->localroot/en_local/$file.php")) {
6792 include("$this->localroot/en_local/$file.php");
6795 // now loop through all langs in correct order
6796 $deps = $this->get_language_dependencies($lang);
6797 foreach ($deps as $dep) {
6798 // legacy location - used by contrib only
6799 if (file_exists("$location/lang/$dep/$file.php")) {
6800 include("$location/lang/$dep/$file.php");
6802 // the main lang string location
6803 if (file_exists("$this->otherroot/$dep/$file.php")) {
6804 include("$this->otherroot/$dep/$file.php");
6806 // local customisations
6807 if (!$disablelocal and file_exists("$this->localroot/{$dep}_local/$file.php")) {
6808 include("$this->localroot/{$dep}_local/$file.php");
6813 // we do not want any extra strings from other languages - everything must be in en lang pack
6814 $string = array_intersect_key($string, $originalkeys);
6816 if (!$disablelocal) {
6817 // now we have a list of strings from all possible sources. put it into both in-memory and on-disk
6818 // caches so we do not need to do all this merging and dependencies resolving again
6819 $this->cache->set($cachekey, $string);
6821 return $string;
6825 * Does the string actually exist?
6827 * get_string() is throwing debug warnings, sometimes we do not want them
6828 * or we want to display better explanation of the problem.
6829 * Note: Use with care!
6831 * @param string $identifier The identifier of the string to search for
6832 * @param string $component The module the string is associated with
6833 * @return boot true if exists
6835 public function string_exists($identifier, $component) {
6836 $lang = current_language();
6837 $string = $this->load_component_strings($component, $lang);
6838 return isset($string[$identifier]);
6842 * Get String returns a requested string
6844 * @param string $identifier The identifier of the string to search for
6845 * @param string $component The module the string is associated with
6846 * @param string|object|array $a An object, string or number that can be used
6847 * within translation strings
6848 * @param string $lang moodle translation language, NULL means use current
6849 * @return string The String !
6851 public function get_string($identifier, $component = '', $a = NULL, $lang = NULL) {
6852 $this->countgetstring++;
6853 // there are very many uses of these time formating strings without the 'langconfig' component,
6854 // it would not be reasonable to expect that all of them would be converted during 2.0 migration
6855 static $langconfigstrs = array(
6856 'strftimedate' => 1,
6857 'strftimedatefullshort' => 1,
6858 'strftimedateshort' => 1,
6859 'strftimedatetime' => 1,
6860 'strftimedatetimeshort' => 1,
6861 'strftimedaydate' => 1,
6862 'strftimedaydatetime' => 1,
6863 'strftimedayshort' => 1,
6864 'strftimedaytime' => 1,
6865 'strftimemonthyear' => 1,
6866 'strftimerecent' => 1,
6867 'strftimerecentfull' => 1,
6868 'strftimetime' => 1);
6870 if (empty($component)) {
6871 if (isset($langconfigstrs[$identifier])) {
6872 $component = 'langconfig';
6873 } else {
6874 $component = 'moodle';
6878 if ($lang === NULL) {
6879 $lang = current_language();
6882 $string = $this->load_component_strings($component, $lang);
6884 if (!isset($string[$identifier])) {
6885 if ($component === 'pix' or $component === 'core_pix') {
6886 // this component contains only alt tags for emoticons,
6887 // not all of them are supposed to be defined
6888 return '';
6890 if ($identifier === 'parentlanguage' and ($component === 'langconfig' or $component === 'core_langconfig')) {
6891 // parentlanguage is a special string, undefined means use English if not defined
6892 return 'en';
6894 if ($this->usecache) {
6895 // maybe the on-disk cache is dirty - let the last attempt be to find the string in original sources,
6896 // do NOT write the results to disk cache because it may end up in race conditions see MDL-31904
6897 $this->usecache = false;
6898 $string = $this->load_component_strings($component, $lang, true);
6899 $this->usecache = true;
6901 if (!isset($string[$identifier])) {
6902 // the string is still missing - should be fixed by developer
6903 list($plugintype, $pluginname) = normalize_component($component);
6904 if ($plugintype == 'core') {
6905 $file = "lang/en/{$component}.php";
6906 } else if ($plugintype == 'mod') {
6907 $file = "mod/{$pluginname}/lang/en/{$pluginname}.php";
6908 } else {
6909 $path = get_plugin_directory($plugintype, $pluginname);
6910 $file = "{$path}/lang/en/{$plugintype}_{$pluginname}.php";
6912 debugging("Invalid get_string() identifier: '{$identifier}' or component '{$component}'. " .
6913 "Perhaps you are missing \$string['{$identifier}'] = ''; in {$file}?", DEBUG_DEVELOPER);
6914 return "[[$identifier]]";
6918 $string = $string[$identifier];
6920 if ($a !== NULL) {
6921 // Process array's and objects (except lang_strings)
6922 if (is_array($a) or (is_object($a) && !($a instanceof lang_string))) {
6923 $a = (array)$a;
6924 $search = array();
6925 $replace = array();
6926 foreach ($a as $key=>$value) {
6927 if (is_int($key)) {
6928 // we do not support numeric keys - sorry!
6929 continue;
6931 if (is_array($value) or (is_object($value) && !($value instanceof lang_string))) {
6932 // we support just string or lang_string as value
6933 continue;
6935 $search[] = '{$a->'.$key.'}';
6936 $replace[] = (string)$value;
6938 if ($search) {
6939 $string = str_replace($search, $replace, $string);
6941 } else {
6942 $string = str_replace('{$a}', (string)$a, $string);
6946 return $string;
6950 * Returns information about the string_manager performance
6952 * @return array
6954 public function get_performance_summary() {
6955 return array(array(
6956 'langcountgetstring' => $this->countgetstring,
6957 ), array(
6958 'langcountgetstring' => 'get_string calls',
6963 * Returns a localised list of all country names, sorted by localised name.
6965 * @param bool $returnall return all or just enabled
6966 * @param string $lang moodle translation language, NULL means use current
6967 * @return array two-letter country code => translated name.
6969 public function get_list_of_countries($returnall = false, $lang = NULL) {
6970 global $CFG;
6972 if ($lang === NULL) {
6973 $lang = current_language();
6976 $countries = $this->load_component_strings('core_countries', $lang);
6977 collatorlib::asort($countries);
6978 if (!$returnall and !empty($CFG->allcountrycodes)) {
6979 $enabled = explode(',', $CFG->allcountrycodes);
6980 $return = array();
6981 foreach ($enabled as $c) {
6982 if (isset($countries[$c])) {
6983 $return[$c] = $countries[$c];
6986 return $return;
6989 return $countries;
6993 * Returns a localised list of languages, sorted by code keys.
6995 * @param string $lang moodle translation language, NULL means use current
6996 * @param string $standard language list standard
6997 * - iso6392: three-letter language code (ISO 639-2/T) => translated name
6998 * - iso6391: two-letter langauge code (ISO 639-1) => translated name
6999 * @return array language code => translated name
7001 public function get_list_of_languages($lang = NULL, $standard = 'iso6391') {
7002 if ($lang === NULL) {
7003 $lang = current_language();
7006 if ($standard === 'iso6392') {
7007 $langs = $this->load_component_strings('core_iso6392', $lang);
7008 ksort($langs);
7009 return $langs;
7011 } else if ($standard === 'iso6391') {
7012 $langs2 = $this->load_component_strings('core_iso6392', $lang);
7013 static $mapping = array('aar' => 'aa', 'abk' => 'ab', 'afr' => 'af', 'aka' => 'ak', 'sqi' => 'sq', 'amh' => 'am', 'ara' => 'ar', 'arg' => 'an', 'hye' => 'hy',
7014 'asm' => 'as', 'ava' => 'av', 'ave' => 'ae', 'aym' => 'ay', 'aze' => 'az', 'bak' => 'ba', 'bam' => 'bm', 'eus' => 'eu', 'bel' => 'be', 'ben' => 'bn', 'bih' => 'bh',
7015 'bis' => 'bi', 'bos' => 'bs', 'bre' => 'br', 'bul' => 'bg', 'mya' => 'my', 'cat' => 'ca', 'cha' => 'ch', 'che' => 'ce', 'zho' => 'zh', 'chu' => 'cu', 'chv' => 'cv',
7016 'cor' => 'kw', 'cos' => 'co', 'cre' => 'cr', 'ces' => 'cs', 'dan' => 'da', 'div' => 'dv', 'nld' => 'nl', 'dzo' => 'dz', 'eng' => 'en', 'epo' => 'eo', 'est' => 'et',
7017 'ewe' => 'ee', 'fao' => 'fo', 'fij' => 'fj', 'fin' => 'fi', 'fra' => 'fr', 'fry' => 'fy', 'ful' => 'ff', 'kat' => 'ka', 'deu' => 'de', 'gla' => 'gd', 'gle' => 'ga',
7018 'glg' => 'gl', 'glv' => 'gv', 'ell' => 'el', 'grn' => 'gn', 'guj' => 'gu', 'hat' => 'ht', 'hau' => 'ha', 'heb' => 'he', 'her' => 'hz', 'hin' => 'hi', 'hmo' => 'ho',
7019 'hrv' => 'hr', 'hun' => 'hu', 'ibo' => 'ig', 'isl' => 'is', 'ido' => 'io', 'iii' => 'ii', 'iku' => 'iu', 'ile' => 'ie', 'ina' => 'ia', 'ind' => 'id', 'ipk' => 'ik',
7020 'ita' => 'it', 'jav' => 'jv', 'jpn' => 'ja', 'kal' => 'kl', 'kan' => 'kn', 'kas' => 'ks', 'kau' => 'kr', 'kaz' => 'kk', 'khm' => 'km', 'kik' => 'ki', 'kin' => 'rw',
7021 'kir' => 'ky', 'kom' => 'kv', 'kon' => 'kg', 'kor' => 'ko', 'kua' => 'kj', 'kur' => 'ku', 'lao' => 'lo', 'lat' => 'la', 'lav' => 'lv', 'lim' => 'li', 'lin' => 'ln',
7022 'lit' => 'lt', 'ltz' => 'lb', 'lub' => 'lu', 'lug' => 'lg', 'mkd' => 'mk', 'mah' => 'mh', 'mal' => 'ml', 'mri' => 'mi', 'mar' => 'mr', 'msa' => 'ms', 'mlg' => 'mg',
7023 'mlt' => 'mt', 'mon' => 'mn', 'nau' => 'na', 'nav' => 'nv', 'nbl' => 'nr', 'nde' => 'nd', 'ndo' => 'ng', 'nep' => 'ne', 'nno' => 'nn', 'nob' => 'nb', 'nor' => 'no',
7024 'nya' => 'ny', 'oci' => 'oc', 'oji' => 'oj', 'ori' => 'or', 'orm' => 'om', 'oss' => 'os', 'pan' => 'pa', 'fas' => 'fa', 'pli' => 'pi', 'pol' => 'pl', 'por' => 'pt',
7025 'pus' => 'ps', 'que' => 'qu', 'roh' => 'rm', 'ron' => 'ro', 'run' => 'rn', 'rus' => 'ru', 'sag' => 'sg', 'san' => 'sa', 'sin' => 'si', 'slk' => 'sk', 'slv' => 'sl',
7026 'sme' => 'se', 'smo' => 'sm', 'sna' => 'sn', 'snd' => 'sd', 'som' => 'so', 'sot' => 'st', 'spa' => 'es', 'srd' => 'sc', 'srp' => 'sr', 'ssw' => 'ss', 'sun' => 'su',
7027 'swa' => 'sw', 'swe' => 'sv', 'tah' => 'ty', 'tam' => 'ta', 'tat' => 'tt', 'tel' => 'te', 'tgk' => 'tg', 'tgl' => 'tl', 'tha' => 'th', 'bod' => 'bo', 'tir' => 'ti',
7028 'ton' => 'to', 'tsn' => 'tn', 'tso' => 'ts', 'tuk' => 'tk', 'tur' => 'tr', 'twi' => 'tw', 'uig' => 'ug', 'ukr' => 'uk', 'urd' => 'ur', 'uzb' => 'uz', 'ven' => 've',
7029 'vie' => 'vi', 'vol' => 'vo', 'cym' => 'cy', 'wln' => 'wa', 'wol' => 'wo', 'xho' => 'xh', 'yid' => 'yi', 'yor' => 'yo', 'zha' => 'za', 'zul' => 'zu');
7030 $langs1 = array();
7031 foreach ($mapping as $c2=>$c1) {
7032 $langs1[$c1] = $langs2[$c2];
7034 ksort($langs1);
7035 return $langs1;
7037 } else {
7038 debugging('Unsupported $standard parameter in get_list_of_languages() method: '.$standard);
7041 return array();
7045 * Checks if the translation exists for the language
7047 * @param string $lang moodle translation language code
7048 * @param bool $includeall include also disabled translations
7049 * @return bool true if exists
7051 public function translation_exists($lang, $includeall = true) {
7053 if (strpos($lang, '_local') !== false) {
7054 // _local packs are not real translations
7055 return false;
7057 if (!$includeall and !empty($this->translist)) {
7058 if (!in_array($lang, $this->translist)) {
7059 return false;
7062 if ($lang === 'en') {
7063 // part of distribution
7064 return true;
7066 return file_exists("$this->otherroot/$lang/langconfig.php");
7070 * Returns localised list of installed translations
7072 * @param bool $returnall return all or just enabled
7073 * @return array moodle translation code => localised translation name
7075 public function get_list_of_translations($returnall = false) {
7076 global $CFG;
7078 $languages = array();
7080 if (!empty($CFG->langcache) and is_readable($this->menucache)) {
7081 // try to re-use the cached list of all available languages
7082 $cachedlist = json_decode(file_get_contents($this->menucache), true);
7084 if (is_array($cachedlist) and !empty($cachedlist)) {
7085 // the cache file is restored correctly
7087 if (!$returnall and !empty($this->translist)) {
7088 // return just enabled translations
7089 foreach ($cachedlist as $langcode => $langname) {
7090 if (in_array($langcode, $this->translist)) {
7091 $languages[$langcode] = $langname;
7094 return $languages;
7096 } else {
7097 // return all translations
7098 return $cachedlist;
7103 // the cached list of languages is not available, let us populate the list
7105 if (!$returnall and !empty($this->translist)) {
7106 // return only some translations
7107 foreach ($this->translist as $lang) {
7108 $lang = trim($lang); //Just trim spaces to be a bit more permissive
7109 if (strstr($lang, '_local') !== false) {
7110 continue;
7112 if (strstr($lang, '_utf8') !== false) {
7113 continue;
7115 if ($lang !== 'en' and !file_exists("$this->otherroot/$lang/langconfig.php")) {
7116 // some broken or missing lang - can not switch to it anyway
7117 continue;
7119 $string = $this->load_component_strings('langconfig', $lang);
7120 if (!empty($string['thislanguage'])) {
7121 $languages[$lang] = $string['thislanguage'].' ('. $lang .')';
7123 unset($string);
7126 } else {
7127 // return all languages available in system
7128 $langdirs = get_list_of_plugins('', '', $this->otherroot);
7130 $langdirs = array_merge($langdirs, array("$CFG->dirroot/lang/en"=>'en'));
7131 // Sort all
7133 // Loop through all langs and get info
7134 foreach ($langdirs as $lang) {
7135 if (strstr($lang, '_local') !== false) {
7136 continue;
7138 if (strstr($lang, '_utf8') !== false) {
7139 continue;
7141 $string = $this->load_component_strings('langconfig', $lang);
7142 if (!empty($string['thislanguage'])) {
7143 $languages[$lang] = $string['thislanguage'].' ('. $lang .')';
7145 unset($string);
7148 if (!empty($CFG->langcache) and !empty($this->menucache)) {
7149 // cache the list so that it can be used next time
7150 collatorlib::asort($languages);
7151 check_dir_exists(dirname($this->menucache), true, true);
7152 file_put_contents($this->menucache, json_encode($languages));
7156 collatorlib::asort($languages);
7158 return $languages;
7162 * Returns localised list of currencies.
7164 * @param string $lang moodle translation language, NULL means use current
7165 * @return array currency code => localised currency name
7167 public function get_list_of_currencies($lang = NULL) {
7168 if ($lang === NULL) {
7169 $lang = current_language();
7172 $currencies = $this->load_component_strings('core_currencies', $lang);
7173 asort($currencies);
7175 return $currencies;
7179 * Clears both in-memory and on-disk caches
7180 * @param bool $phpunitreset true means called from our PHPUnit integration test reset
7182 public function reset_caches($phpunitreset = false) {
7183 global $CFG;
7184 require_once("$CFG->libdir/filelib.php");
7186 // clear the on-disk disk with aggregated string files
7187 $this->cache->purge();
7189 if (!$phpunitreset) {
7190 // Increment the revision counter.
7191 $langrev = get_config('core', 'langrev');
7192 $next = time();
7193 if ($langrev !== false and $next <= $langrev and $langrev - $next < 60*60) {
7194 // This resolves problems when reset is requested repeatedly within 1s,
7195 // the < 1h condition prevents accidental switching to future dates
7196 // because we might not recover from it.
7197 $next = $langrev+1;
7199 set_config('langrev', $next);
7202 // clear the cache containing the list of available translations
7203 // and re-populate it again
7204 fulldelete($this->menucache);
7205 $this->get_list_of_translations(true);
7209 * Returns string revision counter, this is incremented after any
7210 * string cache reset.
7211 * @return int lang string revision counter, -1 if unknown
7213 public function get_revision() {
7214 global $CFG;
7215 if (isset($CFG->langrev)) {
7216 return (int)$CFG->langrev;
7217 } else {
7218 return -1;
7222 /// End of external API ////////////////////////////////////////////////////
7225 * Helper method that recursively loads all parents of the given language.
7227 * @see self::get_language_dependencies()
7228 * @param string $lang language code
7229 * @param array $stack list of parent languages already populated in previous recursive calls
7230 * @return array list of all parents of the given language with the $lang itself added as the last element
7232 protected function populate_parent_languages($lang, array $stack = array()) {
7234 // English does not have a parent language.
7235 if ($lang === 'en') {
7236 return $stack;
7239 // Prevent circular dependency (and thence the infinitive recursion loop).
7240 if (in_array($lang, $stack)) {
7241 return $stack;
7244 // Load language configuration and look for the explicit parent language.
7245 if (!file_exists("$this->otherroot/$lang/langconfig.php")) {
7246 return $stack;
7248 $string = array();
7249 include("$this->otherroot/$lang/langconfig.php");
7251 if (empty($string['parentlanguage']) or $string['parentlanguage'] === 'en') {
7252 unset($string);
7253 return array_merge(array($lang), $stack);
7255 } else {
7256 $parentlang = $string['parentlanguage'];
7257 unset($string);
7258 return $this->populate_parent_languages($parentlang, array_merge(array($lang), $stack));
7265 * Fetches minimum strings for installation
7267 * Minimalistic string fetching implementation
7268 * that is used in installer before we fetch the wanted
7269 * language pack from moodle.org lang download site.
7271 * @package core
7272 * @copyright 2010 Petr Skoda (http://skodak.org)
7273 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
7275 class install_string_manager implements string_manager {
7276 /** @var string location of pre-install packs for all langs */
7277 protected $installroot;
7280 * Crate new instance of install string manager
7282 public function __construct() {
7283 global $CFG;
7284 $this->installroot = "$CFG->dirroot/install/lang";
7288 * Load all strings for one component
7289 * @param string $component The module the string is associated with
7290 * @param string $lang
7291 * @param bool $disablecache Do not use caches, force fetching the strings from sources
7292 * @param bool $disablelocal Do not use customized strings in xx_local language packs
7293 * @return array of all string for given component and lang
7295 public function load_component_strings($component, $lang, $disablecache=false, $disablelocal=false) {
7296 // not needed in installer
7297 return array();
7301 * Does the string actually exist?
7303 * get_string() is throwing debug warnings, sometimes we do not want them
7304 * or we want to display better explanation of the problem.
7306 * Use with care!
7308 * @param string $identifier The identifier of the string to search for
7309 * @param string $component The module the string is associated with
7310 * @return boot true if exists
7312 public function string_exists($identifier, $component) {
7313 // simple old style hack ;)
7314 $str = get_string($identifier, $component);
7315 return (strpos($str, '[[') === false);
7319 * Get String returns a requested string
7321 * @param string $identifier The identifier of the string to search for
7322 * @param string $component The module the string is associated with
7323 * @param string|object|array $a An object, string or number that can be used
7324 * within translation strings
7325 * @param string $lang moodle translation language, NULL means use current
7326 * @return string The String !
7328 public function get_string($identifier, $component = '', $a = NULL, $lang = NULL) {
7329 if (!$component) {
7330 $component = 'moodle';
7333 if ($lang === NULL) {
7334 $lang = current_language();
7337 //get parent lang
7338 $parent = '';
7339 if ($lang !== 'en' and $identifier !== 'parentlanguage' and $component !== 'langconfig') {
7340 if (file_exists("$this->installroot/$lang/langconfig.php")) {
7341 $string = array();
7342 include("$this->installroot/$lang/langconfig.php");
7343 if (isset($string['parentlanguage'])) {
7344 $parent = $string['parentlanguage'];
7346 unset($string);
7350 // include en string first
7351 if (!file_exists("$this->installroot/en/$component.php")) {
7352 return "[[$identifier]]";
7354 $string = array();
7355 include("$this->installroot/en/$component.php");
7357 // now override en with parent if defined
7358 if ($parent and $parent !== 'en' and file_exists("$this->installroot/$parent/$component.php")) {
7359 include("$this->installroot/$parent/$component.php");
7362 // finally override with requested language
7363 if ($lang !== 'en' and file_exists("$this->installroot/$lang/$component.php")) {
7364 include("$this->installroot/$lang/$component.php");
7367 if (!isset($string[$identifier])) {
7368 return "[[$identifier]]";
7371 $string = $string[$identifier];
7373 if ($a !== NULL) {
7374 if (is_object($a) or is_array($a)) {
7375 $a = (array)$a;
7376 $search = array();
7377 $replace = array();
7378 foreach ($a as $key=>$value) {
7379 if (is_int($key)) {
7380 // we do not support numeric keys - sorry!
7381 continue;
7383 $search[] = '{$a->'.$key.'}';
7384 $replace[] = (string)$value;
7386 if ($search) {
7387 $string = str_replace($search, $replace, $string);
7389 } else {
7390 $string = str_replace('{$a}', (string)$a, $string);
7394 return $string;
7398 * Returns a localised list of all country names, sorted by country keys.
7400 * @param bool $returnall return all or just enabled
7401 * @param string $lang moodle translation language, NULL means use current
7402 * @return array two-letter country code => translated name.
7404 public function get_list_of_countries($returnall = false, $lang = NULL) {
7405 //not used in installer
7406 return array();
7410 * Returns a localised list of languages, sorted by code keys.
7412 * @param string $lang moodle translation language, NULL means use current
7413 * @param string $standard language list standard
7414 * iso6392: three-letter language code (ISO 639-2/T) => translated name.
7415 * @return array language code => translated name
7417 public function get_list_of_languages($lang = NULL, $standard = 'iso6392') {
7418 //not used in installer
7419 return array();
7423 * Checks if the translation exists for the language
7425 * @param string $lang moodle translation language code
7426 * @param bool $includeall include also disabled translations
7427 * @return bool true if exists
7429 public function translation_exists($lang, $includeall = true) {
7430 return file_exists($this->installroot.'/'.$lang.'/langconfig.php');
7434 * Returns localised list of installed translations
7435 * @param bool $returnall return all or just enabled
7436 * @return array moodle translation code => localised translation name
7438 public function get_list_of_translations($returnall = false) {
7439 // return all is ignored here - we need to know all langs in installer
7440 $languages = array();
7441 // Get raw list of lang directories
7442 $langdirs = get_list_of_plugins('install/lang');
7443 asort($langdirs);
7444 // Get some info from each lang
7445 foreach ($langdirs as $lang) {
7446 if (file_exists($this->installroot.'/'.$lang.'/langconfig.php')) {
7447 $string = array();
7448 include($this->installroot.'/'.$lang.'/langconfig.php');
7449 if (!empty($string['thislanguage'])) {
7450 $languages[$lang] = $string['thislanguage'].' ('.$lang.')';
7454 // Return array
7455 return $languages;
7459 * Returns localised list of currencies.
7461 * @param string $lang moodle translation language, NULL means use current
7462 * @return array currency code => localised currency name
7464 public function get_list_of_currencies($lang = NULL) {
7465 // not used in installer
7466 return array();
7470 * This implementation does not use any caches
7471 * @param bool $phpunitreset true means called from our PHPUnit integration test reset
7473 public function reset_caches($phpunitreset = false) {
7474 // Nothing to do.
7478 * Returns string revision counter, this is incremented after any
7479 * string cache reset.
7480 * @return int lang string revision counter, -1 if unknown
7482 public function get_revision() {
7483 return -1;
7489 * Returns a localized string.
7491 * Returns the translated string specified by $identifier as
7492 * for $module. Uses the same format files as STphp.
7493 * $a is an object, string or number that can be used
7494 * within translation strings
7496 * eg 'hello {$a->firstname} {$a->lastname}'
7497 * or 'hello {$a}'
7499 * If you would like to directly echo the localized string use
7500 * the function {@link print_string()}
7502 * Example usage of this function involves finding the string you would
7503 * like a local equivalent of and using its identifier and module information
7504 * to retrieve it.<br/>
7505 * If you open moodle/lang/en/moodle.php and look near line 278
7506 * you will find a string to prompt a user for their word for 'course'
7507 * <code>
7508 * $string['course'] = 'Course';
7509 * </code>
7510 * So if you want to display the string 'Course'
7511 * in any language that supports it on your site
7512 * you just need to use the identifier 'course'
7513 * <code>
7514 * $mystring = '<strong>'. get_string('course') .'</strong>';
7515 * or
7516 * </code>
7517 * If the string you want is in another file you'd take a slightly
7518 * different approach. Looking in moodle/lang/en/calendar.php you find
7519 * around line 75:
7520 * <code>
7521 * $string['typecourse'] = 'Course event';
7522 * </code>
7523 * If you want to display the string "Course event" in any language
7524 * supported you would use the identifier 'typecourse' and the module 'calendar'
7525 * (because it is in the file calendar.php):
7526 * <code>
7527 * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>';
7528 * </code>
7530 * As a last resort, should the identifier fail to map to a string
7531 * the returned string will be [[ $identifier ]]
7533 * In Moodle 2.3 there is a new argument to this function $lazyload.
7534 * Setting $lazyload to true causes get_string to return a lang_string object
7535 * rather than the string itself. The fetching of the string is then put off until
7536 * the string object is first used. The object can be used by calling it's out
7537 * method or by casting the object to a string, either directly e.g.
7538 * (string)$stringobject
7539 * or indirectly by using the string within another string or echoing it out e.g.
7540 * echo $stringobject
7541 * return "<p>{$stringobject}</p>";
7542 * It is worth noting that using $lazyload and attempting to use the string as an
7543 * array key will cause a fatal error as objects cannot be used as array keys.
7544 * But you should never do that anyway!
7545 * For more information {@see lang_string}
7547 * @category string
7548 * @param string $identifier The key identifier for the localized string
7549 * @param string $component The module where the key identifier is stored,
7550 * usually expressed as the filename in the language pack without the
7551 * .php on the end but can also be written as mod/forum or grade/export/xls.
7552 * If none is specified then moodle.php is used.
7553 * @param string|object|array $a An object, string or number that can be used
7554 * within translation strings
7555 * @param bool $lazyload If set to true a string object is returned instead of
7556 * the string itself. The string then isn't calculated until it is first used.
7557 * @return string The localized string.
7559 function get_string($identifier, $component = '', $a = NULL, $lazyload = false) {
7560 global $CFG;
7562 // If the lazy load argument has been supplied return a lang_string object
7563 // instead.
7564 // We need to make sure it is true (and a bool) as you will see below there
7565 // used to be a forth argument at one point.
7566 if ($lazyload === true) {
7567 return new lang_string($identifier, $component, $a);
7570 if (debugging('', DEBUG_DEVELOPER) && clean_param($identifier, PARAM_STRINGID) === '') {
7571 throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.');
7574 // There is now a forth argument again, this time it is a boolean however so
7575 // we can still check for the old extralocations parameter.
7576 if (!is_bool($lazyload) && !empty($lazyload)) {
7577 debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.');
7580 if (strpos($component, '/') !== false) {
7581 debugging('The module name you passed to get_string is the deprecated format ' .
7582 'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER);
7583 $componentpath = explode('/', $component);
7585 switch ($componentpath[0]) {
7586 case 'mod':
7587 $component = $componentpath[1];
7588 break;
7589 case 'blocks':
7590 case 'block':
7591 $component = 'block_'.$componentpath[1];
7592 break;
7593 case 'enrol':
7594 $component = 'enrol_'.$componentpath[1];
7595 break;
7596 case 'format':
7597 $component = 'format_'.$componentpath[1];
7598 break;
7599 case 'grade':
7600 $component = 'grade'.$componentpath[1].'_'.$componentpath[2];
7601 break;
7605 $result = get_string_manager()->get_string($identifier, $component, $a);
7607 // Debugging feature lets you display string identifier and component
7608 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
7609 $result .= ' {' . $identifier . '/' . $component . '}';
7611 return $result;
7615 * Converts an array of strings to their localized value.
7617 * @param array $array An array of strings
7618 * @param string $component The language module that these strings can be found in.
7619 * @return stdClass translated strings.
7621 function get_strings($array, $component = '') {
7622 $string = new stdClass;
7623 foreach ($array as $item) {
7624 $string->$item = get_string($item, $component);
7626 return $string;
7630 * Prints out a translated string.
7632 * Prints out a translated string using the return value from the {@link get_string()} function.
7634 * Example usage of this function when the string is in the moodle.php file:<br/>
7635 * <code>
7636 * echo '<strong>';
7637 * print_string('course');
7638 * echo '</strong>';
7639 * </code>
7641 * Example usage of this function when the string is not in the moodle.php file:<br/>
7642 * <code>
7643 * echo '<h1>';
7644 * print_string('typecourse', 'calendar');
7645 * echo '</h1>';
7646 * </code>
7648 * @category string
7649 * @param string $identifier The key identifier for the localized string
7650 * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used.
7651 * @param string|object|array $a An object, string or number that can be used within translation strings
7653 function print_string($identifier, $component = '', $a = NULL) {
7654 echo get_string($identifier, $component, $a);
7658 * Returns a list of charset codes
7660 * Returns a list of charset codes. It's hardcoded, so they should be added manually
7661 * (checking that such charset is supported by the texlib library!)
7663 * @return array And associative array with contents in the form of charset => charset
7665 function get_list_of_charsets() {
7667 $charsets = array(
7668 'EUC-JP' => 'EUC-JP',
7669 'ISO-2022-JP'=> 'ISO-2022-JP',
7670 'ISO-8859-1' => 'ISO-8859-1',
7671 'SHIFT-JIS' => 'SHIFT-JIS',
7672 'GB2312' => 'GB2312',
7673 'GB18030' => 'GB18030', // gb18030 not supported by typo and mbstring
7674 'UTF-8' => 'UTF-8');
7676 asort($charsets);
7678 return $charsets;
7682 * Returns a list of valid and compatible themes
7684 * @return array
7686 function get_list_of_themes() {
7687 global $CFG;
7689 $themes = array();
7691 if (!empty($CFG->themelist)) { // use admin's list of themes
7692 $themelist = explode(',', $CFG->themelist);
7693 } else {
7694 $themelist = array_keys(get_plugin_list("theme"));
7697 foreach ($themelist as $key => $themename) {
7698 $theme = theme_config::load($themename);
7699 $themes[$themename] = $theme;
7702 collatorlib::asort_objects_by_method($themes, 'get_theme_name');
7704 return $themes;
7708 * Returns a list of timezones in the current language
7710 * @global object
7711 * @global object
7712 * @return array
7714 function get_list_of_timezones() {
7715 global $CFG, $DB;
7717 static $timezones;
7719 if (!empty($timezones)) { // This function has been called recently
7720 return $timezones;
7723 $timezones = array();
7725 if ($rawtimezones = $DB->get_records_sql("SELECT MAX(id), name FROM {timezone} GROUP BY name")) {
7726 foreach($rawtimezones as $timezone) {
7727 if (!empty($timezone->name)) {
7728 if (get_string_manager()->string_exists(strtolower($timezone->name), 'timezones')) {
7729 $timezones[$timezone->name] = get_string(strtolower($timezone->name), 'timezones');
7730 } else {
7731 $timezones[$timezone->name] = $timezone->name;
7733 if (substr($timezones[$timezone->name], 0, 1) == '[') { // No translation found
7734 $timezones[$timezone->name] = $timezone->name;
7740 asort($timezones);
7742 for ($i = -13; $i <= 13; $i += .5) {
7743 $tzstring = 'UTC';
7744 if ($i < 0) {
7745 $timezones[sprintf("%.1f", $i)] = $tzstring . $i;
7746 } else if ($i > 0) {
7747 $timezones[sprintf("%.1f", $i)] = $tzstring . '+' . $i;
7748 } else {
7749 $timezones[sprintf("%.1f", $i)] = $tzstring;
7753 return $timezones;
7757 * Factory function for emoticon_manager
7759 * @return emoticon_manager singleton
7761 function get_emoticon_manager() {
7762 static $singleton = null;
7764 if (is_null($singleton)) {
7765 $singleton = new emoticon_manager();
7768 return $singleton;
7772 * Provides core support for plugins that have to deal with
7773 * emoticons (like HTML editor or emoticon filter).
7775 * Whenever this manager mentiones 'emoticon object', the following data
7776 * structure is expected: stdClass with properties text, imagename, imagecomponent,
7777 * altidentifier and altcomponent
7779 * @see admin_setting_emoticons
7781 class emoticon_manager {
7784 * Returns the currently enabled emoticons
7786 * @return array of emoticon objects
7788 public function get_emoticons() {
7789 global $CFG;
7791 if (empty($CFG->emoticons)) {
7792 return array();
7795 $emoticons = $this->decode_stored_config($CFG->emoticons);
7797 if (!is_array($emoticons)) {
7798 // something is wrong with the format of stored setting
7799 debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL);
7800 return array();
7803 return $emoticons;
7807 * Converts emoticon object into renderable pix_emoticon object
7809 * @param stdClass $emoticon emoticon object
7810 * @param array $attributes explicit HTML attributes to set
7811 * @return pix_emoticon
7813 public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array()) {
7814 $stringmanager = get_string_manager();
7815 if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) {
7816 $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent);
7817 } else {
7818 $alt = s($emoticon->text);
7820 return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes);
7824 * Encodes the array of emoticon objects into a string storable in config table
7826 * @see self::decode_stored_config()
7827 * @param array $emoticons array of emtocion objects
7828 * @return string
7830 public function encode_stored_config(array $emoticons) {
7831 return json_encode($emoticons);
7835 * Decodes the string into an array of emoticon objects
7837 * @see self::encode_stored_config()
7838 * @param string $encoded
7839 * @return string|null
7841 public function decode_stored_config($encoded) {
7842 $decoded = json_decode($encoded);
7843 if (!is_array($decoded)) {
7844 return null;
7846 return $decoded;
7850 * Returns default set of emoticons supported by Moodle
7852 * @return array of sdtClasses
7854 public function default_emoticons() {
7855 return array(
7856 $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'),
7857 $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'),
7858 $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'),
7859 $this->prepare_emoticon_object(";-)", 's/wink', 'wink'),
7860 $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'),
7861 $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'),
7862 $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'),
7863 $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'),
7864 $this->prepare_emoticon_object("B-)", 's/cool', 'cool'),
7865 $this->prepare_emoticon_object("^-)", 's/approve', 'approve'),
7866 $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'),
7867 $this->prepare_emoticon_object(":o)", 's/clown', 'clown'),
7868 $this->prepare_emoticon_object(":-(", 's/sad', 'sad'),
7869 $this->prepare_emoticon_object(":(", 's/sad', 'sad'),
7870 $this->prepare_emoticon_object("8-.", 's/shy', 'shy'),
7871 $this->prepare_emoticon_object(":-I", 's/blush', 'blush'),
7872 $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'),
7873 $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'),
7874 $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'),
7875 $this->prepare_emoticon_object("8-[", 's/angry', 'angry'),
7876 $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'),
7877 $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'),
7878 $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'),
7879 $this->prepare_emoticon_object("}-]", 's/evil', 'evil'),
7880 $this->prepare_emoticon_object("(h)", 's/heart', 'heart'),
7881 $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'),
7882 $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'),
7883 $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'),
7884 $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'),
7885 $this->prepare_emoticon_object("( )", 's/egg', 'egg'),
7890 * Helper method preparing the stdClass with the emoticon properties
7892 * @param string|array $text or array of strings
7893 * @param string $imagename to be used by {@see pix_emoticon}
7894 * @param string $altidentifier alternative string identifier, null for no alt
7895 * @param array $altcomponent where the alternative string is defined
7896 * @param string $imagecomponent to be used by {@see pix_emoticon}
7897 * @return stdClass
7899 protected function prepare_emoticon_object($text, $imagename, $altidentifier = null, $altcomponent = 'core_pix', $imagecomponent = 'core') {
7900 return (object)array(
7901 'text' => $text,
7902 'imagename' => $imagename,
7903 'imagecomponent' => $imagecomponent,
7904 'altidentifier' => $altidentifier,
7905 'altcomponent' => $altcomponent,
7910 /// ENCRYPTION ////////////////////////////////////////////////
7913 * rc4encrypt
7915 * Please note that in this version of moodle that the default for rc4encryption is
7916 * using the slightly more secure password key. There may be an issue when upgrading
7917 * from an older version of moodle.
7919 * @todo MDL-31836 Remove the old password key in version 2.4
7920 * Code also needs to be changed in sessionlib.php
7921 * @see get_moodle_cookie()
7922 * @see set_moodle_cookie()
7924 * @param string $data Data to encrypt.
7925 * @param bool $usesecurekey Lets us know if we are using the old or new secure password key.
7926 * @return string The now encrypted data.
7928 function rc4encrypt($data, $usesecurekey = true) {
7929 if (!$usesecurekey) {
7930 $passwordkey = 'nfgjeingjk';
7931 } else {
7932 $passwordkey = get_site_identifier();
7934 return endecrypt($passwordkey, $data, '');
7938 * rc4decrypt
7940 * Please note that in this version of moodle that the default for rc4encryption is
7941 * using the slightly more secure password key. There may be an issue when upgrading
7942 * from an older version of moodle.
7944 * @todo MDL-31836 Remove the old password key in version 2.4
7945 * Code also needs to be changed in sessionlib.php
7946 * @see get_moodle_cookie()
7947 * @see set_moodle_cookie()
7949 * @param string $data Data to decrypt.
7950 * @param bool $usesecurekey Lets us know if we are using the old or new secure password key.
7951 * @return string The now decrypted data.
7953 function rc4decrypt($data, $usesecurekey = true) {
7954 if (!$usesecurekey) {
7955 $passwordkey = 'nfgjeingjk';
7956 } else {
7957 $passwordkey = get_site_identifier();
7959 return endecrypt($passwordkey, $data, 'de');
7963 * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com]
7965 * @todo Finish documenting this function
7967 * @param string $pwd The password to use when encrypting or decrypting
7968 * @param string $data The data to be decrypted/encrypted
7969 * @param string $case Either 'de' for decrypt or '' for encrypt
7970 * @return string
7972 function endecrypt ($pwd, $data, $case) {
7974 if ($case == 'de') {
7975 $data = urldecode($data);
7978 $key[] = '';
7979 $box[] = '';
7980 $temp_swap = '';
7981 $pwd_length = 0;
7983 $pwd_length = strlen($pwd);
7985 for ($i = 0; $i <= 255; $i++) {
7986 $key[$i] = ord(substr($pwd, ($i % $pwd_length), 1));
7987 $box[$i] = $i;
7990 $x = 0;
7992 for ($i = 0; $i <= 255; $i++) {
7993 $x = ($x + $box[$i] + $key[$i]) % 256;
7994 $temp_swap = $box[$i];
7995 $box[$i] = $box[$x];
7996 $box[$x] = $temp_swap;
7999 $temp = '';
8000 $k = '';
8002 $cipherby = '';
8003 $cipher = '';
8005 $a = 0;
8006 $j = 0;
8008 for ($i = 0; $i < strlen($data); $i++) {
8009 $a = ($a + 1) % 256;
8010 $j = ($j + $box[$a]) % 256;
8011 $temp = $box[$a];
8012 $box[$a] = $box[$j];
8013 $box[$j] = $temp;
8014 $k = $box[(($box[$a] + $box[$j]) % 256)];
8015 $cipherby = ord(substr($data, $i, 1)) ^ $k;
8016 $cipher .= chr($cipherby);
8019 if ($case == 'de') {
8020 $cipher = urldecode(urlencode($cipher));
8021 } else {
8022 $cipher = urlencode($cipher);
8025 return $cipher;
8028 /// ENVIRONMENT CHECKING ////////////////////////////////////////////////////////////
8031 * Returns the exact absolute path to plugin directory.
8033 * @param string $plugintype type of plugin
8034 * @param string $name name of the plugin
8035 * @return string full path to plugin directory; NULL if not found
8037 function get_plugin_directory($plugintype, $name) {
8038 global $CFG;
8040 if ($plugintype === '') {
8041 $plugintype = 'mod';
8044 $types = get_plugin_types(true);
8045 if (!array_key_exists($plugintype, $types)) {
8046 return NULL;
8048 $name = clean_param($name, PARAM_SAFEDIR); // just in case ;-)
8050 if (!empty($CFG->themedir) and $plugintype === 'theme') {
8051 if (!is_dir($types['theme'] . '/' . $name)) {
8052 // ok, so the theme is supposed to be in the $CFG->themedir
8053 return $CFG->themedir . '/' . $name;
8057 return $types[$plugintype].'/'.$name;
8061 * Return exact absolute path to a plugin directory.
8063 * @param string $component name such as 'moodle', 'mod_forum'
8064 * @return string full path to component directory; NULL if not found
8066 function get_component_directory($component) {
8067 global $CFG;
8069 list($type, $plugin) = normalize_component($component);
8071 if ($type === 'core') {
8072 if ($plugin === NULL ) {
8073 $path = $CFG->libdir;
8074 } else {
8075 $subsystems = get_core_subsystems();
8076 if (isset($subsystems[$plugin])) {
8077 $path = $CFG->dirroot.'/'.$subsystems[$plugin];
8078 } else {
8079 $path = NULL;
8083 } else {
8084 $path = get_plugin_directory($type, $plugin);
8087 return $path;
8091 * Normalize the component name using the "frankenstyle" names.
8092 * @param string $component
8093 * @return array $type+$plugin elements
8095 function normalize_component($component) {
8096 if ($component === 'moodle' or $component === 'core') {
8097 $type = 'core';
8098 $plugin = NULL;
8100 } else if (strpos($component, '_') === false) {
8101 $subsystems = get_core_subsystems();
8102 if (array_key_exists($component, $subsystems)) {
8103 $type = 'core';
8104 $plugin = $component;
8105 } else {
8106 // everything else is a module
8107 $type = 'mod';
8108 $plugin = $component;
8111 } else {
8112 list($type, $plugin) = explode('_', $component, 2);
8113 $plugintypes = get_plugin_types(false);
8114 if ($type !== 'core' and !array_key_exists($type, $plugintypes)) {
8115 $type = 'mod';
8116 $plugin = $component;
8120 return array($type, $plugin);
8124 * List all core subsystems and their location
8126 * This is a whitelist of components that are part of the core and their
8127 * language strings are defined in /lang/en/<<subsystem>>.php. If a given
8128 * plugin is not listed here and it does not have proper plugintype prefix,
8129 * then it is considered as course activity module.
8131 * The location is dirroot relative path. NULL means there is no special
8132 * directory for this subsystem. If the location is set, the subsystem's
8133 * renderer.php is expected to be there.
8135 * @return array of (string)name => (string|null)location
8137 function get_core_subsystems() {
8138 global $CFG;
8140 static $info = null;
8142 if (!$info) {
8143 $info = array(
8144 'access' => NULL,
8145 'admin' => $CFG->admin,
8146 'auth' => 'auth',
8147 'backup' => 'backup/util/ui',
8148 'badges' => 'badges',
8149 'block' => 'blocks',
8150 'blog' => 'blog',
8151 'bulkusers' => NULL,
8152 'cache' => 'cache',
8153 'calendar' => 'calendar',
8154 'cohort' => 'cohort',
8155 'condition' => NULL,
8156 'completion' => NULL,
8157 'countries' => NULL,
8158 'course' => 'course',
8159 'currencies' => NULL,
8160 'dbtransfer' => NULL,
8161 'debug' => NULL,
8162 'dock' => NULL,
8163 'editor' => 'lib/editor',
8164 'edufields' => NULL,
8165 'enrol' => 'enrol',
8166 'error' => NULL,
8167 'filepicker' => NULL,
8168 'files' => 'files',
8169 'filters' => NULL,
8170 'fonts' => NULL,
8171 'form' => 'lib/form',
8172 'grades' => 'grade',
8173 'grading' => 'grade/grading',
8174 'group' => 'group',
8175 'help' => NULL,
8176 'hub' => NULL,
8177 'imscc' => NULL,
8178 'install' => NULL,
8179 'iso6392' => NULL,
8180 'langconfig' => NULL,
8181 'license' => NULL,
8182 'mathslib' => NULL,
8183 'media' => 'media',
8184 'message' => 'message',
8185 'mimetypes' => NULL,
8186 'mnet' => 'mnet',
8187 'moodle.org' => NULL, // the dot is nasty, watch out! should be renamed to moodleorg
8188 'my' => 'my',
8189 'notes' => 'notes',
8190 'pagetype' => NULL,
8191 'pix' => NULL,
8192 'plagiarism' => 'plagiarism',
8193 'plugin' => NULL,
8194 'portfolio' => 'portfolio',
8195 'publish' => 'course/publish',
8196 'question' => 'question',
8197 'rating' => 'rating',
8198 'register' => 'admin/registration', //TODO: this is wrong, unfortunately we would need to modify hub code to pass around the correct url
8199 'repository' => 'repository',
8200 'rss' => 'rss',
8201 'role' => $CFG->admin.'/role',
8202 'search' => 'search',
8203 'table' => NULL,
8204 'tag' => 'tag',
8205 'timezones' => NULL,
8206 'user' => 'user',
8207 'userkey' => NULL,
8208 'webservice' => 'webservice',
8212 return $info;
8216 * Lists all plugin types
8217 * @param bool $fullpaths false means relative paths from dirroot
8218 * @return array Array of strings - name=>location
8220 function get_plugin_types($fullpaths=true) {
8221 global $CFG;
8223 $cache = cache::make('core', 'plugintypes');
8225 if ($fullpaths) {
8226 // First confirm that dirroot and the stored dirroot match.
8227 if ($CFG->dirroot === $cache->get('dirroot')) {
8228 // They match we can use it.
8229 $cached = $cache->get(1);
8230 } else {
8231 // Oops they didn't match. The moodle directory has been moved on us.
8232 $cached = false;
8234 } else {
8235 $cached = $cache->get(0);
8238 if ($cached !== false) {
8239 return $cached;
8241 } else {
8242 $info = array('qtype' => 'question/type',
8243 'mod' => 'mod',
8244 'auth' => 'auth',
8245 'enrol' => 'enrol',
8246 'message' => 'message/output',
8247 'block' => 'blocks',
8248 'filter' => 'filter',
8249 'editor' => 'lib/editor',
8250 'format' => 'course/format',
8251 'profilefield' => 'user/profile/field',
8252 'report' => 'report',
8253 'coursereport' => 'course/report', // must be after system reports
8254 'gradeexport' => 'grade/export',
8255 'gradeimport' => 'grade/import',
8256 'gradereport' => 'grade/report',
8257 'gradingform' => 'grade/grading/form',
8258 'mnetservice' => 'mnet/service',
8259 'webservice' => 'webservice',
8260 'repository' => 'repository',
8261 'portfolio' => 'portfolio',
8262 'qbehaviour' => 'question/behaviour',
8263 'qformat' => 'question/format',
8264 'plagiarism' => 'plagiarism',
8265 'tool' => $CFG->admin.'/tool',
8266 'cachestore' => 'cache/stores',
8267 'cachelock' => 'cache/locks',
8268 'theme' => 'theme', // this is a bit hacky, themes may be in $CFG->themedir too
8271 $subpluginowners = array_merge(array_values(get_plugin_list('mod')),
8272 array_values(get_plugin_list('editor')));
8273 foreach ($subpluginowners as $ownerdir) {
8274 if (file_exists("$ownerdir/db/subplugins.php")) {
8275 $subplugins = array();
8276 include("$ownerdir/db/subplugins.php");
8277 foreach ($subplugins as $subtype=>$dir) {
8278 $info[$subtype] = $dir;
8283 // local is always last!
8284 $info['local'] = 'local';
8286 $fullinfo = array();
8287 foreach ($info as $type => $dir) {
8288 $fullinfo[$type] = $CFG->dirroot.'/'.$dir;
8291 $cache->set(0, $info);
8292 $cache->set(1, $fullinfo);
8293 // We cache the dirroot as well so that we can compare it when we
8294 // retrieve full info from the cache.
8295 $cache->set('dirroot', $CFG->dirroot);
8297 return ($fullpaths ? $fullinfo : $info);
8302 * This method validates a plug name. It is much faster than calling clean_param.
8303 * @param string $name a string that might be a plugin name.
8304 * @return bool if this string is a valid plugin name.
8306 function is_valid_plugin_name($name) {
8307 return (bool) preg_match('/^[a-z](?:[a-z0-9_](?!__))*[a-z0-9]$/', $name);
8311 * Simplified version of get_list_of_plugins()
8312 * @param string $plugintype type of plugin
8313 * @return array name=>fulllocation pairs of plugins of given type
8315 function get_plugin_list($plugintype) {
8316 global $CFG;
8318 // We use the dirroot as an identifier here because if it has changed the whole cache
8319 // can be considered invalid.
8320 $cache = cache::make('core', 'pluginlist', array('dirroot' => $CFG->dirroot));
8321 $cached = $cache->get($plugintype);
8322 if ($cached !== false) {
8323 return $cached;
8326 $ignored = array('CVS', '_vti_cnf', 'simpletest', 'db', 'yui', 'tests');
8327 if ($plugintype == 'auth') {
8328 // Historically we have had an auth plugin called 'db', so allow a special case.
8329 $key = array_search('db', $ignored);
8330 if ($key !== false) {
8331 unset($ignored[$key]);
8335 if ($plugintype === '') {
8336 $plugintype = 'mod';
8339 $fulldirs = array();
8341 if ($plugintype === 'mod') {
8342 // mod is an exception because we have to call this function from get_plugin_types()
8343 $fulldirs[] = $CFG->dirroot.'/mod';
8345 } else if ($plugintype === 'editor') {
8346 // Exception also needed for editor for same reason.
8347 $fulldirs[] = $CFG->dirroot . '/lib/editor';
8349 } else if ($plugintype === 'theme') {
8350 $fulldirs[] = $CFG->dirroot.'/theme';
8351 // themes are special because they may be stored also in separate directory
8352 if (!empty($CFG->themedir) and file_exists($CFG->themedir) and is_dir($CFG->themedir) ) {
8353 $fulldirs[] = $CFG->themedir;
8356 } else {
8357 $types = get_plugin_types(true);
8358 if (!array_key_exists($plugintype, $types)) {
8359 $cache->set($plugintype, array());
8360 return array();
8362 $fulldir = $types[$plugintype];
8363 if (!file_exists($fulldir)) {
8364 $cache->set($plugintype, array());
8365 return array();
8367 $fulldirs[] = $fulldir;
8369 $result = array();
8371 foreach ($fulldirs as $fulldir) {
8372 if (!is_dir($fulldir)) {
8373 continue;
8375 $items = new DirectoryIterator($fulldir);
8376 foreach ($items as $item) {
8377 if ($item->isDot() or !$item->isDir()) {
8378 continue;
8380 $pluginname = $item->getFilename();
8381 if (in_array($pluginname, $ignored)) {
8382 continue;
8384 if (!is_valid_plugin_name($pluginname)) {
8385 // Better ignore plugins with problematic names here.
8386 continue;
8388 $result[$pluginname] = $fulldir.'/'.$pluginname;
8389 unset($item);
8391 unset($items);
8394 //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!
8395 ksort($result);
8396 $cache->set($plugintype, $result);
8397 return $result;
8401 * Get a list of all the plugins of a given type that contain a particular file.
8402 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
8403 * @param string $file the name of file that must be present in the plugin.
8404 * (e.g. 'view.php', 'db/install.xml').
8405 * @param bool $include if true (default false), the file will be include_once-ed if found.
8406 * @return array with plugin name as keys (e.g. 'forum', 'courselist') and the path
8407 * to the file relative to dirroot as value (e.g. "$CFG->dirroot/mod/forum/view.php").
8409 function get_plugin_list_with_file($plugintype, $file, $include = false) {
8410 global $CFG; // Necessary in case it is referenced by include()d PHP scripts.
8412 $plugins = array();
8414 foreach(get_plugin_list($plugintype) as $plugin => $dir) {
8415 $path = $dir . '/' . $file;
8416 if (file_exists($path)) {
8417 if ($include) {
8418 include_once($path);
8420 $plugins[$plugin] = $path;
8424 return $plugins;
8428 * Get a list of all the plugins of a given type that define a certain API function
8429 * in a certain file. The plugin component names and function names are returned.
8431 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
8432 * @param string $function the part of the name of the function after the
8433 * frankenstyle prefix. e.g 'hook' if you are looking for functions with
8434 * names like report_courselist_hook.
8435 * @param string $file the name of file within the plugin that defines the
8436 * function. Defaults to lib.php.
8437 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
8438 * and the function names as values (e.g. 'report_courselist_hook', 'forum_hook').
8440 function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php') {
8441 $pluginfunctions = array();
8442 foreach (get_plugin_list_with_file($plugintype, $file, true) as $plugin => $notused) {
8443 $fullfunction = $plugintype . '_' . $plugin . '_' . $function;
8445 if (function_exists($fullfunction)) {
8446 // Function exists with standard name. Store, indexed by
8447 // frankenstyle name of plugin
8448 $pluginfunctions[$plugintype . '_' . $plugin] = $fullfunction;
8450 } else if ($plugintype === 'mod') {
8451 // For modules, we also allow plugin without full frankenstyle
8452 // but just starting with the module name
8453 $shortfunction = $plugin . '_' . $function;
8454 if (function_exists($shortfunction)) {
8455 $pluginfunctions[$plugintype . '_' . $plugin] = $shortfunction;
8459 return $pluginfunctions;
8463 * Get a list of all the plugins of a given type that define a certain class
8464 * in a certain file. The plugin component names and class names are returned.
8466 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
8467 * @param string $class the part of the name of the class after the
8468 * frankenstyle prefix. e.g 'thing' if you are looking for classes with
8469 * names like report_courselist_thing. If you are looking for classes with
8470 * the same name as the plugin name (e.g. qtype_multichoice) then pass ''.
8471 * @param string $file the name of file within the plugin that defines the class.
8472 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
8473 * and the class names as values (e.g. 'report_courselist_thing', 'qtype_multichoice').
8475 function get_plugin_list_with_class($plugintype, $class, $file) {
8476 if ($class) {
8477 $suffix = '_' . $class;
8478 } else {
8479 $suffix = '';
8482 $pluginclasses = array();
8483 foreach (get_plugin_list_with_file($plugintype, $file, true) as $plugin => $notused) {
8484 $classname = $plugintype . '_' . $plugin . $suffix;
8485 if (class_exists($classname)) {
8486 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
8490 return $pluginclasses;
8494 * Lists plugin-like directories within specified directory
8496 * This function was originally used for standard Moodle plugins, please use
8497 * new get_plugin_list() now.
8499 * This function is used for general directory listing and backwards compatility.
8501 * @param string $directory relative directory from root
8502 * @param string $exclude dir name to exclude from the list (defaults to none)
8503 * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot)
8504 * @return array Sorted array of directory names found under the requested parameters
8506 function get_list_of_plugins($directory='mod', $exclude='', $basedir='') {
8507 global $CFG;
8509 $plugins = array();
8511 if (empty($basedir)) {
8512 $basedir = $CFG->dirroot .'/'. $directory;
8514 } else {
8515 $basedir = $basedir .'/'. $directory;
8518 if (file_exists($basedir) && filetype($basedir) == 'dir') {
8519 if (!$dirhandle = opendir($basedir)) {
8520 debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER);
8521 return array();
8523 while (false !== ($dir = readdir($dirhandle))) {
8524 $firstchar = substr($dir, 0, 1);
8525 if ($firstchar === '.' or $dir === 'CVS' or $dir === '_vti_cnf' or $dir === 'simpletest' or $dir === 'yui' or $dir === 'phpunit' or $dir === $exclude) {
8526 continue;
8528 if (filetype($basedir .'/'. $dir) != 'dir') {
8529 continue;
8531 $plugins[] = $dir;
8533 closedir($dirhandle);
8535 if ($plugins) {
8536 asort($plugins);
8538 return $plugins;
8542 * Invoke plugin's callback functions
8544 * @param string $type plugin type e.g. 'mod'
8545 * @param string $name plugin name
8546 * @param string $feature feature name
8547 * @param string $action feature's action
8548 * @param array $params parameters of callback function, should be an array
8549 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
8550 * @return mixed
8552 * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743
8554 function plugin_callback($type, $name, $feature, $action, $params = null, $default = null) {
8555 return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default);
8559 * Invoke component's callback functions
8561 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
8562 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
8563 * @param array $params parameters of callback function
8564 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
8565 * @return mixed
8567 function component_callback($component, $function, array $params = array(), $default = null) {
8568 global $CFG; // this is needed for require_once() below
8570 $cleancomponent = clean_param($component, PARAM_COMPONENT);
8571 if (empty($cleancomponent)) {
8572 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
8574 $component = $cleancomponent;
8576 list($type, $name) = normalize_component($component);
8577 $component = $type . '_' . $name;
8579 $oldfunction = $name.'_'.$function;
8580 $function = $component.'_'.$function;
8582 $dir = get_component_directory($component);
8583 if (empty($dir)) {
8584 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
8587 // Load library and look for function
8588 if (file_exists($dir.'/lib.php')) {
8589 require_once($dir.'/lib.php');
8592 if (!function_exists($function) and function_exists($oldfunction)) {
8593 if ($type !== 'mod' and $type !== 'core') {
8594 debugging("Please use new function name $function instead of legacy $oldfunction");
8596 $function = $oldfunction;
8599 if (function_exists($function)) {
8600 // Function exists, so just return function result
8601 $ret = call_user_func_array($function, $params);
8602 if (is_null($ret)) {
8603 return $default;
8604 } else {
8605 return $ret;
8608 return $default;
8612 * Checks whether a plugin supports a specified feature.
8614 * @param string $type Plugin type e.g. 'mod'
8615 * @param string $name Plugin name e.g. 'forum'
8616 * @param string $feature Feature code (FEATURE_xx constant)
8617 * @param mixed $default default value if feature support unknown
8618 * @return mixed Feature result (false if not supported, null if feature is unknown,
8619 * otherwise usually true but may have other feature-specific value such as array)
8621 function plugin_supports($type, $name, $feature, $default = NULL) {
8622 global $CFG;
8624 if ($type === 'mod' and $name === 'NEWMODULE') {
8625 //somebody forgot to rename the module template
8626 return false;
8629 $component = clean_param($type . '_' . $name, PARAM_COMPONENT);
8630 if (empty($component)) {
8631 throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name);
8634 $function = null;
8636 if ($type === 'mod') {
8637 // we need this special case because we support subplugins in modules,
8638 // otherwise it would end up in infinite loop
8639 if (file_exists("$CFG->dirroot/mod/$name/lib.php")) {
8640 include_once("$CFG->dirroot/mod/$name/lib.php");
8641 $function = $component.'_supports';
8642 if (!function_exists($function)) {
8643 // legacy non-frankenstyle function name
8644 $function = $name.'_supports';
8646 } else {
8647 // invalid module
8650 } else {
8651 if (!$path = get_plugin_directory($type, $name)) {
8652 // non existent plugin type
8653 return false;
8655 if (file_exists("$path/lib.php")) {
8656 include_once("$path/lib.php");
8657 $function = $component.'_supports';
8661 if ($function and function_exists($function)) {
8662 $supports = $function($feature);
8663 if (is_null($supports)) {
8664 // plugin does not know - use default
8665 return $default;
8666 } else {
8667 return $supports;
8671 //plugin does not care, so use default
8672 return $default;
8676 * Returns true if the current version of PHP is greater that the specified one.
8678 * @todo Check PHP version being required here is it too low?
8680 * @param string $version The version of php being tested.
8681 * @return bool
8683 function check_php_version($version='5.2.4') {
8684 return (version_compare(phpversion(), $version) >= 0);
8688 * Checks to see if is the browser operating system matches the specified
8689 * brand.
8691 * Known brand: 'Windows','Linux','Macintosh','SGI','SunOS','HP-UX'
8693 * @uses $_SERVER
8694 * @param string $brand The operating system identifier being tested
8695 * @return bool true if the given brand below to the detected operating system
8697 function check_browser_operating_system($brand) {
8698 if (empty($_SERVER['HTTP_USER_AGENT'])) {
8699 return false;
8702 if (preg_match("/$brand/i", $_SERVER['HTTP_USER_AGENT'])) {
8703 return true;
8706 return false;
8710 * Checks to see if is a browser matches the specified
8711 * brand and is equal or better version.
8713 * @uses $_SERVER
8714 * @param string $brand The browser identifier being tested
8715 * @param int $version The version of the browser, if not specified any version (except 5.5 for IE for BC reasons)
8716 * @return bool true if the given version is below that of the detected browser
8718 function check_browser_version($brand, $version = null) {
8719 if (empty($_SERVER['HTTP_USER_AGENT'])) {
8720 return false;
8723 $agent = $_SERVER['HTTP_USER_AGENT'];
8725 switch ($brand) {
8727 case 'Camino': /// OSX browser using Gecke engine
8728 if (strpos($agent, 'Camino') === false) {
8729 return false;
8731 if (empty($version)) {
8732 return true; // no version specified
8734 if (preg_match("/Camino\/([0-9\.]+)/i", $agent, $match)) {
8735 if (version_compare($match[1], $version) >= 0) {
8736 return true;
8739 break;
8742 case 'Firefox': /// Mozilla Firefox browsers
8743 if (strpos($agent, 'Iceweasel') === false and strpos($agent, 'Firefox') === false) {
8744 return false;
8746 if (empty($version)) {
8747 return true; // no version specified
8749 if (preg_match("/(Iceweasel|Firefox)\/([0-9\.]+)/i", $agent, $match)) {
8750 if (version_compare($match[2], $version) >= 0) {
8751 return true;
8754 break;
8757 case 'Gecko': /// Gecko based browsers
8758 // Do not look for dates any more, we expect real Firefox version here.
8759 if (empty($version)) {
8760 $version = 1;
8761 } else if ($version > 20000000) {
8762 // This is just a guess, it is not supposed to be 100% accurate!
8763 if (preg_match('/^201/', $version)) {
8764 $version = 3.6;
8765 } else if (preg_match('/^200[7-9]/', $version)) {
8766 $version = 3;
8767 } else if (preg_match('/^2006/', $version)) {
8768 $version = 2;
8769 } else {
8770 $version = 1.5;
8773 if (preg_match("/(Iceweasel|Firefox)\/([0-9\.]+)/i", $agent, $match)) {
8774 // Use real Firefox version if specified in user agent string.
8775 if (version_compare($match[2], $version) >= 0) {
8776 return true;
8778 } else if (preg_match("/Gecko\/([0-9\.]+)/i", $agent, $match)) {
8779 // Gecko might contain date or Firefox revision, let's just guess the Firefox version from the date.
8780 $browserver = $match[1];
8781 if ($browserver > 20000000) {
8782 // This is just a guess, it is not supposed to be 100% accurate!
8783 if (preg_match('/^201/', $browserver)) {
8784 $browserver = 3.6;
8785 } else if (preg_match('/^200[7-9]/', $browserver)) {
8786 $browserver = 3;
8787 } else if (preg_match('/^2006/', $version)) {
8788 $browserver = 2;
8789 } else {
8790 $browserver = 1.5;
8793 if (version_compare($browserver, $version) >= 0) {
8794 return true;
8797 break;
8800 case 'MSIE': /// Internet Explorer
8801 if (strpos($agent, 'Opera') !== false) { // Reject Opera
8802 return false;
8804 // In case of IE we have to deal with BC of the version parameter.
8805 if (is_null($version)) {
8806 $version = 5.5; // Anything older is not considered a browser at all!
8808 // IE uses simple versions, let's cast it to float to simplify the logic here.
8809 $version = round($version, 1);
8810 // See: http://www.useragentstring.com/pages/Internet%20Explorer/
8811 if (preg_match("/MSIE ([0-9\.]+)/", $agent, $match)) {
8812 $browser = $match[1];
8813 } else {
8814 return false;
8816 // IE8 and later versions may pretend to be IE7 for intranet sites, use Trident version instead,
8817 // the Trident should always describe the capabilities of IE in any emulation mode.
8818 if ($browser === '7.0' and preg_match("/Trident\/([0-9\.]+)/", $agent, $match)) {
8819 $browser = $match[1] + 4; // NOTE: Hopefully this will work also for future IE versions.
8821 $browser = round($browser, 1);
8822 return ($browser >= $version);
8823 break;
8826 case 'Opera': /// Opera
8827 if (strpos($agent, 'Opera') === false) {
8828 return false;
8830 if (empty($version)) {
8831 return true; // no version specified
8833 // Recent Opera useragents have Version/ with the actual version, e.g.:
8834 // Opera/9.80 (Windows NT 6.1; WOW64; U; en) Presto/2.10.289 Version/12.01
8835 // That's Opera 12.01, not 9.8.
8836 if (preg_match("/Version\/([0-9\.]+)/i", $agent, $match)) {
8837 if (version_compare($match[1], $version) >= 0) {
8838 return true;
8840 } else if (preg_match("/Opera\/([0-9\.]+)/i", $agent, $match)) {
8841 if (version_compare($match[1], $version) >= 0) {
8842 return true;
8845 break;
8848 case 'WebKit': /// WebKit based browser - everything derived from it (Safari, Chrome, iOS, Android and other mobiles)
8849 if (strpos($agent, 'AppleWebKit') === false) {
8850 return false;
8852 if (empty($version)) {
8853 return true; // no version specified
8855 if (preg_match("/AppleWebKit\/([0-9.]+)/i", $agent, $match)) {
8856 if (version_compare($match[1], $version) >= 0) {
8857 return true;
8860 break;
8863 case 'Safari': /// Desktop version of Apple Safari browser - no mobile or touch devices
8864 if (strpos($agent, 'AppleWebKit') === false) {
8865 return false;
8867 // Look for AppleWebKit, excluding strings with OmniWeb, Shiira and SymbianOS and any other mobile devices
8868 if (strpos($agent, 'OmniWeb')) { // Reject OmniWeb
8869 return false;
8871 if (strpos($agent, 'Shiira')) { // Reject Shiira
8872 return false;
8874 if (strpos($agent, 'SymbianOS')) { // Reject SymbianOS
8875 return false;
8877 if (strpos($agent, 'Android')) { // Reject Androids too
8878 return false;
8880 if (strpos($agent, 'iPhone') or strpos($agent, 'iPad') or strpos($agent, 'iPod')) {
8881 // No Apple mobile devices here - editor does not work, course ajax is not touch compatible, etc.
8882 return false;
8884 if (strpos($agent, 'Chrome')) { // Reject chrome browsers - it needs to be tested explicitly
8885 return false;
8888 if (empty($version)) {
8889 return true; // no version specified
8891 if (preg_match("/AppleWebKit\/([0-9.]+)/i", $agent, $match)) {
8892 if (version_compare($match[1], $version) >= 0) {
8893 return true;
8896 break;
8899 case 'Chrome':
8900 if (strpos($agent, 'Chrome') === false) {
8901 return false;
8903 if (empty($version)) {
8904 return true; // no version specified
8906 if (preg_match("/Chrome\/(.*)[ ]+/i", $agent, $match)) {
8907 if (version_compare($match[1], $version) >= 0) {
8908 return true;
8911 break;
8914 case 'Safari iOS': /// Safari on iPhone, iPad and iPod touch
8915 if (strpos($agent, 'AppleWebKit') === false or strpos($agent, 'Safari') === false) {
8916 return false;
8918 if (!strpos($agent, 'iPhone') and !strpos($agent, 'iPad') and !strpos($agent, 'iPod')) {
8919 return false;
8921 if (empty($version)) {
8922 return true; // no version specified
8924 if (preg_match("/AppleWebKit\/([0-9]+)/i", $agent, $match)) {
8925 if (version_compare($match[1], $version) >= 0) {
8926 return true;
8929 break;
8932 case 'WebKit Android': /// WebKit browser on Android
8933 if (strpos($agent, 'Linux; U; Android') === false) {
8934 return false;
8936 if (empty($version)) {
8937 return true; // no version specified
8939 if (preg_match("/AppleWebKit\/([0-9]+)/i", $agent, $match)) {
8940 if (version_compare($match[1], $version) >= 0) {
8941 return true;
8944 break;
8948 return false;
8952 * Returns whether a device/browser combination is mobile, tablet, legacy, default or the result of
8953 * an optional admin specified regular expression. If enabledevicedetection is set to no or not set
8954 * it returns default
8956 * @return string device type
8958 function get_device_type() {
8959 global $CFG;
8961 if (empty($CFG->enabledevicedetection) || empty($_SERVER['HTTP_USER_AGENT'])) {
8962 return 'default';
8965 $useragent = $_SERVER['HTTP_USER_AGENT'];
8967 if (!empty($CFG->devicedetectregex)) {
8968 $regexes = json_decode($CFG->devicedetectregex);
8970 foreach ($regexes as $value=>$regex) {
8971 if (preg_match($regex, $useragent)) {
8972 return $value;
8977 //mobile detection PHP direct copy from open source detectmobilebrowser.com
8978 $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';
8979 $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';
8980 if (preg_match($phonesregex,$useragent) || preg_match($modelsregex,substr($useragent, 0, 4))){
8981 return 'mobile';
8984 $tabletregex = '/Tablet browser|android|iPad|iProd|GT-P1000|GT-I9000|SHW-M180S|SGH-T849|SCH-I800|Build\/ERE27|sholest/i';
8985 if (preg_match($tabletregex, $useragent)) {
8986 return 'tablet';
8989 // Safe way to check for IE6 and not get false positives for some IE 7/8 users
8990 if (substr($_SERVER['HTTP_USER_AGENT'], 0, 34) === 'Mozilla/4.0 (compatible; MSIE 6.0;') {
8991 return 'legacy';
8994 return 'default';
8998 * Returns a list of the device types supporting by Moodle
9000 * @param boolean $incusertypes includes types specified using the devicedetectregex admin setting
9001 * @return array $types
9003 function get_device_type_list($incusertypes = true) {
9004 global $CFG;
9006 $types = array('default', 'legacy', 'mobile', 'tablet');
9008 if ($incusertypes && !empty($CFG->devicedetectregex)) {
9009 $regexes = json_decode($CFG->devicedetectregex);
9011 foreach ($regexes as $value => $regex) {
9012 $types[] = $value;
9016 return $types;
9020 * Returns the theme selected for a particular device or false if none selected.
9022 * @param string $devicetype
9023 * @return string|false The name of the theme to use for the device or the false if not set
9025 function get_selected_theme_for_device_type($devicetype = null) {
9026 global $CFG;
9028 if (empty($devicetype)) {
9029 $devicetype = get_user_device_type();
9032 $themevarname = get_device_cfg_var_name($devicetype);
9033 if (empty($CFG->$themevarname)) {
9034 return false;
9037 return $CFG->$themevarname;
9041 * Returns the name of the device type theme var in $CFG (because there is not a standard convention to allow backwards compatability
9043 * @param string $devicetype
9044 * @return string The config variable to use to determine the theme
9046 function get_device_cfg_var_name($devicetype = null) {
9047 if ($devicetype == 'default' || empty($devicetype)) {
9048 return 'theme';
9051 return 'theme' . $devicetype;
9055 * Allows the user to switch the device they are seeing the theme for.
9056 * This allows mobile users to switch back to the default theme, or theme for any other device.
9058 * @param string $newdevice The device the user is currently using.
9059 * @return string The device the user has switched to
9061 function set_user_device_type($newdevice) {
9062 global $USER;
9064 $devicetype = get_device_type();
9065 $devicetypes = get_device_type_list();
9067 if ($newdevice == $devicetype) {
9068 unset_user_preference('switchdevice'.$devicetype);
9069 } else if (in_array($newdevice, $devicetypes)) {
9070 set_user_preference('switchdevice'.$devicetype, $newdevice);
9075 * Returns the device the user is currently using, or if the user has chosen to switch devices
9076 * for the current device type the type they have switched to.
9078 * @return string The device the user is currently using or wishes to use
9080 function get_user_device_type() {
9081 $device = get_device_type();
9082 $switched = get_user_preferences('switchdevice'.$device, false);
9083 if ($switched != false) {
9084 return $switched;
9086 return $device;
9090 * Returns one or several CSS class names that match the user's browser. These can be put
9091 * in the body tag of the page to apply browser-specific rules without relying on CSS hacks
9093 * @return array An array of browser version classes
9095 function get_browser_version_classes() {
9096 $classes = array();
9098 if (check_browser_version("MSIE", "0")) {
9099 $classes[] = 'ie';
9100 for($i=12; $i>=6; $i--) {
9101 if (check_browser_version("MSIE", $i)) {
9102 $classes[] = 'ie'.$i;
9103 break;
9107 } else if (check_browser_version("Firefox") || check_browser_version("Gecko") || check_browser_version("Camino")) {
9108 $classes[] = 'gecko';
9109 if (preg_match('/rv\:([1-2])\.([0-9])/', $_SERVER['HTTP_USER_AGENT'], $matches)) {
9110 $classes[] = "gecko{$matches[1]}{$matches[2]}";
9113 } else if (check_browser_version("WebKit")) {
9114 $classes[] = 'safari';
9115 if (check_browser_version("Safari iOS")) {
9116 $classes[] = 'ios';
9118 } else if (check_browser_version("WebKit Android")) {
9119 $classes[] = 'android';
9122 } else if (check_browser_version("Opera")) {
9123 $classes[] = 'opera';
9127 return $classes;
9131 * Can handle rotated text. Whether it is safe to use the trickery in textrotate.js.
9133 * @return bool True for yes, false for no
9135 function can_use_rotated_text() {
9136 return check_browser_version('MSIE', 9) || check_browser_version('Firefox', 2) ||
9137 check_browser_version('Chrome', 21) || check_browser_version('Safari', 536.25) ||
9138 check_browser_version('Opera', 12) || check_browser_version('Safari iOS', 533);
9142 * Determine if moodle installation requires update
9144 * Checks version numbers of main code and all modules to see
9145 * if there are any mismatches
9147 * @global moodle_database $DB
9148 * @return bool
9150 function moodle_needs_upgrading() {
9151 global $CFG, $DB, $OUTPUT;
9153 if (empty($CFG->version)) {
9154 return true;
9157 // We have to purge plugin related caches now to be sure we have fresh data
9158 // and new plugins can be detected.
9159 cache::make('core', 'plugintypes')->purge();
9160 cache::make('core', 'pluginlist')->purge();
9161 cache::make('core', 'plugininfo_base')->purge();
9162 cache::make('core', 'plugininfo_mod')->purge();
9163 cache::make('core', 'plugininfo_block')->purge();
9164 cache::make('core', 'plugininfo_filter')->purge();
9165 cache::make('core', 'plugininfo_repository')->purge();
9166 cache::make('core', 'plugininfo_portfolio')->purge();
9168 // Check the main version first.
9169 $version = null;
9170 include($CFG->dirroot.'/version.php'); // defines $version and upgrades
9171 if ($version > $CFG->version) {
9172 return true;
9175 // modules
9176 $mods = get_plugin_list('mod');
9177 $installed = $DB->get_records('modules', array(), '', 'name, version');
9178 foreach ($mods as $mod => $fullmod) {
9179 if ($mod === 'NEWMODULE') { // Someone has unzipped the template, ignore it
9180 continue;
9182 $module = new stdClass();
9183 $plugin = new stdClass();
9184 if (!is_readable($fullmod.'/version.php')) {
9185 continue;
9187 include($fullmod.'/version.php'); // defines $module with version etc
9188 if (!isset($module->version) and isset($plugin->version)) {
9189 $module = $plugin;
9191 if (empty($installed[$mod])) {
9192 return true;
9193 } else if ($module->version > $installed[$mod]->version) {
9194 return true;
9197 unset($installed);
9199 // blocks
9200 $blocks = get_plugin_list('block');
9201 $installed = $DB->get_records('block', array(), '', 'name, version');
9202 require_once($CFG->dirroot.'/blocks/moodleblock.class.php');
9203 foreach ($blocks as $blockname=>$fullblock) {
9204 if ($blockname === 'NEWBLOCK') { // Someone has unzipped the template, ignore it
9205 continue;
9207 if (!is_readable($fullblock.'/version.php')) {
9208 continue;
9210 $plugin = new stdClass();
9211 $plugin->version = NULL;
9212 include($fullblock.'/version.php');
9213 if (empty($installed[$blockname])) {
9214 return true;
9215 } else if ($plugin->version > $installed[$blockname]->version) {
9216 return true;
9219 unset($installed);
9221 // now the rest of plugins
9222 $plugintypes = get_plugin_types();
9223 unset($plugintypes['mod']);
9224 unset($plugintypes['block']);
9226 $versions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin, value');
9227 foreach ($plugintypes as $type=>$unused) {
9228 $plugs = get_plugin_list($type);
9229 foreach ($plugs as $plug=>$fullplug) {
9230 $component = $type.'_'.$plug;
9231 if (!is_readable($fullplug.'/version.php')) {
9232 continue;
9234 $plugin = new stdClass();
9235 include($fullplug.'/version.php'); // defines $plugin with version etc
9236 if (array_key_exists($component, $versions)) {
9237 $installedversion = $versions[$component];
9238 } else {
9239 $installedversion = get_config($component, 'version');
9241 if (empty($installedversion)) { // new installation
9242 return true;
9243 } else if ($installedversion < $plugin->version) { // upgrade
9244 return true;
9249 return false;
9253 * Returns the major version of this site
9255 * Moodle version numbers consist of three numbers separated by a dot, for
9256 * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so
9257 * called major version. This function extracts the major version from either
9258 * $CFG->release (default) or eventually from the $release variable defined in
9259 * the main version.php.
9261 * @param bool $fromdisk should the version if source code files be used
9262 * @return string|false the major version like '2.3', false if could not be determined
9264 function moodle_major_version($fromdisk = false) {
9265 global $CFG;
9267 if ($fromdisk) {
9268 $release = null;
9269 require($CFG->dirroot.'/version.php');
9270 if (empty($release)) {
9271 return false;
9274 } else {
9275 if (empty($CFG->release)) {
9276 return false;
9278 $release = $CFG->release;
9281 if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) {
9282 return $matches[0];
9283 } else {
9284 return false;
9288 /// MISCELLANEOUS ////////////////////////////////////////////////////////////////////
9291 * Sets the system locale
9293 * @category string
9294 * @param string $locale Can be used to force a locale
9296 function moodle_setlocale($locale='') {
9297 global $CFG;
9299 static $currentlocale = ''; // last locale caching
9301 $oldlocale = $currentlocale;
9303 /// Fetch the correct locale based on ostype
9304 if ($CFG->ostype == 'WINDOWS') {
9305 $stringtofetch = 'localewin';
9306 } else {
9307 $stringtofetch = 'locale';
9310 /// the priority is the same as in get_string() - parameter, config, course, session, user, global language
9311 if (!empty($locale)) {
9312 $currentlocale = $locale;
9313 } else if (!empty($CFG->locale)) { // override locale for all language packs
9314 $currentlocale = $CFG->locale;
9315 } else {
9316 $currentlocale = get_string($stringtofetch, 'langconfig');
9319 /// do nothing if locale already set up
9320 if ($oldlocale == $currentlocale) {
9321 return;
9324 /// Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values,
9325 /// set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7
9326 /// Some day, numeric, monetary and other categories should be set too, I think. :-/
9328 /// Get current values
9329 $monetary= setlocale (LC_MONETARY, 0);
9330 $numeric = setlocale (LC_NUMERIC, 0);
9331 $ctype = setlocale (LC_CTYPE, 0);
9332 if ($CFG->ostype != 'WINDOWS') {
9333 $messages= setlocale (LC_MESSAGES, 0);
9335 /// Set locale to all
9336 setlocale (LC_ALL, $currentlocale);
9337 /// Set old values
9338 setlocale (LC_MONETARY, $monetary);
9339 setlocale (LC_NUMERIC, $numeric);
9340 if ($CFG->ostype != 'WINDOWS') {
9341 setlocale (LC_MESSAGES, $messages);
9343 if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') { // To workaround a well-known PHP problem with Turkish letter Ii
9344 setlocale (LC_CTYPE, $ctype);
9349 * Count words in a string.
9351 * Words are defined as things between whitespace.
9353 * @category string
9354 * @param string $string The text to be searched for words.
9355 * @return int The count of words in the specified string
9357 function count_words($string) {
9358 $string = strip_tags($string);
9359 return count(preg_split("/\w\b/", $string)) - 1;
9362 /** Count letters in a string.
9364 * Letters are defined as chars not in tags and different from whitespace.
9366 * @category string
9367 * @param string $string The text to be searched for letters.
9368 * @return int The count of letters in the specified text.
9370 function count_letters($string) {
9371 /// Loading the textlib singleton instance. We are going to need it.
9372 $string = strip_tags($string); // Tags are out now
9373 $string = preg_replace('/[[:space:]]*/','',$string); //Whitespace are out now
9375 return textlib::strlen($string);
9379 * Generate and return a random string of the specified length.
9381 * @param int $length The length of the string to be created.
9382 * @return string
9384 function random_string ($length=15) {
9385 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
9386 $pool .= 'abcdefghijklmnopqrstuvwxyz';
9387 $pool .= '0123456789';
9388 $poollen = strlen($pool);
9389 mt_srand ((double) microtime() * 1000000);
9390 $string = '';
9391 for ($i = 0; $i < $length; $i++) {
9392 $string .= substr($pool, (mt_rand()%($poollen)), 1);
9394 return $string;
9398 * Generate a complex random string (useful for md5 salts)
9400 * This function is based on the above {@link random_string()} however it uses a
9401 * larger pool of characters and generates a string between 24 and 32 characters
9403 * @param int $length Optional if set generates a string to exactly this length
9404 * @return string
9406 function complex_random_string($length=null) {
9407 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
9408 $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
9409 $poollen = strlen($pool);
9410 mt_srand ((double) microtime() * 1000000);
9411 if ($length===null) {
9412 $length = floor(rand(24,32));
9414 $string = '';
9415 for ($i = 0; $i < $length; $i++) {
9416 $string .= $pool[(mt_rand()%$poollen)];
9418 return $string;
9422 * Given some text (which may contain HTML) and an ideal length,
9423 * this function truncates the text neatly on a word boundary if possible
9425 * @category string
9426 * @global stdClass $CFG
9427 * @param string $text text to be shortened
9428 * @param int $ideal ideal string length
9429 * @param boolean $exact if false, $text will not be cut mid-word
9430 * @param string $ending The string to append if the passed string is truncated
9431 * @return string $truncate shortened string
9433 function shorten_text($text, $ideal=30, $exact = false, $ending='...') {
9435 global $CFG;
9437 // If the plain text is shorter than the maximum length, return the whole text.
9438 if (textlib::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) {
9439 return $text;
9442 // Splits on HTML tags. Each open/close/empty tag will be the first thing
9443 // and only tag in its 'line'.
9444 preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
9446 $total_length = textlib::strlen($ending);
9447 $truncate = '';
9449 // This array stores information about open and close tags and their position
9450 // in the truncated string. Each item in the array is an object with fields
9451 // ->open (true if open), ->tag (tag name in lower case), and ->pos
9452 // (byte position in truncated text).
9453 $tagdetails = array();
9455 foreach ($lines as $line_matchings) {
9456 // If there is any html-tag in this line, handle it and add it (uncounted) to the output.
9457 if (!empty($line_matchings[1])) {
9458 // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>).
9459 if (preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $line_matchings[1])) {
9460 // Do nothing.
9462 } else if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $line_matchings[1], $tag_matchings)) {
9463 // Record closing tag.
9464 $tagdetails[] = (object) array(
9465 'open' => false,
9466 'tag' => textlib::strtolower($tag_matchings[1]),
9467 'pos' => textlib::strlen($truncate),
9470 } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $line_matchings[1], $tag_matchings)) {
9471 // Record opening tag.
9472 $tagdetails[] = (object) array(
9473 'open' => true,
9474 'tag' => textlib::strtolower($tag_matchings[1]),
9475 'pos' => textlib::strlen($truncate),
9478 // Add html-tag to $truncate'd text.
9479 $truncate .= $line_matchings[1];
9482 // Calculate the length of the plain text part of the line; handle entities as one character.
9483 $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]));
9484 if ($total_length + $content_length > $ideal) {
9485 // The number of characters which are left.
9486 $left = $ideal - $total_length;
9487 $entities_length = 0;
9488 // Search for html entities.
9489 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)) {
9490 // calculate the real length of all entities in the legal range
9491 foreach ($entities[0] as $entity) {
9492 if ($entity[1]+1-$entities_length <= $left) {
9493 $left--;
9494 $entities_length += textlib::strlen($entity[0]);
9495 } else {
9496 // no more characters left
9497 break;
9501 $breakpos = $left + $entities_length;
9503 // if the words shouldn't be cut in the middle...
9504 if (!$exact) {
9505 // ...search the last occurence of a space...
9506 for (; $breakpos > 0; $breakpos--) {
9507 if ($char = textlib::substr($line_matchings[2], $breakpos, 1)) {
9508 if ($char === '.' or $char === ' ') {
9509 $breakpos += 1;
9510 break;
9511 } else if (strlen($char) > 2) { // Chinese/Japanese/Korean text
9512 $breakpos += 1; // can be truncated at any UTF-8
9513 break; // character boundary.
9518 if ($breakpos == 0) {
9519 // This deals with the test_shorten_text_no_spaces case.
9520 $breakpos = $left + $entities_length;
9521 } else if ($breakpos > $left + $entities_length) {
9522 // This deals with the previous for loop breaking on the first char.
9523 $breakpos = $left + $entities_length;
9526 $truncate .= textlib::substr($line_matchings[2], 0, $breakpos);
9527 // maximum length is reached, so get off the loop
9528 break;
9529 } else {
9530 $truncate .= $line_matchings[2];
9531 $total_length += $content_length;
9534 // If the maximum length is reached, get off the loop.
9535 if($total_length >= $ideal) {
9536 break;
9540 // Add the defined ending to the text.
9541 $truncate .= $ending;
9543 // Now calculate the list of open html tags based on the truncate position.
9544 $open_tags = array();
9545 foreach ($tagdetails as $taginfo) {
9546 if ($taginfo->open) {
9547 // Add tag to the beginning of $open_tags list.
9548 array_unshift($open_tags, $taginfo->tag);
9549 } else {
9550 // Can have multiple exact same open tags, close the last one.
9551 $pos = array_search($taginfo->tag, array_reverse($open_tags, true));
9552 if ($pos !== false) {
9553 unset($open_tags[$pos]);
9558 // Close all unclosed html-tags.
9559 foreach ($open_tags as $tag) {
9560 $truncate .= '</' . $tag . '>';
9563 return $truncate;
9568 * Given dates in seconds, how many weeks is the date from startdate
9569 * The first week is 1, the second 2 etc ...
9571 * @todo Finish documenting this function
9573 * @uses WEEKSECS
9574 * @param int $startdate Timestamp for the start date
9575 * @param int $thedate Timestamp for the end date
9576 * @return string
9578 function getweek ($startdate, $thedate) {
9579 if ($thedate < $startdate) { // error
9580 return 0;
9583 return floor(($thedate - $startdate) / WEEKSECS) + 1;
9587 * returns a randomly generated password of length $maxlen. inspired by
9589 * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and
9590 * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254}
9592 * @global stdClass $CFG
9593 * @param int $maxlen The maximum size of the password being generated.
9594 * @return string
9596 function generate_password($maxlen=10) {
9597 global $CFG;
9599 if (empty($CFG->passwordpolicy)) {
9600 $fillers = PASSWORD_DIGITS;
9601 $wordlist = file($CFG->wordlist);
9602 $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]);
9603 $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]);
9604 $filler1 = $fillers[rand(0, strlen($fillers) - 1)];
9605 $password = $word1 . $filler1 . $word2;
9606 } else {
9607 $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0;
9608 $digits = $CFG->minpassworddigits;
9609 $lower = $CFG->minpasswordlower;
9610 $upper = $CFG->minpasswordupper;
9611 $nonalphanum = $CFG->minpasswordnonalphanum;
9612 $total = $lower + $upper + $digits + $nonalphanum;
9613 // minlength should be the greater one of the two ( $minlen and $total )
9614 $minlen = $minlen < $total ? $total : $minlen;
9615 // maxlen can never be smaller than minlen
9616 $maxlen = $minlen > $maxlen ? $minlen : $maxlen;
9617 $additional = $maxlen - $total;
9619 // Make sure we have enough characters to fulfill
9620 // complexity requirements
9621 $passworddigits = PASSWORD_DIGITS;
9622 while ($digits > strlen($passworddigits)) {
9623 $passworddigits .= PASSWORD_DIGITS;
9625 $passwordlower = PASSWORD_LOWER;
9626 while ($lower > strlen($passwordlower)) {
9627 $passwordlower .= PASSWORD_LOWER;
9629 $passwordupper = PASSWORD_UPPER;
9630 while ($upper > strlen($passwordupper)) {
9631 $passwordupper .= PASSWORD_UPPER;
9633 $passwordnonalphanum = PASSWORD_NONALPHANUM;
9634 while ($nonalphanum > strlen($passwordnonalphanum)) {
9635 $passwordnonalphanum .= PASSWORD_NONALPHANUM;
9638 // Now mix and shuffle it all
9639 $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) .
9640 substr(str_shuffle ($passwordupper), 0, $upper) .
9641 substr(str_shuffle ($passworddigits), 0, $digits) .
9642 substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) .
9643 substr(str_shuffle ($passwordlower .
9644 $passwordupper .
9645 $passworddigits .
9646 $passwordnonalphanum), 0 , $additional));
9649 return substr ($password, 0, $maxlen);
9653 * Given a float, prints it nicely.
9654 * Localized floats must not be used in calculations!
9656 * The stripzeros feature is intended for making numbers look nicer in small
9657 * areas where it is not necessary to indicate the degree of accuracy by showing
9658 * ending zeros. If you turn it on with $decimalpoints set to 3, for example,
9659 * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'.
9661 * @param float $float The float to print
9662 * @param int $decimalpoints The number of decimal places to print.
9663 * @param bool $localized use localized decimal separator
9664 * @param bool $stripzeros If true, removes final zeros after decimal point
9665 * @return string locale float
9667 function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=false) {
9668 if (is_null($float)) {
9669 return '';
9671 if ($localized) {
9672 $separator = get_string('decsep', 'langconfig');
9673 } else {
9674 $separator = '.';
9676 $result = number_format($float, $decimalpoints, $separator, '');
9677 if ($stripzeros) {
9678 // Remove zeros and final dot if not needed
9679 $result = preg_replace('~(' . preg_quote($separator) . ')?0+$~', '', $result);
9681 return $result;
9685 * Converts locale specific floating point/comma number back to standard PHP float value
9686 * Do NOT try to do any math operations before this conversion on any user submitted floats!
9688 * @param string $locale_float locale aware float representation
9689 * @param bool $strict If true, then check the input and return false if it is not a valid number.
9690 * @return mixed float|bool - false or the parsed float.
9692 function unformat_float($locale_float, $strict = false) {
9693 $locale_float = trim($locale_float);
9695 if ($locale_float == '') {
9696 return null;
9699 $locale_float = str_replace(' ', '', $locale_float); // no spaces - those might be used as thousand separators
9700 $locale_float = str_replace(get_string('decsep', 'langconfig'), '.', $locale_float);
9702 if ($strict && !is_numeric($locale_float)) {
9703 return false;
9706 return (float)$locale_float;
9710 * Given a simple array, this shuffles it up just like shuffle()
9711 * Unlike PHP's shuffle() this function works on any machine.
9713 * @param array $array The array to be rearranged
9714 * @return array
9716 function swapshuffle($array) {
9718 srand ((double) microtime() * 10000000);
9719 $last = count($array) - 1;
9720 for ($i=0;$i<=$last;$i++) {
9721 $from = rand(0,$last);
9722 $curr = $array[$i];
9723 $array[$i] = $array[$from];
9724 $array[$from] = $curr;
9726 return $array;
9730 * Like {@link swapshuffle()}, but works on associative arrays
9732 * @param array $array The associative array to be rearranged
9733 * @return array
9735 function swapshuffle_assoc($array) {
9737 $newarray = array();
9738 $newkeys = swapshuffle(array_keys($array));
9740 foreach ($newkeys as $newkey) {
9741 $newarray[$newkey] = $array[$newkey];
9743 return $newarray;
9747 * Given an arbitrary array, and a number of draws,
9748 * this function returns an array with that amount
9749 * of items. The indexes are retained.
9751 * @todo Finish documenting this function
9753 * @param array $array
9754 * @param int $draws
9755 * @return array
9757 function draw_rand_array($array, $draws) {
9758 srand ((double) microtime() * 10000000);
9760 $return = array();
9762 $last = count($array);
9764 if ($draws > $last) {
9765 $draws = $last;
9768 while ($draws > 0) {
9769 $last--;
9771 $keys = array_keys($array);
9772 $rand = rand(0, $last);
9774 $return[$keys[$rand]] = $array[$keys[$rand]];
9775 unset($array[$keys[$rand]]);
9777 $draws--;
9780 return $return;
9784 * Calculate the difference between two microtimes
9786 * @param string $a The first Microtime
9787 * @param string $b The second Microtime
9788 * @return string
9790 function microtime_diff($a, $b) {
9791 list($a_dec, $a_sec) = explode(' ', $a);
9792 list($b_dec, $b_sec) = explode(' ', $b);
9793 return $b_sec - $a_sec + $b_dec - $a_dec;
9797 * Given a list (eg a,b,c,d,e) this function returns
9798 * an array of 1->a, 2->b, 3->c etc
9800 * @param string $list The string to explode into array bits
9801 * @param string $separator The separator used within the list string
9802 * @return array The now assembled array
9804 function make_menu_from_list($list, $separator=',') {
9806 $array = array_reverse(explode($separator, $list), true);
9807 foreach ($array as $key => $item) {
9808 $outarray[$key+1] = trim($item);
9810 return $outarray;
9814 * Creates an array that represents all the current grades that
9815 * can be chosen using the given grading type.
9817 * Negative numbers
9818 * are scales, zero is no grade, and positive numbers are maximum
9819 * grades.
9821 * @todo Finish documenting this function or better deprecated this completely!
9823 * @param int $gradingtype
9824 * @return array
9826 function make_grades_menu($gradingtype) {
9827 global $DB;
9829 $grades = array();
9830 if ($gradingtype < 0) {
9831 if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) {
9832 return make_menu_from_list($scale->scale);
9834 } else if ($gradingtype > 0) {
9835 for ($i=$gradingtype; $i>=0; $i--) {
9836 $grades[$i] = $i .' / '. $gradingtype;
9838 return $grades;
9840 return $grades;
9844 * This function returns the number of activities
9845 * using scaleid in a courseid
9847 * @todo Finish documenting this function
9849 * @global object
9850 * @global object
9851 * @param int $courseid ?
9852 * @param int $scaleid ?
9853 * @return int
9855 function course_scale_used($courseid, $scaleid) {
9856 global $CFG, $DB;
9858 $return = 0;
9860 if (!empty($scaleid)) {
9861 if ($cms = get_course_mods($courseid)) {
9862 foreach ($cms as $cm) {
9863 //Check cm->name/lib.php exists
9864 if (file_exists($CFG->dirroot.'/mod/'.$cm->modname.'/lib.php')) {
9865 include_once($CFG->dirroot.'/mod/'.$cm->modname.'/lib.php');
9866 $function_name = $cm->modname.'_scale_used';
9867 if (function_exists($function_name)) {
9868 if ($function_name($cm->instance,$scaleid)) {
9869 $return++;
9876 // check if any course grade item makes use of the scale
9877 $return += $DB->count_records('grade_items', array('courseid'=>$courseid, 'scaleid'=>$scaleid));
9879 // check if any outcome in the course makes use of the scale
9880 $return += $DB->count_records_sql("SELECT COUNT('x')
9881 FROM {grade_outcomes_courses} goc,
9882 {grade_outcomes} go
9883 WHERE go.id = goc.outcomeid
9884 AND go.scaleid = ? AND goc.courseid = ?",
9885 array($scaleid, $courseid));
9887 return $return;
9891 * This function returns the number of activities
9892 * using scaleid in the entire site
9894 * @param int $scaleid
9895 * @param array $courses
9896 * @return int
9898 function site_scale_used($scaleid, &$courses) {
9899 $return = 0;
9901 if (!is_array($courses) || count($courses) == 0) {
9902 $courses = get_courses("all",false,"c.id,c.shortname");
9905 if (!empty($scaleid)) {
9906 if (is_array($courses) && count($courses) > 0) {
9907 foreach ($courses as $course) {
9908 $return += course_scale_used($course->id,$scaleid);
9912 return $return;
9916 * make_unique_id_code
9918 * @todo Finish documenting this function
9920 * @uses $_SERVER
9921 * @param string $extra Extra string to append to the end of the code
9922 * @return string
9924 function make_unique_id_code($extra='') {
9926 $hostname = 'unknownhost';
9927 if (!empty($_SERVER['HTTP_HOST'])) {
9928 $hostname = $_SERVER['HTTP_HOST'];
9929 } else if (!empty($_ENV['HTTP_HOST'])) {
9930 $hostname = $_ENV['HTTP_HOST'];
9931 } else if (!empty($_SERVER['SERVER_NAME'])) {
9932 $hostname = $_SERVER['SERVER_NAME'];
9933 } else if (!empty($_ENV['SERVER_NAME'])) {
9934 $hostname = $_ENV['SERVER_NAME'];
9937 $date = gmdate("ymdHis");
9939 $random = random_string(6);
9941 if ($extra) {
9942 return $hostname .'+'. $date .'+'. $random .'+'. $extra;
9943 } else {
9944 return $hostname .'+'. $date .'+'. $random;
9950 * Function to check the passed address is within the passed subnet
9952 * The parameter is a comma separated string of subnet definitions.
9953 * Subnet strings can be in one of three formats:
9954 * 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn (number of bits in net mask)
9955 * 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)
9956 * 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. (incomplete address, a bit non-technical ;-)
9957 * Code for type 1 modified from user posted comments by mediator at
9958 * {@link http://au.php.net/manual/en/function.ip2long.php}
9960 * @param string $addr The address you are checking
9961 * @param string $subnetstr The string of subnet addresses
9962 * @return bool
9964 function address_in_subnet($addr, $subnetstr) {
9966 if ($addr == '0.0.0.0') {
9967 return false;
9969 $subnets = explode(',', $subnetstr);
9970 $found = false;
9971 $addr = trim($addr);
9972 $addr = cleanremoteaddr($addr, false); // normalise
9973 if ($addr === null) {
9974 return false;
9976 $addrparts = explode(':', $addr);
9978 $ipv6 = strpos($addr, ':');
9980 foreach ($subnets as $subnet) {
9981 $subnet = trim($subnet);
9982 if ($subnet === '') {
9983 continue;
9986 if (strpos($subnet, '/') !== false) {
9987 ///1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn
9988 list($ip, $mask) = explode('/', $subnet);
9989 $mask = trim($mask);
9990 if (!is_number($mask)) {
9991 continue; // incorect mask number, eh?
9993 $ip = cleanremoteaddr($ip, false); // normalise
9994 if ($ip === null) {
9995 continue;
9997 if (strpos($ip, ':') !== false) {
9998 // IPv6
9999 if (!$ipv6) {
10000 continue;
10002 if ($mask > 128 or $mask < 0) {
10003 continue; // nonsense
10005 if ($mask == 0) {
10006 return true; // any address
10008 if ($mask == 128) {
10009 if ($ip === $addr) {
10010 return true;
10012 continue;
10014 $ipparts = explode(':', $ip);
10015 $modulo = $mask % 16;
10016 $ipnet = array_slice($ipparts, 0, ($mask-$modulo)/16);
10017 $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16);
10018 if (implode(':', $ipnet) === implode(':', $addrnet)) {
10019 if ($modulo == 0) {
10020 return true;
10022 $pos = ($mask-$modulo)/16;
10023 $ipnet = hexdec($ipparts[$pos]);
10024 $addrnet = hexdec($addrparts[$pos]);
10025 $mask = 0xffff << (16 - $modulo);
10026 if (($addrnet & $mask) == ($ipnet & $mask)) {
10027 return true;
10031 } else {
10032 // IPv4
10033 if ($ipv6) {
10034 continue;
10036 if ($mask > 32 or $mask < 0) {
10037 continue; // nonsense
10039 if ($mask == 0) {
10040 return true;
10042 if ($mask == 32) {
10043 if ($ip === $addr) {
10044 return true;
10046 continue;
10048 $mask = 0xffffffff << (32 - $mask);
10049 if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
10050 return true;
10054 } else if (strpos($subnet, '-') !== false) {
10055 /// 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.
10056 $parts = explode('-', $subnet);
10057 if (count($parts) != 2) {
10058 continue;
10061 if (strpos($subnet, ':') !== false) {
10062 // IPv6
10063 if (!$ipv6) {
10064 continue;
10066 $ipstart = cleanremoteaddr(trim($parts[0]), false); // normalise
10067 if ($ipstart === null) {
10068 continue;
10070 $ipparts = explode(':', $ipstart);
10071 $start = hexdec(array_pop($ipparts));
10072 $ipparts[] = trim($parts[1]);
10073 $ipend = cleanremoteaddr(implode(':', $ipparts), false); // normalise
10074 if ($ipend === null) {
10075 continue;
10077 $ipparts[7] = '';
10078 $ipnet = implode(':', $ipparts);
10079 if (strpos($addr, $ipnet) !== 0) {
10080 continue;
10082 $ipparts = explode(':', $ipend);
10083 $end = hexdec($ipparts[7]);
10085 $addrend = hexdec($addrparts[7]);
10087 if (($addrend >= $start) and ($addrend <= $end)) {
10088 return true;
10091 } else {
10092 // IPv4
10093 if ($ipv6) {
10094 continue;
10096 $ipstart = cleanremoteaddr(trim($parts[0]), false); // normalise
10097 if ($ipstart === null) {
10098 continue;
10100 $ipparts = explode('.', $ipstart);
10101 $ipparts[3] = trim($parts[1]);
10102 $ipend = cleanremoteaddr(implode('.', $ipparts), false); // normalise
10103 if ($ipend === null) {
10104 continue;
10107 if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
10108 return true;
10112 } else {
10113 /// 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
10114 if (strpos($subnet, ':') !== false) {
10115 // IPv6
10116 if (!$ipv6) {
10117 continue;
10119 $parts = explode(':', $subnet);
10120 $count = count($parts);
10121 if ($parts[$count-1] === '') {
10122 unset($parts[$count-1]); // trim trailing :
10123 $count--;
10124 $subnet = implode('.', $parts);
10126 $isip = cleanremoteaddr($subnet, false); // normalise
10127 if ($isip !== null) {
10128 if ($isip === $addr) {
10129 return true;
10131 continue;
10132 } else if ($count > 8) {
10133 continue;
10135 $zeros = array_fill(0, 8-$count, '0');
10136 $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16);
10137 if (address_in_subnet($addr, $subnet)) {
10138 return true;
10141 } else {
10142 // IPv4
10143 if ($ipv6) {
10144 continue;
10146 $parts = explode('.', $subnet);
10147 $count = count($parts);
10148 if ($parts[$count-1] === '') {
10149 unset($parts[$count-1]); // trim trailing .
10150 $count--;
10151 $subnet = implode('.', $parts);
10153 if ($count == 4) {
10154 $subnet = cleanremoteaddr($subnet, false); // normalise
10155 if ($subnet === $addr) {
10156 return true;
10158 continue;
10159 } else if ($count > 4) {
10160 continue;
10162 $zeros = array_fill(0, 4-$count, '0');
10163 $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8);
10164 if (address_in_subnet($addr, $subnet)) {
10165 return true;
10171 return false;
10175 * For outputting debugging info
10177 * @uses STDOUT
10178 * @param string $string The string to write
10179 * @param string $eol The end of line char(s) to use
10180 * @param string $sleep Period to make the application sleep
10181 * This ensures any messages have time to display before redirect
10183 function mtrace($string, $eol="\n", $sleep=0) {
10185 if (defined('STDOUT') and !PHPUNIT_TEST) {
10186 fwrite(STDOUT, $string.$eol);
10187 } else {
10188 echo $string . $eol;
10191 flush();
10193 //delay to keep message on user's screen in case of subsequent redirect
10194 if ($sleep) {
10195 sleep($sleep);
10200 * Replace 1 or more slashes or backslashes to 1 slash
10202 * @param string $path The path to strip
10203 * @return string the path with double slashes removed
10205 function cleardoubleslashes ($path) {
10206 return preg_replace('/(\/|\\\){1,}/','/',$path);
10210 * Is current ip in give list?
10212 * @param string $list
10213 * @return bool
10215 function remoteip_in_list($list){
10216 $inlist = false;
10217 $client_ip = getremoteaddr(null);
10219 if(!$client_ip){
10220 // ensure access on cli
10221 return true;
10224 $list = explode("\n", $list);
10225 foreach($list as $subnet) {
10226 $subnet = trim($subnet);
10227 if (address_in_subnet($client_ip, $subnet)) {
10228 $inlist = true;
10229 break;
10232 return $inlist;
10236 * Returns most reliable client address
10238 * @global object
10239 * @param string $default If an address can't be determined, then return this
10240 * @return string The remote IP address
10242 function getremoteaddr($default='0.0.0.0') {
10243 global $CFG;
10245 if (empty($CFG->getremoteaddrconf)) {
10246 // This will happen, for example, before just after the upgrade, as the
10247 // user is redirected to the admin screen.
10248 $variablestoskip = 0;
10249 } else {
10250 $variablestoskip = $CFG->getremoteaddrconf;
10252 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) {
10253 if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
10254 $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']);
10255 return $address ? $address : $default;
10258 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
10259 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
10260 $address = cleanremoteaddr($_SERVER['HTTP_X_FORWARDED_FOR']);
10261 return $address ? $address : $default;
10264 if (!empty($_SERVER['REMOTE_ADDR'])) {
10265 $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']);
10266 return $address ? $address : $default;
10267 } else {
10268 return $default;
10273 * Cleans an ip address. Internal addresses are now allowed.
10274 * (Originally local addresses were not allowed.)
10276 * @param string $addr IPv4 or IPv6 address
10277 * @param bool $compress use IPv6 address compression
10278 * @return string normalised ip address string, null if error
10280 function cleanremoteaddr($addr, $compress=false) {
10281 $addr = trim($addr);
10283 //TODO: maybe add a separate function is_addr_public() or something like this
10285 if (strpos($addr, ':') !== false) {
10286 // can be only IPv6
10287 $parts = explode(':', $addr);
10288 $count = count($parts);
10290 if (strpos($parts[$count-1], '.') !== false) {
10291 //legacy ipv4 notation
10292 $last = array_pop($parts);
10293 $ipv4 = cleanremoteaddr($last, true);
10294 if ($ipv4 === null) {
10295 return null;
10297 $bits = explode('.', $ipv4);
10298 $parts[] = dechex($bits[0]).dechex($bits[1]);
10299 $parts[] = dechex($bits[2]).dechex($bits[3]);
10300 $count = count($parts);
10301 $addr = implode(':', $parts);
10304 if ($count < 3 or $count > 8) {
10305 return null; // severly malformed
10308 if ($count != 8) {
10309 if (strpos($addr, '::') === false) {
10310 return null; // malformed
10312 // uncompress ::
10313 $insertat = array_search('', $parts, true);
10314 $missing = array_fill(0, 1 + 8 - $count, '0');
10315 array_splice($parts, $insertat, 1, $missing);
10316 foreach ($parts as $key=>$part) {
10317 if ($part === '') {
10318 $parts[$key] = '0';
10323 $adr = implode(':', $parts);
10324 if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) {
10325 return null; // incorrect format - sorry
10328 // normalise 0s and case
10329 $parts = array_map('hexdec', $parts);
10330 $parts = array_map('dechex', $parts);
10332 $result = implode(':', $parts);
10334 if (!$compress) {
10335 return $result;
10338 if ($result === '0:0:0:0:0:0:0:0') {
10339 return '::'; // all addresses
10342 $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1);
10343 if ($compressed !== $result) {
10344 return $compressed;
10347 $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
10348 if ($compressed !== $result) {
10349 return $compressed;
10352 $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
10353 if ($compressed !== $result) {
10354 return $compressed;
10357 return $result;
10360 // first get all things that look like IPv4 addresses
10361 $parts = array();
10362 if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
10363 return null;
10365 unset($parts[0]);
10367 foreach ($parts as $key=>$match) {
10368 if ($match > 255) {
10369 return null;
10371 $parts[$key] = (int)$match; // normalise 0s
10374 return implode('.', $parts);
10378 * This function will make a complete copy of anything it's given,
10379 * regardless of whether it's an object or not.
10381 * @param mixed $thing Something you want cloned
10382 * @return mixed What ever it is you passed it
10384 function fullclone($thing) {
10385 return unserialize(serialize($thing));
10390 * This function expects to called during shutdown
10391 * should be set via register_shutdown_function()
10392 * in lib/setup.php .
10394 * @return void
10396 function moodle_request_shutdown() {
10397 global $CFG;
10399 // help apache server if possible
10400 $apachereleasemem = false;
10401 if (function_exists('apache_child_terminate') && function_exists('memory_get_usage')
10402 && ini_get_bool('child_terminate')) {
10404 $limit = (empty($CFG->apachemaxmem) ? 64*1024*1024 : $CFG->apachemaxmem); //64MB default
10405 if (memory_get_usage() > get_real_size($limit)) {
10406 $apachereleasemem = $limit;
10407 @apache_child_terminate();
10411 // deal with perf logging
10412 if (defined('MDL_PERF') || (!empty($CFG->perfdebug) and $CFG->perfdebug > 7)) {
10413 if ($apachereleasemem) {
10414 error_log('Mem usage over '.$apachereleasemem.': marking Apache child for reaping.');
10416 if (defined('MDL_PERFTOLOG')) {
10417 $perf = get_performance_info();
10418 error_log("PERF: " . $perf['txt']);
10420 if (defined('MDL_PERFINC')) {
10421 $inc = get_included_files();
10422 $ts = 0;
10423 foreach($inc as $f) {
10424 if (preg_match(':^/:', $f)) {
10425 $fs = filesize($f);
10426 $ts += $fs;
10427 $hfs = display_size($fs);
10428 error_log(substr($f,strlen($CFG->dirroot)) . " size: $fs ($hfs)"
10429 , NULL, NULL, 0);
10430 } else {
10431 error_log($f , NULL, NULL, 0);
10434 if ($ts > 0 ) {
10435 $hts = display_size($ts);
10436 error_log("Total size of files included: $ts ($hts)");
10443 * If new messages are waiting for the current user, then insert
10444 * JavaScript to pop up the messaging window into the page
10446 * @global moodle_page $PAGE
10447 * @return void
10449 function message_popup_window() {
10450 global $USER, $DB, $PAGE, $CFG, $SITE;
10452 if (!$PAGE->get_popup_notification_allowed() || empty($CFG->messaging)) {
10453 return;
10456 if (!isloggedin() || isguestuser()) {
10457 return;
10460 if (!isset($USER->message_lastpopup)) {
10461 $USER->message_lastpopup = 0;
10462 } else if ($USER->message_lastpopup > (time()-120)) {
10463 //dont run the query to check whether to display a popup if its been run in the last 2 minutes
10464 return;
10467 //a quick query to check whether the user has new messages
10468 $messagecount = $DB->count_records('message', array('useridto' => $USER->id));
10469 if ($messagecount<1) {
10470 return;
10473 //got unread messages so now do another query that joins with the user table
10474 $messagesql = "SELECT m.id, m.smallmessage, m.fullmessageformat, m.notification, u.firstname, u.lastname
10475 FROM {message} m
10476 JOIN {message_working} mw ON m.id=mw.unreadmessageid
10477 JOIN {message_processors} p ON mw.processorid=p.id
10478 JOIN {user} u ON m.useridfrom=u.id
10479 WHERE m.useridto = :userid
10480 AND p.name='popup'";
10482 //if the user was last notified over an hour ago we can renotify them of old messages
10483 //so don't worry about when the new message was sent
10484 $lastnotifiedlongago = $USER->message_lastpopup < (time()-3600);
10485 if (!$lastnotifiedlongago) {
10486 $messagesql .= 'AND m.timecreated > :lastpopuptime';
10489 $message_users = $DB->get_records_sql($messagesql, array('userid'=>$USER->id, 'lastpopuptime'=>$USER->message_lastpopup));
10491 //if we have new messages to notify the user about
10492 if (!empty($message_users)) {
10494 $strmessages = '';
10495 if (count($message_users)>1) {
10496 $strmessages = get_string('unreadnewmessages', 'message', count($message_users));
10497 } else {
10498 $message_users = reset($message_users);
10500 //show who the message is from if its not a notification
10501 if (!$message_users->notification) {
10502 $strmessages = get_string('unreadnewmessage', 'message', fullname($message_users) );
10505 //try to display the small version of the message
10506 $smallmessage = null;
10507 if (!empty($message_users->smallmessage)) {
10508 //display the first 200 chars of the message in the popup
10509 $smallmessage = null;
10510 if (textlib::strlen($message_users->smallmessage) > 200) {
10511 $smallmessage = textlib::substr($message_users->smallmessage,0,200).'...';
10512 } else {
10513 $smallmessage = $message_users->smallmessage;
10516 //prevent html symbols being displayed
10517 if ($message_users->fullmessageformat == FORMAT_HTML) {
10518 $smallmessage = html_to_text($smallmessage);
10519 } else {
10520 $smallmessage = s($smallmessage);
10522 } else if ($message_users->notification) {
10523 //its a notification with no smallmessage so just say they have a notification
10524 $smallmessage = get_string('unreadnewnotification', 'message');
10526 if (!empty($smallmessage)) {
10527 $strmessages .= '<div id="usermessage">'.s($smallmessage).'</div>';
10531 $strgomessage = get_string('gotomessages', 'message');
10532 $strstaymessage = get_string('ignore','admin');
10534 $url = $CFG->wwwroot.'/message/index.php';
10535 $content = html_writer::start_tag('div', array('id'=>'newmessageoverlay','class'=>'mdl-align')).
10536 html_writer::start_tag('div', array('id'=>'newmessagetext')).
10537 $strmessages.
10538 html_writer::end_tag('div').
10540 html_writer::start_tag('div', array('id'=>'newmessagelinks')).
10541 html_writer::link($url, $strgomessage, array('id'=>'notificationyes')).'&nbsp;&nbsp;&nbsp;'.
10542 html_writer::link('', $strstaymessage, array('id'=>'notificationno')).
10543 html_writer::end_tag('div');
10544 html_writer::end_tag('div');
10546 $PAGE->requires->js_init_call('M.core_message.init_notification', array('', $content, $url));
10548 $USER->message_lastpopup = time();
10553 * Used to make sure that $min <= $value <= $max
10555 * Make sure that value is between min, and max
10557 * @param int $min The minimum value
10558 * @param int $value The value to check
10559 * @param int $max The maximum value
10561 function bounded_number($min, $value, $max) {
10562 if($value < $min) {
10563 return $min;
10565 if($value > $max) {
10566 return $max;
10568 return $value;
10572 * Check if there is a nested array within the passed array
10574 * @param array $array
10575 * @return bool true if there is a nested array false otherwise
10577 function array_is_nested($array) {
10578 foreach ($array as $value) {
10579 if (is_array($value)) {
10580 return true;
10583 return false;
10587 * get_performance_info() pairs up with init_performance_info()
10588 * loaded in setup.php. Returns an array with 'html' and 'txt'
10589 * values ready for use, and each of the individual stats provided
10590 * separately as well.
10592 * @global object
10593 * @global object
10594 * @global object
10595 * @return array
10597 function get_performance_info() {
10598 global $CFG, $PERF, $DB, $PAGE;
10600 $info = array();
10601 $info['html'] = ''; // holds userfriendly HTML representation
10602 $info['txt'] = me() . ' '; // holds log-friendly representation
10604 $info['realtime'] = microtime_diff($PERF->starttime, microtime());
10606 $info['html'] .= '<span class="timeused">'.$info['realtime'].' secs</span> ';
10607 $info['txt'] .= 'time: '.$info['realtime'].'s ';
10609 if (function_exists('memory_get_usage')) {
10610 $info['memory_total'] = memory_get_usage();
10611 $info['memory_growth'] = memory_get_usage() - $PERF->startmemory;
10612 $info['html'] .= '<span class="memoryused">RAM: '.display_size($info['memory_total']).'</span> ';
10613 $info['txt'] .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '.$info['memory_growth'].'B ('.display_size($info['memory_growth']).') ';
10616 if (function_exists('memory_get_peak_usage')) {
10617 $info['memory_peak'] = memory_get_peak_usage();
10618 $info['html'] .= '<span class="memoryused">RAM peak: '.display_size($info['memory_peak']).'</span> ';
10619 $info['txt'] .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') ';
10622 $inc = get_included_files();
10623 //error_log(print_r($inc,1));
10624 $info['includecount'] = count($inc);
10625 $info['html'] .= '<span class="included">Included '.$info['includecount'].' files</span> ';
10626 $info['txt'] .= 'includecount: '.$info['includecount'].' ';
10628 if (!empty($CFG->early_install_lang) or empty($PAGE)) {
10629 // We can not track more performance before installation or before PAGE init, sorry.
10630 return $info;
10633 $filtermanager = filter_manager::instance();
10634 if (method_exists($filtermanager, 'get_performance_summary')) {
10635 list($filterinfo, $nicenames) = $filtermanager->get_performance_summary();
10636 $info = array_merge($filterinfo, $info);
10637 foreach ($filterinfo as $key => $value) {
10638 $info['html'] .= "<span class='$key'>$nicenames[$key]: $value </span> ";
10639 $info['txt'] .= "$key: $value ";
10643 $stringmanager = get_string_manager();
10644 if (method_exists($stringmanager, 'get_performance_summary')) {
10645 list($filterinfo, $nicenames) = $stringmanager->get_performance_summary();
10646 $info = array_merge($filterinfo, $info);
10647 foreach ($filterinfo as $key => $value) {
10648 $info['html'] .= "<span class='$key'>$nicenames[$key]: $value </span> ";
10649 $info['txt'] .= "$key: $value ";
10653 $jsmodules = $PAGE->requires->get_loaded_modules();
10654 if ($jsmodules) {
10655 $yuicount = 0;
10656 $othercount = 0;
10657 $details = '';
10658 foreach ($jsmodules as $module => $backtraces) {
10659 if (strpos($module, 'yui') === 0) {
10660 $yuicount += 1;
10661 } else {
10662 $othercount += 1;
10664 if (!empty($CFG->yuimoduledebug)) {
10665 // hidden feature for developers working on YUI module infrastructure
10666 $details .= "<div class='yui-module'><p>$module</p>";
10667 foreach ($backtraces as $backtrace) {
10668 $details .= "<div class='backtrace'>$backtrace</div>";
10670 $details .= '</div>';
10673 $info['html'] .= "<span class='includedyuimodules'>Included YUI modules: $yuicount</span> ";
10674 $info['txt'] .= "includedyuimodules: $yuicount ";
10675 $info['html'] .= "<span class='includedjsmodules'>Other JavaScript modules: $othercount</span> ";
10676 $info['txt'] .= "includedjsmodules: $othercount ";
10677 if ($details) {
10678 $info['html'] .= '<div id="yui-module-debug" class="notifytiny">'.$details.'</div>';
10682 if (!empty($PERF->logwrites)) {
10683 $info['logwrites'] = $PERF->logwrites;
10684 $info['html'] .= '<span class="logwrites">Log DB writes '.$info['logwrites'].'</span> ';
10685 $info['txt'] .= 'logwrites: '.$info['logwrites'].' ';
10688 $info['dbqueries'] = $DB->perf_get_reads().'/'.($DB->perf_get_writes() - $PERF->logwrites);
10689 $info['html'] .= '<span class="dbqueries">DB reads/writes: '.$info['dbqueries'].'</span> ';
10690 $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' ';
10692 if (function_exists('posix_times')) {
10693 $ptimes = posix_times();
10694 if (is_array($ptimes)) {
10695 foreach ($ptimes as $key => $val) {
10696 $info[$key] = $ptimes[$key] - $PERF->startposixtimes[$key];
10698 $info['html'] .= "<span class=\"posixtimes\">ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</span> ";
10699 $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] ";
10703 // Grab the load average for the last minute
10704 // /proc will only work under some linux configurations
10705 // while uptime is there under MacOSX/Darwin and other unices
10706 if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) {
10707 list($server_load) = explode(' ', $loadavg[0]);
10708 unset($loadavg);
10709 } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) {
10710 if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) {
10711 $server_load = $matches[1];
10712 } else {
10713 trigger_error('Could not parse uptime output!');
10716 if (!empty($server_load)) {
10717 $info['serverload'] = $server_load;
10718 $info['html'] .= '<span class="serverload">Load average: '.$info['serverload'].'</span> ';
10719 $info['txt'] .= "serverload: {$info['serverload']} ";
10722 // Display size of session if session started
10723 if (session_id()) {
10724 $info['sessionsize'] = display_size(strlen(session_encode()));
10725 $info['html'] .= '<span class="sessionsize">Session: ' . $info['sessionsize'] . '</span> ';
10726 $info['txt'] .= "Session: {$info['sessionsize']} ";
10729 if ($stats = cache_helper::get_stats()) {
10730 $html = '<span class="cachesused">';
10731 $html .= '<span class="cache-stats-heading">Caches used (hits/misses/sets)</span>';
10732 $text = 'Caches used (hits/misses/sets): ';
10733 $hits = 0;
10734 $misses = 0;
10735 $sets = 0;
10736 foreach ($stats as $definition => $stores) {
10737 $html .= '<span class="cache-definition-stats">';
10738 $html .= '<span class="cache-definition-stats-heading">'.$definition.'</span>';
10739 $text .= "$definition {";
10740 foreach ($stores as $store => $data) {
10741 $hits += $data['hits'];
10742 $misses += $data['misses'];
10743 $sets += $data['sets'];
10744 if ($data['hits'] == 0 and $data['misses'] > 0) {
10745 $cachestoreclass = 'nohits';
10746 } else if ($data['hits'] < $data['misses']) {
10747 $cachestoreclass = 'lowhits';
10748 } else {
10749 $cachestoreclass = 'hihits';
10751 $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
10752 $html .= "<span class=\"cache-store-stats $cachestoreclass\">$store: $data[hits] / $data[misses] / $data[sets]</span>";
10754 $html .= '</span>';
10755 $text .= '} ';
10757 $html .= "<span class='cache-total-stats'>Total: $hits / $misses / $sets</span>";
10758 $html .= '</span> ';
10759 $info['cachesused'] = "$hits / $misses / $sets";
10760 $info['html'] .= $html;
10761 $info['txt'] .= $text.'. ';
10762 } else {
10763 $info['cachesused'] = '0 / 0 / 0';
10764 $info['html'] .= '<span class="cachesused">Caches used (hits/misses/sets): 0/0/0</span>';
10765 $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 ';
10768 $info['html'] = '<div class="performanceinfo siteinfo">'.$info['html'].'</div>';
10769 return $info;
10773 * @todo Document this function linux people
10775 function apd_get_profiling() {
10776 return shell_exec('pprofp -u ' . ini_get('apd.dumpdir') . '/pprof.' . getmypid() . '.*');
10780 * Delete directory or only its content
10782 * @param string $dir directory path
10783 * @param bool $content_only
10784 * @return bool success, true also if dir does not exist
10786 function remove_dir($dir, $content_only=false) {
10787 if (!file_exists($dir)) {
10788 // nothing to do
10789 return true;
10791 if (!$handle = opendir($dir)) {
10792 return false;
10794 $result = true;
10795 while (false!==($item = readdir($handle))) {
10796 if($item != '.' && $item != '..') {
10797 if(is_dir($dir.'/'.$item)) {
10798 $result = remove_dir($dir.'/'.$item) && $result;
10799 }else{
10800 $result = unlink($dir.'/'.$item) && $result;
10804 closedir($handle);
10805 if ($content_only) {
10806 clearstatcache(); // make sure file stat cache is properly invalidated
10807 return $result;
10809 $result = rmdir($dir); // if anything left the result will be false, no need for && $result
10810 clearstatcache(); // make sure file stat cache is properly invalidated
10811 return $result;
10815 * Detect if an object or a class contains a given property
10816 * will take an actual object or the name of a class
10818 * @param mix $obj Name of class or real object to test
10819 * @param string $property name of property to find
10820 * @return bool true if property exists
10822 function object_property_exists( $obj, $property ) {
10823 if (is_string( $obj )) {
10824 $properties = get_class_vars( $obj );
10826 else {
10827 $properties = get_object_vars( $obj );
10829 return array_key_exists( $property, $properties );
10833 * Converts an object into an associative array
10835 * This function converts an object into an associative array by iterating
10836 * over its public properties. Because this function uses the foreach
10837 * construct, Iterators are respected. It works recursively on arrays of objects.
10838 * Arrays and simple values are returned as is.
10840 * If class has magic properties, it can implement IteratorAggregate
10841 * and return all available properties in getIterator()
10843 * @param mixed $var
10844 * @return array
10846 function convert_to_array($var) {
10847 $result = array();
10849 // loop over elements/properties
10850 foreach ($var as $key => $value) {
10851 // recursively convert objects
10852 if (is_object($value) || is_array($value)) {
10853 $result[$key] = convert_to_array($value);
10854 } else {
10855 // simple values are untouched
10856 $result[$key] = $value;
10859 return $result;
10863 * Detect a custom script replacement in the data directory that will
10864 * replace an existing moodle script
10866 * @return string|bool full path name if a custom script exists, false if no custom script exists
10868 function custom_script_path() {
10869 global $CFG, $SCRIPT;
10871 if ($SCRIPT === null) {
10872 // Probably some weird external script
10873 return false;
10876 $scriptpath = $CFG->customscripts . $SCRIPT;
10878 // check the custom script exists
10879 if (file_exists($scriptpath) and is_file($scriptpath)) {
10880 return $scriptpath;
10881 } else {
10882 return false;
10887 * Returns whether or not the user object is a remote MNET user. This function
10888 * is in moodlelib because it does not rely on loading any of the MNET code.
10890 * @global object
10891 * @param object $user A valid user object
10892 * @return bool True if the user is from a remote Moodle.
10894 function is_mnet_remote_user($user) {
10895 global $CFG;
10897 if (!isset($CFG->mnet_localhost_id)) {
10898 include_once $CFG->dirroot . '/mnet/lib.php';
10899 $env = new mnet_environment();
10900 $env->init();
10901 unset($env);
10904 return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id);
10908 * This function will search for browser prefereed languages, setting Moodle
10909 * to use the best one available if $SESSION->lang is undefined
10911 * @global object
10912 * @global object
10913 * @global object
10915 function setup_lang_from_browser() {
10917 global $CFG, $SESSION, $USER;
10919 if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) {
10920 // Lang is defined in session or user profile, nothing to do
10921 return;
10924 if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do
10925 return;
10928 /// Extract and clean langs from headers
10929 $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
10930 $rawlangs = str_replace('-', '_', $rawlangs); // we are using underscores
10931 $rawlangs = explode(',', $rawlangs); // Convert to array
10932 $langs = array();
10934 $order = 1.0;
10935 foreach ($rawlangs as $lang) {
10936 if (strpos($lang, ';') === false) {
10937 $langs[(string)$order] = $lang;
10938 $order = $order-0.01;
10939 } else {
10940 $parts = explode(';', $lang);
10941 $pos = strpos($parts[1], '=');
10942 $langs[substr($parts[1], $pos+1)] = $parts[0];
10945 krsort($langs, SORT_NUMERIC);
10947 /// Look for such langs under standard locations
10948 foreach ($langs as $lang) {
10949 $lang = strtolower(clean_param($lang, PARAM_SAFEDIR)); // clean it properly for include
10950 if (get_string_manager()->translation_exists($lang, false)) {
10951 $SESSION->lang = $lang; /// Lang exists, set it in session
10952 break; /// We have finished. Go out
10955 return;
10959 * check if $url matches anything in proxybypass list
10961 * any errors just result in the proxy being used (least bad)
10963 * @global object
10964 * @param string $url url to check
10965 * @return boolean true if we should bypass the proxy
10967 function is_proxybypass( $url ) {
10968 global $CFG;
10970 // sanity check
10971 if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) {
10972 return false;
10975 // get the host part out of the url
10976 if (!$host = parse_url( $url, PHP_URL_HOST )) {
10977 return false;
10980 // get the possible bypass hosts into an array
10981 $matches = explode( ',', $CFG->proxybypass );
10983 // check for a match
10984 // (IPs need to match the left hand side and hosts the right of the url,
10985 // but we can recklessly check both as there can't be a false +ve)
10986 $bypass = false;
10987 foreach ($matches as $match) {
10988 $match = trim($match);
10990 // try for IP match (Left side)
10991 $lhs = substr($host,0,strlen($match));
10992 if (strcasecmp($match,$lhs)==0) {
10993 return true;
10996 // try for host match (Right side)
10997 $rhs = substr($host,-strlen($match));
10998 if (strcasecmp($match,$rhs)==0) {
10999 return true;
11003 // nothing matched.
11004 return false;
11008 ////////////////////////////////////////////////////////////////////////////////
11011 * Check if the passed navigation is of the new style
11013 * @param mixed $navigation
11014 * @return bool true for yes false for no
11016 function is_newnav($navigation) {
11017 if (is_array($navigation) && !empty($navigation['newnav'])) {
11018 return true;
11019 } else {
11020 return false;
11025 * Checks whether the given variable name is defined as a variable within the given object.
11027 * This will NOT work with stdClass objects, which have no class variables.
11029 * @param string $var The variable name
11030 * @param object $object The object to check
11031 * @return boolean
11033 function in_object_vars($var, $object) {
11034 $class_vars = get_class_vars(get_class($object));
11035 $class_vars = array_keys($class_vars);
11036 return in_array($var, $class_vars);
11040 * Returns an array without repeated objects.
11041 * This function is similar to array_unique, but for arrays that have objects as values
11043 * @param array $array
11044 * @param bool $keep_key_assoc
11045 * @return array
11047 function object_array_unique($array, $keep_key_assoc = true) {
11048 $duplicate_keys = array();
11049 $tmp = array();
11051 foreach ($array as $key=>$val) {
11052 // convert objects to arrays, in_array() does not support objects
11053 if (is_object($val)) {
11054 $val = (array)$val;
11057 if (!in_array($val, $tmp)) {
11058 $tmp[] = $val;
11059 } else {
11060 $duplicate_keys[] = $key;
11064 foreach ($duplicate_keys as $key) {
11065 unset($array[$key]);
11068 return $keep_key_assoc ? $array : array_values($array);
11072 * Is a userid the primary administrator?
11074 * @param int $userid int id of user to check
11075 * @return boolean
11077 function is_primary_admin($userid){
11078 $primaryadmin = get_admin();
11080 if($userid == $primaryadmin->id){
11081 return true;
11082 }else{
11083 return false;
11088 * Returns the site identifier
11090 * @global object
11091 * @return string $CFG->siteidentifier, first making sure it is properly initialised.
11093 function get_site_identifier() {
11094 global $CFG;
11095 // Check to see if it is missing. If so, initialise it.
11096 if (empty($CFG->siteidentifier)) {
11097 set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']);
11099 // Return it.
11100 return $CFG->siteidentifier;
11104 * Check whether the given password has no more than the specified
11105 * number of consecutive identical characters.
11107 * @param string $password password to be checked against the password policy
11108 * @param integer $maxchars maximum number of consecutive identical characters
11110 function check_consecutive_identical_characters($password, $maxchars) {
11112 if ($maxchars < 1) {
11113 return true; // 0 is to disable this check
11115 if (strlen($password) <= $maxchars) {
11116 return true; // too short to fail this test
11119 $previouschar = '';
11120 $consecutivecount = 1;
11121 foreach (str_split($password) as $char) {
11122 if ($char != $previouschar) {
11123 $consecutivecount = 1;
11125 else {
11126 $consecutivecount++;
11127 if ($consecutivecount > $maxchars) {
11128 return false; // check failed already
11132 $previouschar = $char;
11135 return true;
11139 * helper function to do partial function binding
11140 * so we can use it for preg_replace_callback, for example
11141 * this works with php functions, user functions, static methods and class methods
11142 * it returns you a callback that you can pass on like so:
11144 * $callback = partial('somefunction', $arg1, $arg2);
11145 * or
11146 * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
11147 * or even
11148 * $obj = new someclass();
11149 * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2);
11151 * and then the arguments that are passed through at calltime are appended to the argument list.
11153 * @param mixed $function a php callback
11154 * $param mixed $arg1.. $argv arguments to partially bind with
11156 * @return callback
11158 function partial() {
11159 if (!class_exists('partial')) {
11160 class partial{
11161 var $values = array();
11162 var $func;
11164 function __construct($func, $args) {
11165 $this->values = $args;
11166 $this->func = $func;
11169 function method() {
11170 $args = func_get_args();
11171 return call_user_func_array($this->func, array_merge($this->values, $args));
11175 $args = func_get_args();
11176 $func = array_shift($args);
11177 $p = new partial($func, $args);
11178 return array($p, 'method');
11182 * helper function to load up and initialise the mnet environment
11183 * this must be called before you use mnet functions.
11185 * @return mnet_environment the equivalent of old $MNET global
11187 function get_mnet_environment() {
11188 global $CFG;
11189 require_once($CFG->dirroot . '/mnet/lib.php');
11190 static $instance = null;
11191 if (empty($instance)) {
11192 $instance = new mnet_environment();
11193 $instance->init();
11195 return $instance;
11199 * during xmlrpc server code execution, any code wishing to access
11200 * information about the remote peer must use this to get it.
11202 * @return mnet_remote_client the equivalent of old $MNET_REMOTE_CLIENT global
11204 function get_mnet_remote_client() {
11205 if (!defined('MNET_SERVER')) {
11206 debugging(get_string('notinxmlrpcserver', 'mnet'));
11207 return false;
11209 global $MNET_REMOTE_CLIENT;
11210 if (isset($MNET_REMOTE_CLIENT)) {
11211 return $MNET_REMOTE_CLIENT;
11213 return false;
11217 * during the xmlrpc server code execution, this will be called
11218 * to setup the object returned by {@see get_mnet_remote_client}
11220 * @param mnet_remote_client $client the client to set up
11222 function set_mnet_remote_client($client) {
11223 if (!defined('MNET_SERVER')) {
11224 throw new moodle_exception('notinxmlrpcserver', 'mnet');
11226 global $MNET_REMOTE_CLIENT;
11227 $MNET_REMOTE_CLIENT = $client;
11231 * return the jump url for a given remote user
11232 * this is used for rewriting forum post links in emails, etc
11234 * @param stdclass $user the user to get the idp url for
11236 function mnet_get_idp_jump_url($user) {
11237 global $CFG;
11239 static $mnetjumps = array();
11240 if (!array_key_exists($user->mnethostid, $mnetjumps)) {
11241 $idp = mnet_get_peer_host($user->mnethostid);
11242 $idpjumppath = mnet_get_app_jumppath($idp->applicationid);
11243 $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl=';
11245 return $mnetjumps[$user->mnethostid];
11249 * Gets the homepage to use for the current user
11251 * @return int One of HOMEPAGE_*
11253 function get_home_page() {
11254 global $CFG;
11256 if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) {
11257 if ($CFG->defaulthomepage == HOMEPAGE_MY) {
11258 return HOMEPAGE_MY;
11259 } else {
11260 return (int)get_user_preferences('user_home_page_preference', HOMEPAGE_MY);
11263 return HOMEPAGE_SITE;
11267 * Gets the name of a course to be displayed when showing a list of courses.
11268 * By default this is just $course->fullname but user can configure it. The
11269 * result of this function should be passed through print_string.
11270 * @param stdClass|course_in_list $course Moodle course object
11271 * @return string Display name of course (either fullname or short + fullname)
11273 function get_course_display_name_for_list($course) {
11274 global $CFG;
11275 if (!empty($CFG->courselistshortnames)) {
11276 if (!($course instanceof stdClass)) {
11277 $course = (object)convert_to_array($course);
11279 return get_string('courseextendednamedisplay', '', $course);
11280 } else {
11281 return $course->fullname;
11286 * The lang_string class
11288 * This special class is used to create an object representation of a string request.
11289 * It is special because processing doesn't occur until the object is first used.
11290 * The class was created especially to aid performance in areas where strings were
11291 * required to be generated but were not necessarily used.
11292 * As an example the admin tree when generated uses over 1500 strings, of which
11293 * normally only 1/3 are ever actually printed at any time.
11294 * The performance advantage is achieved by not actually processing strings that
11295 * arn't being used, as such reducing the processing required for the page.
11297 * How to use the lang_string class?
11298 * There are two methods of using the lang_string class, first through the
11299 * forth argument of the get_string function, and secondly directly.
11300 * The following are examples of both.
11301 * 1. Through get_string calls e.g.
11302 * $string = get_string($identifier, $component, $a, true);
11303 * $string = get_string('yes', 'moodle', null, true);
11304 * 2. Direct instantiation
11305 * $string = new lang_string($identifier, $component, $a, $lang);
11306 * $string = new lang_string('yes');
11308 * How do I use a lang_string object?
11309 * The lang_string object makes use of a magic __toString method so that you
11310 * are able to use the object exactly as you would use a string in most cases.
11311 * This means you are able to collect it into a variable and then directly
11312 * echo it, or concatenate it into another string, or similar.
11313 * The other thing you can do is manually get the string by calling the
11314 * lang_strings out method e.g.
11315 * $string = new lang_string('yes');
11316 * $string->out();
11317 * Also worth noting is that the out method can take one argument, $lang which
11318 * allows the developer to change the language on the fly.
11320 * When should I use a lang_string object?
11321 * The lang_string object is designed to be used in any situation where a
11322 * string may not be needed, but needs to be generated.
11323 * The admin tree is a good example of where lang_string objects should be
11324 * used.
11325 * A more practical example would be any class that requries strings that may
11326 * not be printed (after all classes get renderer by renderers and who knows
11327 * what they will do ;))
11329 * When should I not use a lang_string object?
11330 * Don't use lang_strings when you are going to use a string immediately.
11331 * There is no need as it will be processed immediately and there will be no
11332 * advantage, and in fact perhaps a negative hit as a class has to be
11333 * instantiated for a lang_string object, however get_string won't require
11334 * that.
11336 * Limitations:
11337 * 1. You cannot use a lang_string object as an array offset. Doing so will
11338 * result in PHP throwing an error. (You can use it as an object property!)
11340 * @package core
11341 * @category string
11342 * @copyright 2011 Sam Hemelryk
11343 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
11345 class lang_string {
11347 /** @var string The strings identifier */
11348 protected $identifier;
11349 /** @var string The strings component. Default '' */
11350 protected $component = '';
11351 /** @var array|stdClass Any arguments required for the string. Default null */
11352 protected $a = null;
11353 /** @var string The language to use when processing the string. Default null */
11354 protected $lang = null;
11356 /** @var string The processed string (once processed) */
11357 protected $string = null;
11360 * A special boolean. If set to true then the object has been woken up and
11361 * cannot be regenerated. If this is set then $this->string MUST be used.
11362 * @var bool
11364 protected $forcedstring = false;
11367 * Constructs a lang_string object
11369 * This function should do as little processing as possible to ensure the best
11370 * performance for strings that won't be used.
11372 * @param string $identifier The strings identifier
11373 * @param string $component The strings component
11374 * @param stdClass|array $a Any arguments the string requires
11375 * @param string $lang The language to use when processing the string.
11377 public function __construct($identifier, $component = '', $a = null, $lang = null) {
11378 if (empty($component)) {
11379 $component = 'moodle';
11382 $this->identifier = $identifier;
11383 $this->component = $component;
11384 $this->lang = $lang;
11386 // We MUST duplicate $a to ensure that it if it changes by reference those
11387 // changes are not carried across.
11388 // To do this we always ensure $a or its properties/values are strings
11389 // and that any properties/values that arn't convertable are forgotten.
11390 if (!empty($a)) {
11391 if (is_scalar($a)) {
11392 $this->a = $a;
11393 } else if ($a instanceof lang_string) {
11394 $this->a = $a->out();
11395 } else if (is_object($a) or is_array($a)) {
11396 $a = (array)$a;
11397 $this->a = array();
11398 foreach ($a as $key => $value) {
11399 // Make sure conversion errors don't get displayed (results in '')
11400 if (is_array($value)) {
11401 $this->a[$key] = '';
11402 } else if (is_object($value)) {
11403 if (method_exists($value, '__toString')) {
11404 $this->a[$key] = $value->__toString();
11405 } else {
11406 $this->a[$key] = '';
11408 } else {
11409 $this->a[$key] = (string)$value;
11415 if (debugging(false, DEBUG_DEVELOPER)) {
11416 if (clean_param($this->identifier, PARAM_STRINGID) == '') {
11417 throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition');
11419 if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') {
11420 throw new coding_exception('Invalid string compontent. Please check your string definition');
11422 if (!get_string_manager()->string_exists($this->identifier, $this->component)) {
11423 debugging('String does not exist. Please check your string definition for '.$this->identifier.'/'.$this->component, DEBUG_DEVELOPER);
11429 * Processes the string.
11431 * This function actually processes the string, stores it in the string property
11432 * and then returns it.
11433 * You will notice that this function is VERY similar to the get_string method.
11434 * That is because it is pretty much doing the same thing.
11435 * However as this function is an upgrade it isn't as tolerant to backwards
11436 * compatability.
11438 * @return string
11440 protected function get_string() {
11441 global $CFG;
11443 // Check if we need to process the string
11444 if ($this->string === null) {
11445 // Check the quality of the identifier.
11446 if (debugging('', DEBUG_DEVELOPER) && clean_param($this->identifier, PARAM_STRINGID) === '') {
11447 throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition');
11450 // Process the string
11451 $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang);
11452 // Debugging feature lets you display string identifier and component
11453 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
11454 $this->string .= ' {' . $this->identifier . '/' . $this->component . '}';
11457 // Return the string
11458 return $this->string;
11462 * Returns the string
11464 * @param string $lang The langauge to use when processing the string
11465 * @return string
11467 public function out($lang = null) {
11468 if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) {
11469 if ($this->forcedstring) {
11470 debugging('lang_string objects that have been serialised and unserialised cannot be printed in another language. ('.$this->lang.' used)', DEBUG_DEVELOPER);
11471 return $this->get_string();
11473 $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang);
11474 return $translatedstring->out();
11476 return $this->get_string();
11480 * Magic __toString method for printing a string
11482 * @return string
11484 public function __toString() {
11485 return $this->get_string();
11489 * Magic __set_state method used for var_export
11491 * @return string
11493 public function __set_state() {
11494 return $this->get_string();
11498 * Prepares the lang_string for sleep and stores only the forcedstring and
11499 * string properties... the string cannot be regenerated so we need to ensure
11500 * it is generated for this.
11502 * @return string
11504 public function __sleep() {
11505 $this->get_string();
11506 $this->forcedstring = true;
11507 return array('forcedstring', 'string', 'lang');