MDL-31202 do not try sending emails to invalid addresses
[moodle.git] / lib / moodlelib.php
blobe17d8d089f9bc80edd84189e0f75d151bfaaaeaa
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 /**
19 * moodlelib.php - Moodle main library
21 * Main library file of miscellaneous general-purpose Moodle functions.
22 * Other main libraries:
23 * - weblib.php - functions that produce web output
24 * - datalib.php - functions that access the database
26 * @package core
27 * @subpackage lib
28 * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com
29 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
32 defined('MOODLE_INTERNAL') || die();
34 /// CONSTANTS (Encased in phpdoc proper comments)/////////////////////////
36 /// Date and time constants ///
37 /**
38 * Time constant - the number of seconds in a year
40 define('YEARSECS', 31536000);
42 /**
43 * Time constant - the number of seconds in a week
45 define('WEEKSECS', 604800);
47 /**
48 * Time constant - the number of seconds in a day
50 define('DAYSECS', 86400);
52 /**
53 * Time constant - the number of seconds in an hour
55 define('HOURSECS', 3600);
57 /**
58 * Time constant - the number of seconds in a minute
60 define('MINSECS', 60);
62 /**
63 * Time constant - the number of minutes in a day
65 define('DAYMINS', 1440);
67 /**
68 * Time constant - the number of minutes in an hour
70 define('HOURMINS', 60);
72 /// Parameter constants - every call to optional_param(), required_param() ///
73 /// or clean_param() should have a specified type of parameter. //////////////
77 /**
78 * PARAM_ALPHA - contains only english ascii letters a-zA-Z.
80 define('PARAM_ALPHA', 'alpha');
82 /**
83 * PARAM_ALPHAEXT the same contents as PARAM_ALPHA plus the chars in quotes: "_-" allowed
84 * NOTE: originally this allowed "/" too, please use PARAM_SAFEPATH if "/" needed
86 define('PARAM_ALPHAEXT', 'alphaext');
88 /**
89 * PARAM_ALPHANUM - expected numbers and letters only.
91 define('PARAM_ALPHANUM', 'alphanum');
93 /**
94 * PARAM_ALPHANUMEXT - expected numbers, letters only and _-.
96 define('PARAM_ALPHANUMEXT', 'alphanumext');
98 /**
99 * PARAM_AUTH - actually checks to make sure the string is a valid auth plugin
101 define('PARAM_AUTH', 'auth');
104 * PARAM_BASE64 - Base 64 encoded format
106 define('PARAM_BASE64', 'base64');
109 * PARAM_BOOL - converts input into 0 or 1, use for switches in forms and urls.
111 define('PARAM_BOOL', 'bool');
114 * PARAM_CAPABILITY - A capability name, like 'moodle/role:manage'. Actually
115 * checked against the list of capabilities in the database.
117 define('PARAM_CAPABILITY', 'capability');
120 * PARAM_CLEANHTML - cleans submitted HTML code. use only for text in HTML format. This cleaning may fix xhtml strictness too.
122 define('PARAM_CLEANHTML', 'cleanhtml');
125 * PARAM_EMAIL - an email address following the RFC
127 define('PARAM_EMAIL', 'email');
130 * PARAM_FILE - safe file name, all dangerous chars are stripped, protects against XSS, SQL injections and directory traversals
132 define('PARAM_FILE', 'file');
135 * PARAM_FLOAT - a real/floating point number.
137 define('PARAM_FLOAT', 'float');
140 * PARAM_HOST - expected fully qualified domain name (FQDN) or an IPv4 dotted quad (IP address)
142 define('PARAM_HOST', 'host');
145 * PARAM_INT - integers only, use when expecting only numbers.
147 define('PARAM_INT', 'int');
150 * PARAM_LANG - checks to see if the string is a valid installed language in the current site.
152 define('PARAM_LANG', 'lang');
155 * 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!)
157 define('PARAM_LOCALURL', 'localurl');
160 * PARAM_NOTAGS - all html tags are stripped from the text. Do not abuse this type.
162 define('PARAM_NOTAGS', 'notags');
165 * PARAM_PATH - safe relative path name, all dangerous chars are stripped, protects against XSS, SQL injections and directory traversals
166 * note: the leading slash is not removed, window drive letter is not allowed
168 define('PARAM_PATH', 'path');
171 * PARAM_PEM - Privacy Enhanced Mail format
173 define('PARAM_PEM', 'pem');
176 * PARAM_PERMISSION - A permission, one of CAP_INHERIT, CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT.
178 define('PARAM_PERMISSION', 'permission');
181 * PARAM_RAW specifies a parameter that is not cleaned/processed in any way except the discarding of the invalid utf-8 characters
183 define('PARAM_RAW', 'raw');
186 * PARAM_RAW_TRIMMED like PARAM_RAW but leading and trailing whitespace is stripped.
188 define('PARAM_RAW_TRIMMED', 'raw_trimmed');
191 * PARAM_SAFEDIR - safe directory name, suitable for include() and require()
193 define('PARAM_SAFEDIR', 'safedir');
196 * PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths, etc.
198 define('PARAM_SAFEPATH', 'safepath');
201 * PARAM_SEQUENCE - expects a sequence of numbers like 8 to 1,5,6,4,6,8,9. Numbers and comma only.
203 define('PARAM_SEQUENCE', 'sequence');
206 * PARAM_TAG - one tag (interests, blogs, etc.) - mostly international characters and space, <> not supported
208 define('PARAM_TAG', 'tag');
211 * PARAM_TAGLIST - list of tags separated by commas (interests, blogs, etc.)
213 define('PARAM_TAGLIST', 'taglist');
216 * PARAM_TEXT - general plain text compatible with multilang filter, no other html tags. Please note '<', or '>' are allowed here.
218 define('PARAM_TEXT', 'text');
221 * PARAM_THEME - Checks to see if the string is a valid theme name in the current site
223 define('PARAM_THEME', 'theme');
226 * PARAM_URL - expected properly formatted URL. Please note that domain part is required, http://localhost/ is not accepted but http://localhost.localdomain/ is ok.
228 define('PARAM_URL', 'url');
231 * 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!!
233 define('PARAM_USERNAME', 'username');
236 * PARAM_STRINGID - used to check if the given string is valid string identifier for get_string()
238 define('PARAM_STRINGID', 'stringid');
240 ///// DEPRECATED PARAM TYPES OR ALIASES - DO NOT USE FOR NEW CODE /////
242 * PARAM_CLEAN - obsoleted, please use a more specific type of parameter.
243 * It was one of the first types, that is why it is abused so much ;-)
244 * @deprecated since 2.0
246 define('PARAM_CLEAN', 'clean');
249 * PARAM_INTEGER - deprecated alias for PARAM_INT
251 define('PARAM_INTEGER', 'int');
254 * PARAM_NUMBER - deprecated alias of PARAM_FLOAT
256 define('PARAM_NUMBER', 'float');
259 * PARAM_ACTION - deprecated alias for PARAM_ALPHANUMEXT, use for various actions in forms and urls
260 * NOTE: originally alias for PARAM_APLHA
262 define('PARAM_ACTION', 'alphanumext');
265 * PARAM_FORMAT - deprecated alias for PARAM_ALPHANUMEXT, use for names of plugins, formats, etc.
266 * NOTE: originally alias for PARAM_APLHA
268 define('PARAM_FORMAT', 'alphanumext');
271 * PARAM_MULTILANG - deprecated alias of PARAM_TEXT.
273 define('PARAM_MULTILANG', 'text');
276 * PARAM_TIMEZONE - expected timezone. Timezone can be int +-(0-13) or float +-(0.5-12.5) or
277 * string seperated by '/' and can have '-' &/ '_' (eg. America/North_Dakota/New_Salem
278 * America/Port-au-Prince)
280 define('PARAM_TIMEZONE', 'timezone');
283 * PARAM_CLEANFILE - deprecated alias of PARAM_FILE; originally was removing regional chars too
285 define('PARAM_CLEANFILE', 'file');
288 * PARAM_COMPONENT is used for full component names (aka frankenstyle) such as 'mod_forum', 'core_rating', 'auth_ldap'.
289 * Short legacy subsystem names and module names are accepted too ex: 'forum', 'rating', 'user'.
290 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
291 * NOTE: numbers and underscores are strongly discouraged in plugin names!
293 define('PARAM_COMPONENT', 'component');
296 * PARAM_AREA is a name of area used when addressing files, comments, ratings, etc.
297 * It is usually used together with context id and component.
298 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
300 define('PARAM_AREA', 'area');
303 * PARAM_PLUGIN is used for plugin names such as 'forum', 'glossary', 'ldap', 'radius', 'paypal', 'completionstatus'.
304 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
305 * NOTE: numbers and underscores are strongly discouraged in plugin names! Underscores are forbidden in module names.
307 define('PARAM_PLUGIN', 'plugin');
310 /// Web Services ///
313 * VALUE_REQUIRED - if the parameter is not supplied, there is an error
315 define('VALUE_REQUIRED', 1);
318 * VALUE_OPTIONAL - if the parameter is not supplied, then the param has no value
320 define('VALUE_OPTIONAL', 2);
323 * VALUE_DEFAULT - if the parameter is not supplied, then the default value is used
325 define('VALUE_DEFAULT', 0);
328 * NULL_NOT_ALLOWED - the parameter can not be set to null in the database
330 define('NULL_NOT_ALLOWED', false);
333 * NULL_ALLOWED - the parameter can be set to null in the database
335 define('NULL_ALLOWED', true);
337 /// Page types ///
339 * PAGE_COURSE_VIEW is a definition of a page type. For more information on the page class see moodle/lib/pagelib.php.
341 define('PAGE_COURSE_VIEW', 'course-view');
343 /** Get remote addr constant */
344 define('GETREMOTEADDR_SKIP_HTTP_CLIENT_IP', '1');
345 /** Get remote addr constant */
346 define('GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR', '2');
348 /// Blog access level constant declaration ///
349 define ('BLOG_USER_LEVEL', 1);
350 define ('BLOG_GROUP_LEVEL', 2);
351 define ('BLOG_COURSE_LEVEL', 3);
352 define ('BLOG_SITE_LEVEL', 4);
353 define ('BLOG_GLOBAL_LEVEL', 5);
356 ///Tag constants///
358 * To prevent problems with multibytes strings,Flag updating in nav not working on the review page. this should not exceed the
359 * length of "varchar(255) / 3 (bytes / utf-8 character) = 85".
360 * TODO: this is not correct, varchar(255) are 255 unicode chars ;-)
362 * @todo define(TAG_MAX_LENGTH) this is not correct, varchar(255) are 255 unicode chars ;-)
364 define('TAG_MAX_LENGTH', 50);
366 /// Password policy constants ///
367 define ('PASSWORD_LOWER', 'abcdefghijklmnopqrstuvwxyz');
368 define ('PASSWORD_UPPER', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
369 define ('PASSWORD_DIGITS', '0123456789');
370 define ('PASSWORD_NONALPHANUM', '.,;:!?_-+/*@#&$');
372 /// Feature constants ///
373 // Used for plugin_supports() to report features that are, or are not, supported by a module.
375 /** True if module can provide a grade */
376 define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade');
377 /** True if module supports outcomes */
378 define('FEATURE_GRADE_OUTCOMES', 'outcomes');
379 /** True if module supports advanced grading methods */
380 define('FEATURE_ADVANCED_GRADING', 'grade_advanced_grading');
382 /** True if module has code to track whether somebody viewed it */
383 define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views');
384 /** True if module has custom completion rules */
385 define('FEATURE_COMPLETION_HAS_RULES', 'completion_has_rules');
387 /** True if module has no 'view' page (like label) */
388 define('FEATURE_NO_VIEW_LINK', 'viewlink');
389 /** True if module supports outcomes */
390 define('FEATURE_IDNUMBER', 'idnumber');
391 /** True if module supports groups */
392 define('FEATURE_GROUPS', 'groups');
393 /** True if module supports groupings */
394 define('FEATURE_GROUPINGS', 'groupings');
395 /** True if module supports groupmembersonly */
396 define('FEATURE_GROUPMEMBERSONLY', 'groupmembersonly');
398 /** Type of module */
399 define('FEATURE_MOD_ARCHETYPE', 'mod_archetype');
400 /** True if module supports intro editor */
401 define('FEATURE_MOD_INTRO', 'mod_intro');
402 /** True if module has default completion */
403 define('FEATURE_MODEDIT_DEFAULT_COMPLETION', 'modedit_default_completion');
405 define('FEATURE_COMMENT', 'comment');
407 define('FEATURE_RATE', 'rate');
408 /** True if module supports backup/restore of moodle2 format */
409 define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2');
411 /** True if module can show description on course main page */
412 define('FEATURE_SHOW_DESCRIPTION', 'showdescription');
414 /** Unspecified module archetype */
415 define('MOD_ARCHETYPE_OTHER', 0);
416 /** Resource-like type module */
417 define('MOD_ARCHETYPE_RESOURCE', 1);
418 /** Assignment module archetype */
419 define('MOD_ARCHETYPE_ASSIGNMENT', 2);
422 * Security token used for allowing access
423 * from external application such as web services.
424 * Scripts do not use any session, performance is relatively
425 * low because we need to load access info in each request.
426 * Scripts are executed in parallel.
428 define('EXTERNAL_TOKEN_PERMANENT', 0);
431 * Security token used for allowing access
432 * of embedded applications, the code is executed in the
433 * active user session. Token is invalidated after user logs out.
434 * Scripts are executed serially - normal session locking is used.
436 define('EXTERNAL_TOKEN_EMBEDDED', 1);
439 * The home page should be the site home
441 define('HOMEPAGE_SITE', 0);
443 * The home page should be the users my page
445 define('HOMEPAGE_MY', 1);
447 * The home page can be chosen by the user
449 define('HOMEPAGE_USER', 2);
452 * Hub directory url (should be moodle.org)
454 define('HUB_HUBDIRECTORYURL', "http://hubdirectory.moodle.org");
458 * Moodle.org url (should be moodle.org)
460 define('HUB_MOODLEORGHUBURL', "http://hub.moodle.org");
463 * Moodle mobile app service name
465 define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app');
467 /// PARAMETER HANDLING ////////////////////////////////////////////////////
470 * Returns a particular value for the named variable, taken from
471 * POST or GET. If the parameter doesn't exist then an error is
472 * thrown because we require this variable.
474 * This function should be used to initialise all required values
475 * in a script that are based on parameters. Usually it will be
476 * used like this:
477 * $id = required_param('id', PARAM_INT);
479 * Please note the $type parameter is now required and the value can not be array.
481 * @param string $parname the name of the page parameter we want
482 * @param string $type expected type of parameter
483 * @return mixed
485 function required_param($parname, $type) {
486 if (func_num_args() != 2 or empty($parname) or empty($type)) {
487 throw new coding_exception('required_param() requires $parname and $type to be specified (parameter: '.$parname.')');
489 if (isset($_POST[$parname])) { // POST has precedence
490 $param = $_POST[$parname];
491 } else if (isset($_GET[$parname])) {
492 $param = $_GET[$parname];
493 } else {
494 print_error('missingparam', '', '', $parname);
497 if (is_array($param)) {
498 debugging('Invalid array parameter detected in required_param(): '.$parname);
499 // TODO: switch to fatal error in Moodle 2.3
500 //print_error('missingparam', '', '', $parname);
501 return required_param_array($parname, $type);
504 return clean_param($param, $type);
508 * Returns a particular array 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 * $ids = required_param_array('ids', PARAM_INT);
517 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
519 * @param string $parname the name of the page parameter we want
520 * @param string $type expected type of parameter
521 * @return array
523 function required_param_array($parname, $type) {
524 if (func_num_args() != 2 or empty($parname) or empty($type)) {
525 throw new coding_exception('required_param_array() 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);
534 if (!is_array($param)) {
535 print_error('missingparam', '', '', $parname);
538 $result = array();
539 foreach($param as $key=>$value) {
540 if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
541 debugging('Invalid key name in required_param_array() detected: '.$key.', parameter: '.$parname);
542 continue;
544 $result[$key] = clean_param($value, $type);
547 return $result;
551 * Returns a particular value for the named variable, taken from
552 * POST or GET, otherwise returning a given default.
554 * This function should be used to initialise all optional values
555 * in a script that are based on parameters. Usually it will be
556 * used like this:
557 * $name = optional_param('name', 'Fred', PARAM_TEXT);
559 * Please note the $type parameter is now required and the value can not be array.
561 * @param string $parname the name of the page parameter we want
562 * @param mixed $default the default value to return if nothing is found
563 * @param string $type expected type of parameter
564 * @return mixed
566 function optional_param($parname, $default, $type) {
567 if (func_num_args() != 3 or empty($parname) or empty($type)) {
568 throw new coding_exception('optional_param() requires $parname, $default and $type to be specified (parameter: '.$parname.')');
570 if (!isset($default)) {
571 $default = null;
574 if (isset($_POST[$parname])) { // POST has precedence
575 $param = $_POST[$parname];
576 } else if (isset($_GET[$parname])) {
577 $param = $_GET[$parname];
578 } else {
579 return $default;
582 if (is_array($param)) {
583 debugging('Invalid array parameter detected in required_param(): '.$parname);
584 // TODO: switch to $default in Moodle 2.3
585 //return $default;
586 return optional_param_array($parname, $default, $type);
589 return clean_param($param, $type);
593 * Returns a particular array value for the named variable, taken from
594 * POST or GET, otherwise returning a given default.
596 * This function should be used to initialise all optional values
597 * in a script that are based on parameters. Usually it will be
598 * used like this:
599 * $ids = optional_param('id', array(), PARAM_INT);
601 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
603 * @param string $parname the name of the page parameter we want
604 * @param mixed $default the default value to return if nothing is found
605 * @param string $type expected type of parameter
606 * @return array
608 function optional_param_array($parname, $default, $type) {
609 if (func_num_args() != 3 or empty($parname) or empty($type)) {
610 throw new coding_exception('optional_param_array() requires $parname, $default and $type to be specified (parameter: '.$parname.')');
613 if (isset($_POST[$parname])) { // POST has precedence
614 $param = $_POST[$parname];
615 } else if (isset($_GET[$parname])) {
616 $param = $_GET[$parname];
617 } else {
618 return $default;
620 if (!is_array($param)) {
621 debugging('optional_param_array() expects array parameters only: '.$parname);
622 return $default;
625 $result = array();
626 foreach($param as $key=>$value) {
627 if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
628 debugging('Invalid key name in optional_param_array() detected: '.$key.', parameter: '.$parname);
629 continue;
631 $result[$key] = clean_param($value, $type);
634 return $result;
638 * Strict validation of parameter values, the values are only converted
639 * to requested PHP type. Internally it is using clean_param, the values
640 * before and after cleaning must be equal - otherwise
641 * an invalid_parameter_exception is thrown.
642 * Objects and classes are not accepted.
644 * @param mixed $param
645 * @param string $type PARAM_ constant
646 * @param bool $allownull are nulls valid value?
647 * @param string $debuginfo optional debug information
648 * @return mixed the $param value converted to PHP type or invalid_parameter_exception
650 function validate_param($param, $type, $allownull=NULL_NOT_ALLOWED, $debuginfo='') {
651 if (is_null($param)) {
652 if ($allownull == NULL_ALLOWED) {
653 return null;
654 } else {
655 throw new invalid_parameter_exception($debuginfo);
658 if (is_array($param) or is_object($param)) {
659 throw new invalid_parameter_exception($debuginfo);
662 $cleaned = clean_param($param, $type);
663 if ((string)$param !== (string)$cleaned) {
664 // conversion to string is usually lossless
665 throw new invalid_parameter_exception($debuginfo);
668 return $cleaned;
672 * Makes sure array contains only the allowed types,
673 * this function does not validate array key names!
674 * <code>
675 * $options = clean_param($options, PARAM_INT);
676 * </code>
678 * @param array $param the variable array we are cleaning
679 * @param string $type expected format of param after cleaning.
680 * @param bool $recursive clean recursive arrays
681 * @return array
683 function clean_param_array(array $param = null, $type, $recursive = false) {
684 $param = (array)$param; // convert null to empty array
685 foreach ($param as $key => $value) {
686 if (is_array($value)) {
687 if ($recursive) {
688 $param[$key] = clean_param_array($value, $type, true);
689 } else {
690 throw new coding_exception('clean_param_array() can not process multidimensional arrays when $recursive is false.');
692 } else {
693 $param[$key] = clean_param($value, $type);
696 return $param;
700 * Used by {@link optional_param()} and {@link required_param()} to
701 * clean the variables and/or cast to specific types, based on
702 * an options field.
703 * <code>
704 * $course->format = clean_param($course->format, PARAM_ALPHA);
705 * $selectedgrade_item = clean_param($selectedgrade_item, PARAM_INT);
706 * </code>
708 * @param mixed $param the variable we are cleaning
709 * @param string $type expected format of param after cleaning.
710 * @return mixed
712 function clean_param($param, $type) {
714 global $CFG;
716 if (is_array($param)) {
717 throw new coding_exception('clean_param() can not process arrays, please use clean_param_array() instead.');
718 } else if (is_object($param)) {
719 if (method_exists($param, '__toString')) {
720 $param = $param->__toString();
721 } else {
722 throw new coding_exception('clean_param() can not process objects, please use clean_param_array() instead.');
726 switch ($type) {
727 case PARAM_RAW: // no cleaning at all
728 $param = fix_utf8($param);
729 return $param;
731 case PARAM_RAW_TRIMMED: // no cleaning, but strip leading and trailing whitespace.
732 $param = fix_utf8($param);
733 return trim($param);
735 case PARAM_CLEAN: // General HTML cleaning, try to use more specific type if possible
736 // this is deprecated!, please use more specific type instead
737 if (is_numeric($param)) {
738 return $param;
740 $param = fix_utf8($param);
741 return clean_text($param); // Sweep for scripts, etc
743 case PARAM_CLEANHTML: // clean html fragment
744 $param = fix_utf8($param);
745 $param = clean_text($param, FORMAT_HTML); // Sweep for scripts, etc
746 return trim($param);
748 case PARAM_INT:
749 return (int)$param; // Convert to integer
751 case PARAM_FLOAT:
752 case PARAM_NUMBER:
753 return (float)$param; // Convert to float
755 case PARAM_ALPHA: // Remove everything not a-z
756 return preg_replace('/[^a-zA-Z]/i', '', $param);
758 case PARAM_ALPHAEXT: // Remove everything not a-zA-Z_- (originally allowed "/" too)
759 return preg_replace('/[^a-zA-Z_-]/i', '', $param);
761 case PARAM_ALPHANUM: // Remove everything not a-zA-Z0-9
762 return preg_replace('/[^A-Za-z0-9]/i', '', $param);
764 case PARAM_ALPHANUMEXT: // Remove everything not a-zA-Z0-9_-
765 return preg_replace('/[^A-Za-z0-9_-]/i', '', $param);
767 case PARAM_SEQUENCE: // Remove everything not 0-9,
768 return preg_replace('/[^0-9,]/i', '', $param);
770 case PARAM_BOOL: // Convert to 1 or 0
771 $tempstr = strtolower($param);
772 if ($tempstr === 'on' or $tempstr === 'yes' or $tempstr === 'true') {
773 $param = 1;
774 } else if ($tempstr === 'off' or $tempstr === 'no' or $tempstr === 'false') {
775 $param = 0;
776 } else {
777 $param = empty($param) ? 0 : 1;
779 return $param;
781 case PARAM_NOTAGS: // Strip all tags
782 $param = fix_utf8($param);
783 return strip_tags($param);
785 case PARAM_TEXT: // leave only tags needed for multilang
786 $param = fix_utf8($param);
787 // if the multilang syntax is not correct we strip all tags
788 // because it would break xhtml strict which is required for accessibility standards
789 // please note this cleaning does not strip unbalanced '>' for BC compatibility reasons
790 do {
791 if (strpos($param, '</lang>') !== false) {
792 // old and future mutilang syntax
793 $param = strip_tags($param, '<lang>');
794 if (!preg_match_all('/<.*>/suU', $param, $matches)) {
795 break;
797 $open = false;
798 foreach ($matches[0] as $match) {
799 if ($match === '</lang>') {
800 if ($open) {
801 $open = false;
802 continue;
803 } else {
804 break 2;
807 if (!preg_match('/^<lang lang="[a-zA-Z0-9_-]+"\s*>$/u', $match)) {
808 break 2;
809 } else {
810 $open = true;
813 if ($open) {
814 break;
816 return $param;
818 } else if (strpos($param, '</span>') !== false) {
819 // current problematic multilang syntax
820 $param = strip_tags($param, '<span>');
821 if (!preg_match_all('/<.*>/suU', $param, $matches)) {
822 break;
824 $open = false;
825 foreach ($matches[0] as $match) {
826 if ($match === '</span>') {
827 if ($open) {
828 $open = false;
829 continue;
830 } else {
831 break 2;
834 if (!preg_match('/^<span(\s+lang="[a-zA-Z0-9_-]+"|\s+class="multilang"){2}\s*>$/u', $match)) {
835 break 2;
836 } else {
837 $open = true;
840 if ($open) {
841 break;
843 return $param;
845 } while (false);
846 // easy, just strip all tags, if we ever want to fix orphaned '&' we have to do that in format_string()
847 return strip_tags($param);
849 case PARAM_COMPONENT:
850 // we do not want any guessing here, either the name is correct or not
851 // please note only normalised component names are accepted
852 if (!preg_match('/^[a-z]+(_[a-z][a-z0-9_]*)?[a-z0-9]$/', $param)) {
853 return '';
855 if (strpos($param, '__') !== false) {
856 return '';
858 if (strpos($param, 'mod_') === 0) {
859 // module names must not contain underscores because we need to differentiate them from invalid plugin types
860 if (substr_count($param, '_') != 1) {
861 return '';
864 return $param;
866 case PARAM_PLUGIN:
867 case PARAM_AREA:
868 // we do not want any guessing here, either the name is correct or not
869 if (!preg_match('/^[a-z][a-z0-9_]*[a-z0-9]$/', $param)) {
870 return '';
872 if (strpos($param, '__') !== false) {
873 return '';
875 return $param;
877 case PARAM_SAFEDIR: // Remove everything not a-zA-Z0-9_-
878 return preg_replace('/[^a-zA-Z0-9_-]/i', '', $param);
880 case PARAM_SAFEPATH: // Remove everything not a-zA-Z0-9/_-
881 return preg_replace('/[^a-zA-Z0-9\/_-]/i', '', $param);
883 case PARAM_FILE: // Strip all suspicious characters from filename
884 $param = fix_utf8($param);
885 $param = preg_replace('~[[:cntrl:]]|[&<>"`\|\':\\\\/]~u', '', $param);
886 $param = preg_replace('~\.\.+~', '', $param);
887 if ($param === '.') {
888 $param = '';
890 return $param;
892 case PARAM_PATH: // Strip all suspicious characters from file path
893 $param = fix_utf8($param);
894 $param = str_replace('\\', '/', $param);
895 $param = preg_replace('~[[:cntrl:]]|[&<>"`\|\':]~u', '', $param);
896 $param = preg_replace('~\.\.+~', '', $param);
897 $param = preg_replace('~//+~', '/', $param);
898 return preg_replace('~/(\./)+~', '/', $param);
900 case PARAM_HOST: // allow FQDN or IPv4 dotted quad
901 $param = preg_replace('/[^\.\d\w-]/','', $param ); // only allowed chars
902 // match ipv4 dotted quad
903 if (preg_match('/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/',$param, $match)){
904 // confirm values are ok
905 if ( $match[0] > 255
906 || $match[1] > 255
907 || $match[3] > 255
908 || $match[4] > 255 ) {
909 // hmmm, what kind of dotted quad is this?
910 $param = '';
912 } elseif ( preg_match('/^[\w\d\.-]+$/', $param) // dots, hyphens, numbers
913 && !preg_match('/^[\.-]/', $param) // no leading dots/hyphens
914 && !preg_match('/[\.-]$/', $param) // no trailing dots/hyphens
916 // all is ok - $param is respected
917 } else {
918 // all is not ok...
919 $param='';
921 return $param;
923 case PARAM_URL: // allow safe ftp, http, mailto urls
924 $param = fix_utf8($param);
925 include_once($CFG->dirroot . '/lib/validateurlsyntax.php');
926 if (!empty($param) && validateUrlSyntax($param, 's?H?S?F?E?u-P-a?I?p?f?q?r?')) {
927 // all is ok, param is respected
928 } else {
929 $param =''; // not really ok
931 return $param;
933 case PARAM_LOCALURL: // allow http absolute, root relative and relative URLs within wwwroot
934 $param = clean_param($param, PARAM_URL);
935 if (!empty($param)) {
936 if (preg_match(':^/:', $param)) {
937 // root-relative, ok!
938 } elseif (preg_match('/^'.preg_quote($CFG->wwwroot, '/').'/i',$param)) {
939 // absolute, and matches our wwwroot
940 } else {
941 // relative - let's make sure there are no tricks
942 if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?')) {
943 // looks ok.
944 } else {
945 $param = '';
949 return $param;
951 case PARAM_PEM:
952 $param = trim($param);
953 // PEM formatted strings may contain letters/numbers and the symbols
954 // forward slash: /
955 // plus sign: +
956 // equal sign: =
957 // , surrounded by BEGIN and END CERTIFICATE prefix and suffixes
958 if (preg_match('/^-----BEGIN CERTIFICATE-----([\s\w\/\+=]+)-----END CERTIFICATE-----$/', trim($param), $matches)) {
959 list($wholething, $body) = $matches;
960 unset($wholething, $matches);
961 $b64 = clean_param($body, PARAM_BASE64);
962 if (!empty($b64)) {
963 return "-----BEGIN CERTIFICATE-----\n$b64\n-----END CERTIFICATE-----\n";
964 } else {
965 return '';
968 return '';
970 case PARAM_BASE64:
971 if (!empty($param)) {
972 // PEM formatted strings may contain letters/numbers and the symbols
973 // forward slash: /
974 // plus sign: +
975 // equal sign: =
976 if (0 >= preg_match('/^([\s\w\/\+=]+)$/', trim($param))) {
977 return '';
979 $lines = preg_split('/[\s]+/', $param, -1, PREG_SPLIT_NO_EMPTY);
980 // Each line of base64 encoded data must be 64 characters in
981 // length, except for the last line which may be less than (or
982 // equal to) 64 characters long.
983 for ($i=0, $j=count($lines); $i < $j; $i++) {
984 if ($i + 1 == $j) {
985 if (64 < strlen($lines[$i])) {
986 return '';
988 continue;
991 if (64 != strlen($lines[$i])) {
992 return '';
995 return implode("\n",$lines);
996 } else {
997 return '';
1000 case PARAM_TAG:
1001 $param = fix_utf8($param);
1002 // Please note it is not safe to use the tag name directly anywhere,
1003 // it must be processed with s(), urlencode() before embedding anywhere.
1004 // remove some nasties
1005 $param = preg_replace('~[[:cntrl:]]|[<>`]~u', '', $param);
1006 //convert many whitespace chars into one
1007 $param = preg_replace('/\s+/', ' ', $param);
1008 $textlib = textlib_get_instance();
1009 $param = $textlib->substr(trim($param), 0, TAG_MAX_LENGTH);
1010 return $param;
1012 case PARAM_TAGLIST:
1013 $param = fix_utf8($param);
1014 $tags = explode(',', $param);
1015 $result = array();
1016 foreach ($tags as $tag) {
1017 $res = clean_param($tag, PARAM_TAG);
1018 if ($res !== '') {
1019 $result[] = $res;
1022 if ($result) {
1023 return implode(',', $result);
1024 } else {
1025 return '';
1028 case PARAM_CAPABILITY:
1029 if (get_capability_info($param)) {
1030 return $param;
1031 } else {
1032 return '';
1035 case PARAM_PERMISSION:
1036 $param = (int)$param;
1037 if (in_array($param, array(CAP_INHERIT, CAP_ALLOW, CAP_PREVENT, CAP_PROHIBIT))) {
1038 return $param;
1039 } else {
1040 return CAP_INHERIT;
1043 case PARAM_AUTH:
1044 $param = clean_param($param, PARAM_PLUGIN);
1045 if (empty($param)) {
1046 return '';
1047 } else if (exists_auth_plugin($param)) {
1048 return $param;
1049 } else {
1050 return '';
1053 case PARAM_LANG:
1054 $param = clean_param($param, PARAM_SAFEDIR);
1055 if (get_string_manager()->translation_exists($param)) {
1056 return $param;
1057 } else {
1058 return ''; // Specified language is not installed or param malformed
1061 case PARAM_THEME:
1062 $param = clean_param($param, PARAM_PLUGIN);
1063 if (empty($param)) {
1064 return '';
1065 } else if (file_exists("$CFG->dirroot/theme/$param/config.php")) {
1066 return $param;
1067 } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$param/config.php")) {
1068 return $param;
1069 } else {
1070 return ''; // Specified theme is not installed
1073 case PARAM_USERNAME:
1074 $param = fix_utf8($param);
1075 $param = str_replace(" " , "", $param);
1076 $param = moodle_strtolower($param); // Convert uppercase to lowercase MDL-16919
1077 if (empty($CFG->extendedusernamechars)) {
1078 // regular expression, eliminate all chars EXCEPT:
1079 // alphanum, dash (-), underscore (_), at sign (@) and period (.) characters.
1080 $param = preg_replace('/[^-\.@_a-z0-9]/', '', $param);
1082 return $param;
1084 case PARAM_EMAIL:
1085 $param = fix_utf8($param);
1086 if (validate_email($param)) {
1087 return $param;
1088 } else {
1089 return '';
1092 case PARAM_STRINGID:
1093 if (preg_match('|^[a-zA-Z][a-zA-Z0-9\.:/_-]*$|', $param)) {
1094 return $param;
1095 } else {
1096 return '';
1099 case PARAM_TIMEZONE: //can be int, float(with .5 or .0) or string seperated by '/' and can have '-_'
1100 $param = fix_utf8($param);
1101 $timezonepattern = '/^(([+-]?(0?[0-9](\.[5|0])?|1[0-3]|1[0-2]\.5))|(99)|[[:alnum:]]+(\/?[[:alpha:]_-])+)$/';
1102 if (preg_match($timezonepattern, $param)) {
1103 return $param;
1104 } else {
1105 return '';
1108 default: // throw error, switched parameters in optional_param or another serious problem
1109 print_error("unknownparamtype", '', '', $type);
1114 * Makes sure the data is using valid utf8, invalid characters are discarded.
1116 * Note: this function is not intended for full objects with methods and private properties.
1118 * @param mixed $value
1119 * @return mixed with proper utf-8 encoding
1121 function fix_utf8($value) {
1122 if (is_null($value) or $value === '') {
1123 return $value;
1125 } else if (is_string($value)) {
1126 if ((string)(int)$value === $value) {
1127 // shortcut
1128 return $value;
1130 return iconv('UTF-8', 'UTF-8//IGNORE', $value);
1132 } else if (is_array($value)) {
1133 foreach ($value as $k=>$v) {
1134 $value[$k] = fix_utf8($v);
1136 return $value;
1138 } else if (is_object($value)) {
1139 $value = clone($value); // do not modify original
1140 foreach ($value as $k=>$v) {
1141 $value->$k = fix_utf8($v);
1143 return $value;
1145 } else {
1146 // this is some other type, no utf-8 here
1147 return $value;
1152 * Return true if given value is integer or string with integer value
1154 * @param mixed $value String or Int
1155 * @return bool true if number, false if not
1157 function is_number($value) {
1158 if (is_int($value)) {
1159 return true;
1160 } else if (is_string($value)) {
1161 return ((string)(int)$value) === $value;
1162 } else {
1163 return false;
1168 * Returns host part from url
1169 * @param string $url full url
1170 * @return string host, null if not found
1172 function get_host_from_url($url) {
1173 preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches);
1174 if ($matches) {
1175 return $matches[1];
1177 return null;
1181 * Tests whether anything was returned by text editor
1183 * This function is useful for testing whether something you got back from
1184 * the HTML editor actually contains anything. Sometimes the HTML editor
1185 * appear to be empty, but actually you get back a <br> tag or something.
1187 * @param string $string a string containing HTML.
1188 * @return boolean does the string contain any actual content - that is text,
1189 * images, objects, etc.
1191 function html_is_blank($string) {
1192 return trim(strip_tags($string, '<img><object><applet><input><select><textarea><hr>')) == '';
1196 * Set a key in global configuration
1198 * Set a key/value pair in both this session's {@link $CFG} global variable
1199 * and in the 'config' database table for future sessions.
1201 * Can also be used to update keys for plugin-scoped configs in config_plugin table.
1202 * In that case it doesn't affect $CFG.
1204 * A NULL value will delete the entry.
1206 * @global object
1207 * @global object
1208 * @param string $name the key to set
1209 * @param string $value the value to set (without magic quotes)
1210 * @param string $plugin (optional) the plugin scope, default NULL
1211 * @return bool true or exception
1213 function set_config($name, $value, $plugin=NULL) {
1214 global $CFG, $DB;
1216 if (empty($plugin)) {
1217 if (!array_key_exists($name, $CFG->config_php_settings)) {
1218 // So it's defined for this invocation at least
1219 if (is_null($value)) {
1220 unset($CFG->$name);
1221 } else {
1222 $CFG->$name = (string)$value; // settings from db are always strings
1226 if ($DB->get_field('config', 'name', array('name'=>$name))) {
1227 if ($value === null) {
1228 $DB->delete_records('config', array('name'=>$name));
1229 } else {
1230 $DB->set_field('config', 'value', $value, array('name'=>$name));
1232 } else {
1233 if ($value !== null) {
1234 $config = new stdClass();
1235 $config->name = $name;
1236 $config->value = $value;
1237 $DB->insert_record('config', $config, false);
1241 } else { // plugin scope
1242 if ($id = $DB->get_field('config_plugins', 'id', array('name'=>$name, 'plugin'=>$plugin))) {
1243 if ($value===null) {
1244 $DB->delete_records('config_plugins', array('name'=>$name, 'plugin'=>$plugin));
1245 } else {
1246 $DB->set_field('config_plugins', 'value', $value, array('id'=>$id));
1248 } else {
1249 if ($value !== null) {
1250 $config = new stdClass();
1251 $config->plugin = $plugin;
1252 $config->name = $name;
1253 $config->value = $value;
1254 $DB->insert_record('config_plugins', $config, false);
1259 return true;
1263 * Get configuration values from the global config table
1264 * or the config_plugins table.
1266 * If called with one parameter, it will load all the config
1267 * variables for one plugin, and return them as an object.
1269 * If called with 2 parameters it will return a string single
1270 * value or false if the value is not found.
1272 * @param string $plugin full component name
1273 * @param string $name default NULL
1274 * @return mixed hash-like object or single value, return false no config found
1276 function get_config($plugin, $name = NULL) {
1277 global $CFG, $DB;
1279 // normalise component name
1280 if ($plugin === 'moodle' or $plugin === 'core') {
1281 $plugin = NULL;
1284 if (!empty($name)) { // the user is asking for a specific value
1285 if (!empty($plugin)) {
1286 if (isset($CFG->forced_plugin_settings[$plugin]) and array_key_exists($name, $CFG->forced_plugin_settings[$plugin])) {
1287 // setting forced in config file
1288 return $CFG->forced_plugin_settings[$plugin][$name];
1289 } else {
1290 return $DB->get_field('config_plugins', 'value', array('plugin'=>$plugin, 'name'=>$name));
1292 } else {
1293 if (array_key_exists($name, $CFG->config_php_settings)) {
1294 // setting force in config file
1295 return $CFG->config_php_settings[$name];
1296 } else {
1297 return $DB->get_field('config', 'value', array('name'=>$name));
1302 // the user is after a recordset
1303 if ($plugin) {
1304 $localcfg = $DB->get_records_menu('config_plugins', array('plugin'=>$plugin), '', 'name,value');
1305 if (isset($CFG->forced_plugin_settings[$plugin])) {
1306 foreach($CFG->forced_plugin_settings[$plugin] as $n=>$v) {
1307 if (is_null($v) or is_array($v) or is_object($v)) {
1308 // we do not want any extra mess here, just real settings that could be saved in db
1309 unset($localcfg[$n]);
1310 } else {
1311 //convert to string as if it went through the DB
1312 $localcfg[$n] = (string)$v;
1316 if ($localcfg) {
1317 return (object)$localcfg;
1318 } else {
1319 return null;
1322 } else {
1323 // this part is not really used any more, but anyway...
1324 $localcfg = $DB->get_records_menu('config', array(), '', 'name,value');
1325 foreach($CFG->config_php_settings as $n=>$v) {
1326 if (is_null($v) or is_array($v) or is_object($v)) {
1327 // we do not want any extra mess here, just real settings that could be saved in db
1328 unset($localcfg[$n]);
1329 } else {
1330 //convert to string as if it went through the DB
1331 $localcfg[$n] = (string)$v;
1334 return (object)$localcfg;
1339 * Removes a key from global configuration
1341 * @param string $name the key to set
1342 * @param string $plugin (optional) the plugin scope
1343 * @global object
1344 * @return boolean whether the operation succeeded.
1346 function unset_config($name, $plugin=NULL) {
1347 global $CFG, $DB;
1349 if (empty($plugin)) {
1350 unset($CFG->$name);
1351 $DB->delete_records('config', array('name'=>$name));
1352 } else {
1353 $DB->delete_records('config_plugins', array('name'=>$name, 'plugin'=>$plugin));
1356 return true;
1360 * Remove all the config variables for a given plugin.
1362 * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice';
1363 * @return boolean whether the operation succeeded.
1365 function unset_all_config_for_plugin($plugin) {
1366 global $DB;
1367 $DB->delete_records('config_plugins', array('plugin' => $plugin));
1368 $like = $DB->sql_like('name', '?', true, true, false, '|');
1369 $params = array($DB->sql_like_escape($plugin.'_', '|') . '%');
1370 $DB->delete_records_select('config', $like, $params);
1371 return true;
1375 * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability.
1377 * All users are verified if they still have the necessary capability.
1379 * @param string $value the value of the config setting.
1380 * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor.
1381 * @param bool $include admins, include administrators
1382 * @return array of user objects.
1384 function get_users_from_config($value, $capability, $includeadmins = true) {
1385 global $CFG, $DB;
1387 if (empty($value) or $value === '$@NONE@$') {
1388 return array();
1391 // we have to make sure that users still have the necessary capability,
1392 // it should be faster to fetch them all first and then test if they are present
1393 // instead of validating them one-by-one
1394 $users = get_users_by_capability(get_context_instance(CONTEXT_SYSTEM), $capability);
1395 if ($includeadmins) {
1396 $admins = get_admins();
1397 foreach ($admins as $admin) {
1398 $users[$admin->id] = $admin;
1402 if ($value === '$@ALL@$') {
1403 return $users;
1406 $result = array(); // result in correct order
1407 $allowed = explode(',', $value);
1408 foreach ($allowed as $uid) {
1409 if (isset($users[$uid])) {
1410 $user = $users[$uid];
1411 $result[$user->id] = $user;
1415 return $result;
1420 * Invalidates browser caches and cached data in temp
1421 * @return void
1423 function purge_all_caches() {
1424 global $CFG;
1426 reset_text_filters_cache();
1427 js_reset_all_caches();
1428 theme_reset_all_caches();
1429 get_string_manager()->reset_caches();
1431 // purge all other caches: rss, simplepie, etc.
1432 remove_dir($CFG->cachedir.'', true);
1434 // make sure cache dir is writable, throws exception if not
1435 make_cache_directory('');
1437 // hack: this script may get called after the purifier was initialised,
1438 // but we do not want to verify repeatedly this exists in each call
1439 make_cache_directory('htmlpurifier');
1443 * Get volatile flags
1445 * @param string $type
1446 * @param int $changedsince default null
1447 * @return records array
1449 function get_cache_flags($type, $changedsince=NULL) {
1450 global $DB;
1452 $params = array('type'=>$type, 'expiry'=>time());
1453 $sqlwhere = "flagtype = :type AND expiry >= :expiry";
1454 if ($changedsince !== NULL) {
1455 $params['changedsince'] = $changedsince;
1456 $sqlwhere .= " AND timemodified > :changedsince";
1458 $cf = array();
1460 if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) {
1461 foreach ($flags as $flag) {
1462 $cf[$flag->name] = $flag->value;
1465 return $cf;
1469 * Get volatile flags
1471 * @param string $type
1472 * @param string $name
1473 * @param int $changedsince default null
1474 * @return records array
1476 function get_cache_flag($type, $name, $changedsince=NULL) {
1477 global $DB;
1479 $params = array('type'=>$type, 'name'=>$name, 'expiry'=>time());
1481 $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry";
1482 if ($changedsince !== NULL) {
1483 $params['changedsince'] = $changedsince;
1484 $sqlwhere .= " AND timemodified > :changedsince";
1487 return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params);
1491 * Set a volatile flag
1493 * @param string $type the "type" namespace for the key
1494 * @param string $name the key to set
1495 * @param string $value the value to set (without magic quotes) - NULL will remove the flag
1496 * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs
1497 * @return bool Always returns true
1499 function set_cache_flag($type, $name, $value, $expiry=NULL) {
1500 global $DB;
1502 $timemodified = time();
1503 if ($expiry===NULL || $expiry < $timemodified) {
1504 $expiry = $timemodified + 24 * 60 * 60;
1505 } else {
1506 $expiry = (int)$expiry;
1509 if ($value === NULL) {
1510 unset_cache_flag($type,$name);
1511 return true;
1514 if ($f = $DB->get_record('cache_flags', array('name'=>$name, 'flagtype'=>$type), '*', IGNORE_MULTIPLE)) { // this is a potential problem in DEBUG_DEVELOPER
1515 if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) {
1516 return true; //no need to update; helps rcache too
1518 $f->value = $value;
1519 $f->expiry = $expiry;
1520 $f->timemodified = $timemodified;
1521 $DB->update_record('cache_flags', $f);
1522 } else {
1523 $f = new stdClass();
1524 $f->flagtype = $type;
1525 $f->name = $name;
1526 $f->value = $value;
1527 $f->expiry = $expiry;
1528 $f->timemodified = $timemodified;
1529 $DB->insert_record('cache_flags', $f);
1531 return true;
1535 * Removes a single volatile flag
1537 * @global object
1538 * @param string $type the "type" namespace for the key
1539 * @param string $name the key to set
1540 * @return bool
1542 function unset_cache_flag($type, $name) {
1543 global $DB;
1544 $DB->delete_records('cache_flags', array('name'=>$name, 'flagtype'=>$type));
1545 return true;
1549 * Garbage-collect volatile flags
1551 * @return bool Always returns true
1553 function gc_cache_flags() {
1554 global $DB;
1555 $DB->delete_records_select('cache_flags', 'expiry < ?', array(time()));
1556 return true;
1559 /// FUNCTIONS FOR HANDLING USER PREFERENCES ////////////////////////////////////
1562 * Refresh user preference cache. This is used most often for $USER
1563 * object that is stored in session, but it also helps with performance in cron script.
1565 * Preferences for each user are loaded on first use on every page, then again after the timeout expires.
1567 * @param stdClass $user user object, preferences are preloaded into ->preference property
1568 * @param int $cachelifetime cache life time on the current page (ins seconds)
1569 * @return void
1571 function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120) {
1572 global $DB;
1573 static $loadedusers = array(); // Static cache, we need to check on each page load, not only every 2 minutes.
1575 if (!isset($user->id)) {
1576 throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field');
1579 if (empty($user->id) or isguestuser($user->id)) {
1580 // No permanent storage for not-logged-in users and guest
1581 if (!isset($user->preference)) {
1582 $user->preference = array();
1584 return;
1587 $timenow = time();
1589 if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) {
1590 // Already loaded at least once on this page. Are we up to date?
1591 if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) {
1592 // no need to reload - we are on the same page and we loaded prefs just a moment ago
1593 return;
1595 } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) {
1596 // no change since the lastcheck on this page
1597 $user->preference['_lastloaded'] = $timenow;
1598 return;
1602 // OK, so we have to reload all preferences
1603 $loadedusers[$user->id] = true;
1604 $user->preference = $DB->get_records_menu('user_preferences', array('userid'=>$user->id), '', 'name,value'); // All values
1605 $user->preference['_lastloaded'] = $timenow;
1609 * Called from set/delete_user_preferences, so that the prefs can
1610 * be correctly reloaded in different sessions.
1612 * NOTE: internal function, do not call from other code.
1614 * @param integer $userid the user whose prefs were changed.
1615 * @return void
1617 function mark_user_preferences_changed($userid) {
1618 global $CFG;
1620 if (empty($userid) or isguestuser($userid)) {
1621 // no cache flags for guest and not-logged-in users
1622 return;
1625 set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout);
1629 * Sets a preference for the specified user.
1631 * If user object submitted, 'preference' property contains the preferences cache.
1633 * @param string $name The key to set as preference for the specified user
1634 * @param string $value The value to set for the $name key in the specified user's record,
1635 * null means delete current value
1636 * @param stdClass|int $user A moodle user object or id, null means current user
1637 * @return bool always true or exception
1639 function set_user_preference($name, $value, $user = null) {
1640 global $USER, $DB;
1642 if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1643 throw new coding_exception('Invalid preference name in set_user_preference() call');
1646 if (is_null($value)) {
1647 // null means delete current
1648 return unset_user_preference($name, $user);
1649 } else if (is_object($value)) {
1650 throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed');
1651 } else if (is_array($value)) {
1652 throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed');
1654 $value = (string)$value;
1655 if (textlib::strlen($value) > 1333) { //value column maximum length is 1333 characters
1656 throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column');
1659 if (is_null($user)) {
1660 $user = $USER;
1661 } else if (isset($user->id)) {
1662 // $user is valid object
1663 } else if (is_numeric($user)) {
1664 $user = (object)array('id'=>(int)$user);
1665 } else {
1666 throw new coding_exception('Invalid $user parameter in set_user_preference() call');
1669 check_user_preferences_loaded($user);
1671 if (empty($user->id) or isguestuser($user->id)) {
1672 // no permanent storage for not-logged-in users and guest
1673 $user->preference[$name] = $value;
1674 return true;
1677 if ($preference = $DB->get_record('user_preferences', array('userid'=>$user->id, 'name'=>$name))) {
1678 if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) {
1679 // preference already set to this value
1680 return true;
1682 $DB->set_field('user_preferences', 'value', $value, array('id'=>$preference->id));
1684 } else {
1685 $preference = new stdClass();
1686 $preference->userid = $user->id;
1687 $preference->name = $name;
1688 $preference->value = $value;
1689 $DB->insert_record('user_preferences', $preference);
1692 // update value in cache
1693 $user->preference[$name] = $value;
1695 // set reload flag for other sessions
1696 mark_user_preferences_changed($user->id);
1698 return true;
1702 * Sets a whole array of preferences for the current user
1704 * If user object submitted, 'preference' property contains the preferences cache.
1706 * @param array $prefarray An array of key/value pairs to be set
1707 * @param stdClass|int $user A moodle user object or id, null means current user
1708 * @return bool always true or exception
1710 function set_user_preferences(array $prefarray, $user = null) {
1711 foreach ($prefarray as $name => $value) {
1712 set_user_preference($name, $value, $user);
1714 return true;
1718 * Unsets a preference completely by deleting it from the database
1720 * If user object submitted, 'preference' property contains the preferences cache.
1722 * @param string $name The key to unset as preference for the specified user
1723 * @param stdClass|int $user A moodle user object or id, null means current user
1724 * @return bool always true or exception
1726 function unset_user_preference($name, $user = null) {
1727 global $USER, $DB;
1729 if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1730 throw new coding_exception('Invalid preference name in unset_user_preference() call');
1733 if (is_null($user)) {
1734 $user = $USER;
1735 } else if (isset($user->id)) {
1736 // $user is valid object
1737 } else if (is_numeric($user)) {
1738 $user = (object)array('id'=>(int)$user);
1739 } else {
1740 throw new coding_exception('Invalid $user parameter in unset_user_preference() call');
1743 check_user_preferences_loaded($user);
1745 if (empty($user->id) or isguestuser($user->id)) {
1746 // no permanent storage for not-logged-in user and guest
1747 unset($user->preference[$name]);
1748 return true;
1751 // delete from DB
1752 $DB->delete_records('user_preferences', array('userid'=>$user->id, 'name'=>$name));
1754 // delete the preference from cache
1755 unset($user->preference[$name]);
1757 // set reload flag for other sessions
1758 mark_user_preferences_changed($user->id);
1760 return true;
1764 * Used to fetch user preference(s)
1766 * If no arguments are supplied this function will return
1767 * all of the current user preferences as an array.
1769 * If a name is specified then this function
1770 * attempts to return that particular preference value. If
1771 * none is found, then the optional value $default is returned,
1772 * otherwise NULL.
1774 * If user object submitted, 'preference' property contains the preferences cache.
1776 * @param string $name Name of the key to use in finding a preference value
1777 * @param mixed $default Value to be returned if the $name key is not set in the user preferences
1778 * @param stdClass|int $user A moodle user object or id, null means current user
1779 * @return mixed string value or default
1781 function get_user_preferences($name = null, $default = null, $user = null) {
1782 global $USER;
1784 if (is_null($name)) {
1785 // all prefs
1786 } else if (is_numeric($name) or $name === '_lastloaded') {
1787 throw new coding_exception('Invalid preference name in get_user_preferences() call');
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 get_user_preferences() call');
1800 check_user_preferences_loaded($user);
1802 if (empty($name)) {
1803 return $user->preference; // All values
1804 } else if (isset($user->preference[$name])) {
1805 return $user->preference[$name]; // The single string value
1806 } else {
1807 return $default; // Default value (null if not specified)
1811 /// FUNCTIONS FOR HANDLING TIME ////////////////////////////////////////////
1814 * Given date parts in user time produce a GMT timestamp.
1816 * @todo Finish documenting this function
1817 * @param int $year The year part to create timestamp of
1818 * @param int $month The month part to create timestamp of
1819 * @param int $day The day part to create timestamp of
1820 * @param int $hour The hour part to create timestamp of
1821 * @param int $minute The minute part to create timestamp of
1822 * @param int $second The second part to create timestamp of
1823 * @param mixed $timezone Timezone modifier, if 99 then use default user's timezone
1824 * @param bool $applydst Toggle Daylight Saving Time, default true, will be
1825 * applied only if timezone is 99 or string.
1826 * @return int timestamp
1828 function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) {
1830 //save input timezone, required for dst offset check.
1831 $passedtimezone = $timezone;
1833 $timezone = get_user_timezone_offset($timezone);
1835 if (abs($timezone) > 13) { //server time
1836 $time = mktime((int)$hour, (int)$minute, (int)$second, (int)$month, (int)$day, (int)$year);
1837 } else {
1838 $time = gmmktime((int)$hour, (int)$minute, (int)$second, (int)$month, (int)$day, (int)$year);
1839 $time = usertime($time, $timezone);
1841 //Apply dst for string timezones or if 99 then try dst offset with user's default timezone
1842 if ($applydst && ((99 == $passedtimezone) || !is_numeric($passedtimezone))) {
1843 $time -= dst_offset_on($time, $passedtimezone);
1847 return $time;
1852 * Format a date/time (seconds) as weeks, days, hours etc as needed
1854 * Given an amount of time in seconds, returns string
1855 * formatted nicely as weeks, days, hours etc as needed
1857 * @uses MINSECS
1858 * @uses HOURSECS
1859 * @uses DAYSECS
1860 * @uses YEARSECS
1861 * @param int $totalsecs Time in seconds
1862 * @param object $str Should be a time object
1863 * @return string A nicely formatted date/time string
1865 function format_time($totalsecs, $str=NULL) {
1867 $totalsecs = abs($totalsecs);
1869 if (!$str) { // Create the str structure the slow way
1870 $str = new stdClass();
1871 $str->day = get_string('day');
1872 $str->days = get_string('days');
1873 $str->hour = get_string('hour');
1874 $str->hours = get_string('hours');
1875 $str->min = get_string('min');
1876 $str->mins = get_string('mins');
1877 $str->sec = get_string('sec');
1878 $str->secs = get_string('secs');
1879 $str->year = get_string('year');
1880 $str->years = get_string('years');
1884 $years = floor($totalsecs/YEARSECS);
1885 $remainder = $totalsecs - ($years*YEARSECS);
1886 $days = floor($remainder/DAYSECS);
1887 $remainder = $totalsecs - ($days*DAYSECS);
1888 $hours = floor($remainder/HOURSECS);
1889 $remainder = $remainder - ($hours*HOURSECS);
1890 $mins = floor($remainder/MINSECS);
1891 $secs = $remainder - ($mins*MINSECS);
1893 $ss = ($secs == 1) ? $str->sec : $str->secs;
1894 $sm = ($mins == 1) ? $str->min : $str->mins;
1895 $sh = ($hours == 1) ? $str->hour : $str->hours;
1896 $sd = ($days == 1) ? $str->day : $str->days;
1897 $sy = ($years == 1) ? $str->year : $str->years;
1899 $oyears = '';
1900 $odays = '';
1901 $ohours = '';
1902 $omins = '';
1903 $osecs = '';
1905 if ($years) $oyears = $years .' '. $sy;
1906 if ($days) $odays = $days .' '. $sd;
1907 if ($hours) $ohours = $hours .' '. $sh;
1908 if ($mins) $omins = $mins .' '. $sm;
1909 if ($secs) $osecs = $secs .' '. $ss;
1911 if ($years) return trim($oyears .' '. $odays);
1912 if ($days) return trim($odays .' '. $ohours);
1913 if ($hours) return trim($ohours .' '. $omins);
1914 if ($mins) return trim($omins .' '. $osecs);
1915 if ($secs) return $osecs;
1916 return get_string('now');
1920 * Returns a formatted string that represents a date in user time
1922 * Returns a formatted string that represents a date in user time
1923 * <b>WARNING: note that the format is for strftime(), not date().</b>
1924 * Because of a bug in most Windows time libraries, we can't use
1925 * the nicer %e, so we have to use %d which has leading zeroes.
1926 * A lot of the fuss in the function is just getting rid of these leading
1927 * zeroes as efficiently as possible.
1929 * If parameter fixday = true (default), then take off leading
1930 * zero from %d, else maintain it.
1932 * @param int $date the timestamp in UTC, as obtained from the database.
1933 * @param string $format strftime format. You should probably get this using
1934 * get_string('strftime...', 'langconfig');
1935 * @param mixed $timezone by default, uses the user's time zone. if numeric and
1936 * not 99 then daylight saving will not be added.
1937 * @param bool $fixday If true (default) then the leading zero from %d is removed.
1938 * If false then the leading zero is maintained.
1939 * @return string the formatted date/time.
1941 function userdate($date, $format = '', $timezone = 99, $fixday = true) {
1943 global $CFG;
1945 if (empty($format)) {
1946 $format = get_string('strftimedaydatetime', 'langconfig');
1949 if (!empty($CFG->nofixday)) { // Config.php can force %d not to be fixed.
1950 $fixday = false;
1951 } else if ($fixday) {
1952 $formatnoday = str_replace('%d', 'DD', $format);
1953 $fixday = ($formatnoday != $format);
1956 //add daylight saving offset for string timezones only, as we can't get dst for
1957 //float values. if timezone is 99 (user default timezone), then try update dst.
1958 if ((99 == $timezone) || !is_numeric($timezone)) {
1959 $date += dst_offset_on($date, $timezone);
1962 $timezone = get_user_timezone_offset($timezone);
1964 if (abs($timezone) > 13) { /// Server time
1965 if ($fixday) {
1966 $datestring = strftime($formatnoday, $date);
1967 $daystring = ltrim(str_replace(array(' 0', ' '), '', strftime(' %d', $date)));
1968 $datestring = str_replace('DD', $daystring, $datestring);
1969 } else {
1970 $datestring = strftime($format, $date);
1972 } else {
1973 $date += (int)($timezone * 3600);
1974 if ($fixday) {
1975 $datestring = gmstrftime($formatnoday, $date);
1976 $daystring = ltrim(str_replace(array(' 0', ' '), '', gmstrftime(' %d', $date)));
1977 $datestring = str_replace('DD', $daystring, $datestring);
1978 } else {
1979 $datestring = gmstrftime($format, $date);
1983 /// If we are running under Windows convert from windows encoding to UTF-8
1984 /// (because it's impossible to specify UTF-8 to fetch locale info in Win32)
1986 if ($CFG->ostype == 'WINDOWS') {
1987 if ($localewincharset = get_string('localewincharset', 'langconfig')) {
1988 $textlib = textlib_get_instance();
1989 $datestring = $textlib->convert($datestring, $localewincharset, 'utf-8');
1993 return $datestring;
1997 * Given a $time timestamp in GMT (seconds since epoch),
1998 * returns an array that represents the date in user time
2000 * @todo Finish documenting this function
2001 * @uses HOURSECS
2002 * @param int $time Timestamp in GMT
2003 * @param mixed $timezone offset time with timezone, if float and not 99, then no
2004 * dst offset is applyed
2005 * @return array An array that represents the date in user time
2007 function usergetdate($time, $timezone=99) {
2009 //save input timezone, required for dst offset check.
2010 $passedtimezone = $timezone;
2012 $timezone = get_user_timezone_offset($timezone);
2014 if (abs($timezone) > 13) { // Server time
2015 return getdate($time);
2018 //add daylight saving offset for string timezones only, as we can't get dst for
2019 //float values. if timezone is 99 (user default timezone), then try update dst.
2020 if ($passedtimezone == 99 || !is_numeric($passedtimezone)) {
2021 $time += dst_offset_on($time, $passedtimezone);
2024 $time += intval((float)$timezone * HOURSECS);
2026 $datestring = gmstrftime('%B_%A_%j_%Y_%m_%w_%d_%H_%M_%S', $time);
2028 //be careful to ensure the returned array matches that produced by getdate() above
2029 list(
2030 $getdate['month'],
2031 $getdate['weekday'],
2032 $getdate['yday'],
2033 $getdate['year'],
2034 $getdate['mon'],
2035 $getdate['wday'],
2036 $getdate['mday'],
2037 $getdate['hours'],
2038 $getdate['minutes'],
2039 $getdate['seconds']
2040 ) = explode('_', $datestring);
2042 return $getdate;
2046 * Given a GMT timestamp (seconds since epoch), offsets it by
2047 * the timezone. eg 3pm in India is 3pm GMT - 7 * 3600 seconds
2049 * @uses HOURSECS
2050 * @param int $date Timestamp in GMT
2051 * @param float $timezone
2052 * @return int
2054 function usertime($date, $timezone=99) {
2056 $timezone = get_user_timezone_offset($timezone);
2058 if (abs($timezone) > 13) {
2059 return $date;
2061 return $date - (int)($timezone * HOURSECS);
2065 * Given a time, return the GMT timestamp of the most recent midnight
2066 * for the current user.
2068 * @param int $date Timestamp in GMT
2069 * @param float $timezone Defaults to user's timezone
2070 * @return int Returns a GMT timestamp
2072 function usergetmidnight($date, $timezone=99) {
2074 $userdate = usergetdate($date, $timezone);
2076 // Time of midnight of this user's day, in GMT
2077 return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone);
2082 * Returns a string that prints the user's timezone
2084 * @param float $timezone The user's timezone
2085 * @return string
2087 function usertimezone($timezone=99) {
2089 $tz = get_user_timezone($timezone);
2091 if (!is_float($tz)) {
2092 return $tz;
2095 if(abs($tz) > 13) { // Server time
2096 return get_string('serverlocaltime');
2099 if($tz == intval($tz)) {
2100 // Don't show .0 for whole hours
2101 $tz = intval($tz);
2104 if($tz == 0) {
2105 return 'UTC';
2107 else if($tz > 0) {
2108 return 'UTC+'.$tz;
2110 else {
2111 return 'UTC'.$tz;
2117 * Returns a float which represents the user's timezone difference from GMT in hours
2118 * Checks various settings and picks the most dominant of those which have a value
2120 * @global object
2121 * @global object
2122 * @param float $tz If this value is provided and not equal to 99, it will be returned as is and no other settings will be checked
2123 * @return float
2125 function get_user_timezone_offset($tz = 99) {
2127 global $USER, $CFG;
2129 $tz = get_user_timezone($tz);
2131 if (is_float($tz)) {
2132 return $tz;
2133 } else {
2134 $tzrecord = get_timezone_record($tz);
2135 if (empty($tzrecord)) {
2136 return 99.0;
2138 return (float)$tzrecord->gmtoff / HOURMINS;
2143 * Returns an int which represents the systems's timezone difference from GMT in seconds
2145 * @global object
2146 * @param mixed $tz timezone
2147 * @return int if found, false is timezone 99 or error
2149 function get_timezone_offset($tz) {
2150 global $CFG;
2152 if ($tz == 99) {
2153 return false;
2156 if (is_numeric($tz)) {
2157 return intval($tz * 60*60);
2160 if (!$tzrecord = get_timezone_record($tz)) {
2161 return false;
2163 return intval($tzrecord->gmtoff * 60);
2167 * Returns a float or a string which denotes the user's timezone
2168 * 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)
2169 * means that for this timezone there are also DST rules to be taken into account
2170 * Checks various settings and picks the most dominant of those which have a value
2172 * @global object
2173 * @global object
2174 * @param mixed $tz If this value is provided and not equal to 99, it will be returned as is and no other settings will be checked
2175 * @return mixed
2177 function get_user_timezone($tz = 99) {
2178 global $USER, $CFG;
2180 $timezones = array(
2181 $tz,
2182 isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99,
2183 isset($USER->timezone) ? $USER->timezone : 99,
2184 isset($CFG->timezone) ? $CFG->timezone : 99,
2187 $tz = 99;
2189 while(($tz == '' || $tz == 99 || $tz == NULL) && $next = each($timezones)) {
2190 $tz = $next['value'];
2193 return is_numeric($tz) ? (float) $tz : $tz;
2197 * Returns cached timezone record for given $timezonename
2199 * @global object
2200 * @global object
2201 * @param string $timezonename
2202 * @return mixed timezonerecord object or false
2204 function get_timezone_record($timezonename) {
2205 global $CFG, $DB;
2206 static $cache = NULL;
2208 if ($cache === NULL) {
2209 $cache = array();
2212 if (isset($cache[$timezonename])) {
2213 return $cache[$timezonename];
2216 return $cache[$timezonename] = $DB->get_record_sql('SELECT * FROM {timezone}
2217 WHERE name = ? ORDER BY year DESC', array($timezonename), IGNORE_MULTIPLE);
2221 * Build and store the users Daylight Saving Time (DST) table
2223 * @global object
2224 * @global object
2225 * @global object
2226 * @param mixed $from_year Start year for the table, defaults to 1971
2227 * @param mixed $to_year End year for the table, defaults to 2035
2228 * @param mixed $strtimezone, if null or 99 then user's default timezone is used
2229 * @return bool
2231 function calculate_user_dst_table($from_year = NULL, $to_year = NULL, $strtimezone = NULL) {
2232 global $CFG, $SESSION, $DB;
2234 $usertz = get_user_timezone($strtimezone);
2236 if (is_float($usertz)) {
2237 // Trivial timezone, no DST
2238 return false;
2241 if (!empty($SESSION->dst_offsettz) && $SESSION->dst_offsettz != $usertz) {
2242 // We have precalculated values, but the user's effective TZ has changed in the meantime, so reset
2243 unset($SESSION->dst_offsets);
2244 unset($SESSION->dst_range);
2247 if (!empty($SESSION->dst_offsets) && empty($from_year) && empty($to_year)) {
2248 // Repeat calls which do not request specific year ranges stop here, we have already calculated the table
2249 // This will be the return path most of the time, pretty light computationally
2250 return true;
2253 // Reaching here means we either need to extend our table or create it from scratch
2255 // Remember which TZ we calculated these changes for
2256 $SESSION->dst_offsettz = $usertz;
2258 if(empty($SESSION->dst_offsets)) {
2259 // If we 're creating from scratch, put the two guard elements in there
2260 $SESSION->dst_offsets = array(1 => NULL, 0 => NULL);
2262 if(empty($SESSION->dst_range)) {
2263 // If creating from scratch
2264 $from = max((empty($from_year) ? intval(date('Y')) - 3 : $from_year), 1971);
2265 $to = min((empty($to_year) ? intval(date('Y')) + 3 : $to_year), 2035);
2267 // Fill in the array with the extra years we need to process
2268 $yearstoprocess = array();
2269 for($i = $from; $i <= $to; ++$i) {
2270 $yearstoprocess[] = $i;
2273 // Take note of which years we have processed for future calls
2274 $SESSION->dst_range = array($from, $to);
2276 else {
2277 // If needing to extend the table, do the same
2278 $yearstoprocess = array();
2280 $from = max((empty($from_year) ? $SESSION->dst_range[0] : $from_year), 1971);
2281 $to = min((empty($to_year) ? $SESSION->dst_range[1] : $to_year), 2035);
2283 if($from < $SESSION->dst_range[0]) {
2284 // Take note of which years we need to process and then note that we have processed them for future calls
2285 for($i = $from; $i < $SESSION->dst_range[0]; ++$i) {
2286 $yearstoprocess[] = $i;
2288 $SESSION->dst_range[0] = $from;
2290 if($to > $SESSION->dst_range[1]) {
2291 // Take note of which years we need to process and then note that we have processed them for future calls
2292 for($i = $SESSION->dst_range[1] + 1; $i <= $to; ++$i) {
2293 $yearstoprocess[] = $i;
2295 $SESSION->dst_range[1] = $to;
2299 if(empty($yearstoprocess)) {
2300 // This means that there was a call requesting a SMALLER range than we have already calculated
2301 return true;
2304 // From now on, we know that the array has at least the two guard elements, and $yearstoprocess has the years we need
2305 // Also, the array is sorted in descending timestamp order!
2307 // Get DB data
2309 static $presets_cache = array();
2310 if (!isset($presets_cache[$usertz])) {
2311 $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');
2313 if(empty($presets_cache[$usertz])) {
2314 return false;
2317 // Remove ending guard (first element of the array)
2318 reset($SESSION->dst_offsets);
2319 unset($SESSION->dst_offsets[key($SESSION->dst_offsets)]);
2321 // Add all required change timestamps
2322 foreach($yearstoprocess as $y) {
2323 // Find the record which is in effect for the year $y
2324 foreach($presets_cache[$usertz] as $year => $preset) {
2325 if($year <= $y) {
2326 break;
2330 $changes = dst_changes_for_year($y, $preset);
2332 if($changes === NULL) {
2333 continue;
2335 if($changes['dst'] != 0) {
2336 $SESSION->dst_offsets[$changes['dst']] = $preset->dstoff * MINSECS;
2338 if($changes['std'] != 0) {
2339 $SESSION->dst_offsets[$changes['std']] = 0;
2343 // Put in a guard element at the top
2344 $maxtimestamp = max(array_keys($SESSION->dst_offsets));
2345 $SESSION->dst_offsets[($maxtimestamp + DAYSECS)] = NULL; // DAYSECS is arbitrary, any "small" number will do
2347 // Sort again
2348 krsort($SESSION->dst_offsets);
2350 return true;
2354 * Calculates the required DST change and returns a Timestamp Array
2356 * @uses HOURSECS
2357 * @uses MINSECS
2358 * @param mixed $year Int or String Year to focus on
2359 * @param object $timezone Instatiated Timezone object
2360 * @return mixed Null, or Array dst=>xx, 0=>xx, std=>yy, 1=>yy
2362 function dst_changes_for_year($year, $timezone) {
2364 if($timezone->dst_startday == 0 && $timezone->dst_weekday == 0 && $timezone->std_startday == 0 && $timezone->std_weekday == 0) {
2365 return NULL;
2368 $monthdaydst = find_day_in_month($timezone->dst_startday, $timezone->dst_weekday, $timezone->dst_month, $year);
2369 $monthdaystd = find_day_in_month($timezone->std_startday, $timezone->std_weekday, $timezone->std_month, $year);
2371 list($dst_hour, $dst_min) = explode(':', $timezone->dst_time);
2372 list($std_hour, $std_min) = explode(':', $timezone->std_time);
2374 $timedst = make_timestamp($year, $timezone->dst_month, $monthdaydst, 0, 0, 0, 99, false);
2375 $timestd = make_timestamp($year, $timezone->std_month, $monthdaystd, 0, 0, 0, 99, false);
2377 // Instead of putting hour and minute in make_timestamp(), we add them afterwards.
2378 // This has the advantage of being able to have negative values for hour, i.e. for timezones
2379 // where GMT time would be in the PREVIOUS day than the local one on which DST changes.
2381 $timedst += $dst_hour * HOURSECS + $dst_min * MINSECS;
2382 $timestd += $std_hour * HOURSECS + $std_min * MINSECS;
2384 return array('dst' => $timedst, 0 => $timedst, 'std' => $timestd, 1 => $timestd);
2388 * Calculates the Daylight Saving Offset for a given date/time (timestamp)
2389 * - Note: Daylight saving only works for string timezones and not for float.
2391 * @global object
2392 * @param int $time must NOT be compensated at all, it has to be a pure timestamp
2393 * @param mixed $strtimezone timezone for which offset is expected, if 99 or null
2394 * then user's default timezone is used.
2395 * @return int
2397 function dst_offset_on($time, $strtimezone = NULL) {
2398 global $SESSION;
2400 if(!calculate_user_dst_table(NULL, NULL, $strtimezone) || empty($SESSION->dst_offsets)) {
2401 return 0;
2404 reset($SESSION->dst_offsets);
2405 while(list($from, $offset) = each($SESSION->dst_offsets)) {
2406 if($from <= $time) {
2407 break;
2411 // This is the normal return path
2412 if($offset !== NULL) {
2413 return $offset;
2416 // Reaching this point means we haven't calculated far enough, do it now:
2417 // Calculate extra DST changes if needed and recurse. The recursion always
2418 // moves toward the stopping condition, so will always end.
2420 if($from == 0) {
2421 // We need a year smaller than $SESSION->dst_range[0]
2422 if($SESSION->dst_range[0] == 1971) {
2423 return 0;
2425 calculate_user_dst_table($SESSION->dst_range[0] - 5, NULL, $strtimezone);
2426 return dst_offset_on($time, $strtimezone);
2428 else {
2429 // We need a year larger than $SESSION->dst_range[1]
2430 if($SESSION->dst_range[1] == 2035) {
2431 return 0;
2433 calculate_user_dst_table(NULL, $SESSION->dst_range[1] + 5, $strtimezone);
2434 return dst_offset_on($time, $strtimezone);
2441 * @todo Document what this function does
2442 * @param int $startday
2443 * @param int $weekday
2444 * @param int $month
2445 * @param int $year
2446 * @return int
2448 function find_day_in_month($startday, $weekday, $month, $year) {
2450 $daysinmonth = days_in_month($month, $year);
2452 if($weekday == -1) {
2453 // Don't care about weekday, so return:
2454 // abs($startday) if $startday != -1
2455 // $daysinmonth otherwise
2456 return ($startday == -1) ? $daysinmonth : abs($startday);
2459 // From now on we 're looking for a specific weekday
2461 // Give "end of month" its actual value, since we know it
2462 if($startday == -1) {
2463 $startday = -1 * $daysinmonth;
2466 // Starting from day $startday, the sign is the direction
2468 if($startday < 1) {
2470 $startday = abs($startday);
2471 $lastmonthweekday = strftime('%w', mktime(12, 0, 0, $month, $daysinmonth, $year));
2473 // This is the last such weekday of the month
2474 $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday;
2475 if($lastinmonth > $daysinmonth) {
2476 $lastinmonth -= 7;
2479 // Find the first such weekday <= $startday
2480 while($lastinmonth > $startday) {
2481 $lastinmonth -= 7;
2484 return $lastinmonth;
2487 else {
2489 $indexweekday = strftime('%w', mktime(12, 0, 0, $month, $startday, $year));
2491 $diff = $weekday - $indexweekday;
2492 if($diff < 0) {
2493 $diff += 7;
2496 // This is the first such weekday of the month equal to or after $startday
2497 $firstfromindex = $startday + $diff;
2499 return $firstfromindex;
2505 * Calculate the number of days in a given month
2507 * @param int $month The month whose day count is sought
2508 * @param int $year The year of the month whose day count is sought
2509 * @return int
2511 function days_in_month($month, $year) {
2512 return intval(date('t', mktime(12, 0, 0, $month, 1, $year)));
2516 * Calculate the position in the week of a specific calendar day
2518 * @param int $day The day of the date whose position in the week is sought
2519 * @param int $month The month of the date whose position in the week is sought
2520 * @param int $year The year of the date whose position in the week is sought
2521 * @return int
2523 function dayofweek($day, $month, $year) {
2524 // I wonder if this is any different from
2525 // strftime('%w', mktime(12, 0, 0, $month, $daysinmonth, $year, 0));
2526 return intval(date('w', mktime(12, 0, 0, $month, $day, $year)));
2529 /// USER AUTHENTICATION AND LOGIN ////////////////////////////////////////
2532 * Returns full login url.
2534 * @return string login url
2536 function get_login_url() {
2537 global $CFG;
2539 $url = "$CFG->wwwroot/login/index.php";
2541 if (!empty($CFG->loginhttps)) {
2542 $url = str_replace('http:', 'https:', $url);
2545 return $url;
2549 * This function checks that the current user is logged in and has the
2550 * required privileges
2552 * This function checks that the current user is logged in, and optionally
2553 * whether they are allowed to be in a particular course and view a particular
2554 * course module.
2555 * If they are not logged in, then it redirects them to the site login unless
2556 * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which
2557 * case they are automatically logged in as guests.
2558 * If $courseid is given and the user is not enrolled in that course then the
2559 * user is redirected to the course enrolment page.
2560 * If $cm is given and the course module is hidden and the user is not a teacher
2561 * in the course then the user is redirected to the course home page.
2563 * When $cm parameter specified, this function sets page layout to 'module'.
2564 * You need to change it manually later if some other layout needed.
2566 * @param mixed $courseorid id of the course or course object
2567 * @param bool $autologinguest default true
2568 * @param object $cm course module object
2569 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2570 * true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2571 * in order to keep redirects working properly. MDL-14495
2572 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2573 * @return mixed Void, exit, and die depending on path
2575 function require_login($courseorid = NULL, $autologinguest = true, $cm = NULL, $setwantsurltome = true, $preventredirect = false) {
2576 global $CFG, $SESSION, $USER, $FULLME, $PAGE, $SITE, $DB, $OUTPUT;
2578 // setup global $COURSE, themes, language and locale
2579 if (!empty($courseorid)) {
2580 if (is_object($courseorid)) {
2581 $course = $courseorid;
2582 } else if ($courseorid == SITEID) {
2583 $course = clone($SITE);
2584 } else {
2585 $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST);
2587 if ($cm) {
2588 if ($cm->course != $course->id) {
2589 throw new coding_exception('course and cm parameters in require_login() call do not match!!');
2591 // make sure we have a $cm from get_fast_modinfo as this contains activity access details
2592 if (!($cm instanceof cm_info)) {
2593 // note: nearly all pages call get_fast_modinfo anyway and it does not make any
2594 // db queries so this is not really a performance concern, however it is obviously
2595 // better if you use get_fast_modinfo to get the cm before calling this.
2596 $modinfo = get_fast_modinfo($course);
2597 $cm = $modinfo->get_cm($cm->id);
2599 $PAGE->set_cm($cm, $course); // set's up global $COURSE
2600 $PAGE->set_pagelayout('incourse');
2601 } else {
2602 $PAGE->set_course($course); // set's up global $COURSE
2604 } else {
2605 // do not touch global $COURSE via $PAGE->set_course(),
2606 // the reasons is we need to be able to call require_login() at any time!!
2607 $course = $SITE;
2608 if ($cm) {
2609 throw new coding_exception('cm parameter in require_login() requires valid course parameter!');
2613 // If the user is not even logged in yet then make sure they are
2614 if (!isloggedin()) {
2615 if ($autologinguest and !empty($CFG->guestloginbutton) and !empty($CFG->autologinguests)) {
2616 if (!$guest = get_complete_user_data('id', $CFG->siteguest)) {
2617 // misconfigured site guest, just redirect to login page
2618 redirect(get_login_url());
2619 exit; // never reached
2621 $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang;
2622 complete_user_login($guest);
2623 $USER->autologinguest = true;
2624 $SESSION->lang = $lang;
2625 } else {
2626 //NOTE: $USER->site check was obsoleted by session test cookie,
2627 // $USER->confirmed test is in login/index.php
2628 if ($preventredirect) {
2629 throw new require_login_exception('You are not logged in');
2632 if ($setwantsurltome) {
2633 // TODO: switch to PAGE->url
2634 $SESSION->wantsurl = $FULLME;
2636 if (!empty($_SERVER['HTTP_REFERER'])) {
2637 $SESSION->fromurl = $_SERVER['HTTP_REFERER'];
2639 redirect(get_login_url());
2640 exit; // never reached
2644 // loginas as redirection if needed
2645 if ($course->id != SITEID and session_is_loggedinas()) {
2646 if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) {
2647 if ($USER->loginascontext->instanceid != $course->id) {
2648 print_error('loginasonecourse', '', $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid);
2653 // check whether the user should be changing password (but only if it is REALLY them)
2654 if (get_user_preferences('auth_forcepasswordchange') && !session_is_loggedinas()) {
2655 $userauth = get_auth_plugin($USER->auth);
2656 if ($userauth->can_change_password() and !$preventredirect) {
2657 $SESSION->wantsurl = $FULLME;
2658 if ($changeurl = $userauth->change_password_url()) {
2659 //use plugin custom url
2660 redirect($changeurl);
2661 } else {
2662 //use moodle internal method
2663 if (empty($CFG->loginhttps)) {
2664 redirect($CFG->wwwroot .'/login/change_password.php');
2665 } else {
2666 $wwwroot = str_replace('http:','https:', $CFG->wwwroot);
2667 redirect($wwwroot .'/login/change_password.php');
2670 } else {
2671 print_error('nopasswordchangeforced', 'auth');
2675 // Check that the user account is properly set up
2676 if (user_not_fully_set_up($USER)) {
2677 if ($preventredirect) {
2678 throw new require_login_exception('User not fully set-up');
2680 $SESSION->wantsurl = $FULLME;
2681 redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&amp;course='. SITEID);
2684 // Make sure the USER has a sesskey set up. Used for CSRF protection.
2685 sesskey();
2687 // Do not bother admins with any formalities
2688 if (is_siteadmin()) {
2689 //set accesstime or the user will appear offline which messes up messaging
2690 user_accesstime_log($course->id);
2691 return;
2694 // Check that the user has agreed to a site policy if there is one - do not test in case of admins
2695 if (!$USER->policyagreed and !is_siteadmin()) {
2696 if (!empty($CFG->sitepolicy) and !isguestuser()) {
2697 if ($preventredirect) {
2698 throw new require_login_exception('Policy not agreed');
2700 $SESSION->wantsurl = $FULLME;
2701 redirect($CFG->wwwroot .'/user/policy.php');
2702 } else if (!empty($CFG->sitepolicyguest) and isguestuser()) {
2703 if ($preventredirect) {
2704 throw new require_login_exception('Policy not agreed');
2706 $SESSION->wantsurl = $FULLME;
2707 redirect($CFG->wwwroot .'/user/policy.php');
2711 // Fetch the system context, the course context, and prefetch its child contexts
2712 $sysctx = get_context_instance(CONTEXT_SYSTEM);
2713 $coursecontext = get_context_instance(CONTEXT_COURSE, $course->id, MUST_EXIST);
2714 if ($cm) {
2715 $cmcontext = get_context_instance(CONTEXT_MODULE, $cm->id, MUST_EXIST);
2716 } else {
2717 $cmcontext = null;
2720 // If the site is currently under maintenance, then print a message
2721 if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:config', $sysctx)) {
2722 if ($preventredirect) {
2723 throw new require_login_exception('Maintenance in progress');
2726 print_maintenance_message();
2729 // make sure the course itself is not hidden
2730 if ($course->id == SITEID) {
2731 // frontpage can not be hidden
2732 } else {
2733 if (is_role_switched($course->id)) {
2734 // when switching roles ignore the hidden flag - user had to be in course to do the switch
2735 } else {
2736 if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
2737 // originally there was also test of parent category visibility,
2738 // BUT is was very slow in complex queries involving "my courses"
2739 // now it is also possible to simply hide all courses user is not enrolled in :-)
2740 if ($preventredirect) {
2741 throw new require_login_exception('Course is hidden');
2743 notice(get_string('coursehidden'), $CFG->wwwroot .'/');
2748 // is the user enrolled?
2749 if ($course->id == SITEID) {
2750 // everybody is enrolled on the frontpage
2752 } else {
2753 if (session_is_loggedinas()) {
2754 // Make sure the REAL person can access this course first
2755 $realuser = session_get_realuser();
2756 if (!is_enrolled($coursecontext, $realuser->id, '', true) and !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) {
2757 if ($preventredirect) {
2758 throw new require_login_exception('Invalid course login-as access');
2760 echo $OUTPUT->header();
2761 notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/');
2765 $access = false;
2767 if (is_role_switched($course->id)) {
2768 // ok, user had to be inside this course before the switch
2769 $access = true;
2771 } else if (is_viewing($coursecontext, $USER)) {
2772 // ok, no need to mess with enrol
2773 $access = true;
2775 } else {
2776 if (isset($USER->enrol['enrolled'][$course->id])) {
2777 if ($USER->enrol['enrolled'][$course->id] > time()) {
2778 $access = true;
2779 if (isset($USER->enrol['tempguest'][$course->id])) {
2780 unset($USER->enrol['tempguest'][$course->id]);
2781 remove_temp_course_roles($coursecontext);
2783 } else {
2784 //expired
2785 unset($USER->enrol['enrolled'][$course->id]);
2788 if (isset($USER->enrol['tempguest'][$course->id])) {
2789 if ($USER->enrol['tempguest'][$course->id] == 0) {
2790 $access = true;
2791 } else if ($USER->enrol['tempguest'][$course->id] > time()) {
2792 $access = true;
2793 } else {
2794 //expired
2795 unset($USER->enrol['tempguest'][$course->id]);
2796 remove_temp_course_roles($coursecontext);
2800 if ($access) {
2801 // cache ok
2802 } else {
2803 $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id);
2804 if ($until !== false) {
2805 // active participants may always access, a timestamp in the future, 0 (always) or false.
2806 if ($until == 0) {
2807 $until = ENROL_MAX_TIMESTAMP;
2809 $USER->enrol['enrolled'][$course->id] = $until;
2810 $access = true;
2812 } else {
2813 $instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder, id ASC');
2814 $enrols = enrol_get_plugins(true);
2815 // first ask all enabled enrol instances in course if they want to auto enrol user
2816 foreach($instances as $instance) {
2817 if (!isset($enrols[$instance->enrol])) {
2818 continue;
2820 // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false.
2821 $until = $enrols[$instance->enrol]->try_autoenrol($instance);
2822 if ($until !== false) {
2823 if ($until == 0) {
2824 $until = ENROL_MAX_TIMESTAMP;
2826 $USER->enrol['enrolled'][$course->id] = $until;
2827 $access = true;
2828 break;
2831 // if not enrolled yet try to gain temporary guest access
2832 if (!$access) {
2833 foreach($instances as $instance) {
2834 if (!isset($enrols[$instance->enrol])) {
2835 continue;
2837 // Get a duration for the guest access, a timestamp in the future or false.
2838 $until = $enrols[$instance->enrol]->try_guestaccess($instance);
2839 if ($until !== false and $until > time()) {
2840 $USER->enrol['tempguest'][$course->id] = $until;
2841 $access = true;
2842 break;
2850 if (!$access) {
2851 if ($preventredirect) {
2852 throw new require_login_exception('Not enrolled');
2854 $SESSION->wantsurl = $FULLME;
2855 redirect($CFG->wwwroot .'/enrol/index.php?id='. $course->id);
2859 // Check visibility of activity to current user; includes visible flag, groupmembersonly,
2860 // conditional availability, etc
2861 if ($cm && !$cm->uservisible) {
2862 if ($preventredirect) {
2863 throw new require_login_exception('Activity is hidden');
2865 redirect($CFG->wwwroot, get_string('activityiscurrentlyhidden'));
2868 // Finally access granted, update lastaccess times
2869 user_accesstime_log($course->id);
2874 * This function just makes sure a user is logged out.
2876 * @global object
2878 function require_logout() {
2879 global $USER;
2881 $params = $USER;
2883 if (isloggedin()) {
2884 add_to_log(SITEID, "user", "logout", "view.php?id=$USER->id&course=".SITEID, $USER->id, 0, $USER->id);
2886 $authsequence = get_enabled_auth_plugins(); // auths, in sequence
2887 foreach($authsequence as $authname) {
2888 $authplugin = get_auth_plugin($authname);
2889 $authplugin->prelogout_hook();
2893 events_trigger('user_logout', $params);
2894 session_get_instance()->terminate_current();
2895 unset($params);
2899 * Weaker version of require_login()
2901 * This is a weaker version of {@link require_login()} which only requires login
2902 * when called from within a course rather than the site page, unless
2903 * the forcelogin option is turned on.
2904 * @see require_login()
2906 * @global object
2907 * @param mixed $courseorid The course object or id in question
2908 * @param bool $autologinguest Allow autologin guests if that is wanted
2909 * @param object $cm Course activity module if known
2910 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2911 * true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2912 * in order to keep redirects working properly. MDL-14495
2913 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2914 * @return void
2916 function require_course_login($courseorid, $autologinguest = true, $cm = NULL, $setwantsurltome = true, $preventredirect = false) {
2917 global $CFG, $PAGE, $SITE;
2918 $issite = (is_object($courseorid) and $courseorid->id == SITEID)
2919 or (!is_object($courseorid) and $courseorid == SITEID);
2920 if ($issite && !empty($cm) && !($cm instanceof cm_info)) {
2921 // note: nearly all pages call get_fast_modinfo anyway and it does not make any
2922 // db queries so this is not really a performance concern, however it is obviously
2923 // better if you use get_fast_modinfo to get the cm before calling this.
2924 if (is_object($courseorid)) {
2925 $course = $courseorid;
2926 } else {
2927 $course = clone($SITE);
2929 $modinfo = get_fast_modinfo($course);
2930 $cm = $modinfo->get_cm($cm->id);
2932 if (!empty($CFG->forcelogin)) {
2933 // login required for both SITE and courses
2934 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2936 } else if ($issite && !empty($cm) and !$cm->uservisible) {
2937 // always login for hidden activities
2938 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2940 } else if ($issite) {
2941 //login for SITE not required
2942 if ($cm and empty($cm->visible)) {
2943 // hidden activities are not accessible without login
2944 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2945 } else if ($cm and !empty($CFG->enablegroupmembersonly) and $cm->groupmembersonly) {
2946 // not-logged-in users do not have any group membership
2947 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2948 } else {
2949 // We still need to instatiate PAGE vars properly so that things
2950 // that rely on it like navigation function correctly.
2951 if (!empty($courseorid)) {
2952 if (is_object($courseorid)) {
2953 $course = $courseorid;
2954 } else {
2955 $course = clone($SITE);
2957 if ($cm) {
2958 if ($cm->course != $course->id) {
2959 throw new coding_exception('course and cm parameters in require_course_login() call do not match!!');
2961 $PAGE->set_cm($cm, $course);
2962 $PAGE->set_pagelayout('incourse');
2963 } else {
2964 $PAGE->set_course($course);
2966 } else {
2967 // If $PAGE->course, and hence $PAGE->context, have not already been set
2968 // up properly, set them up now.
2969 $PAGE->set_course($PAGE->course);
2971 //TODO: verify conditional activities here
2972 user_accesstime_log(SITEID);
2973 return;
2976 } else {
2977 // course login always required
2978 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2983 * Require key login. Function terminates with error if key not found or incorrect.
2985 * @global object
2986 * @global object
2987 * @global object
2988 * @global object
2989 * @uses NO_MOODLE_COOKIES
2990 * @uses PARAM_ALPHANUM
2991 * @param string $script unique script identifier
2992 * @param int $instance optional instance id
2993 * @return int Instance ID
2995 function require_user_key_login($script, $instance=null) {
2996 global $USER, $SESSION, $CFG, $DB;
2998 if (!NO_MOODLE_COOKIES) {
2999 print_error('sessioncookiesdisable');
3002 /// extra safety
3003 @session_write_close();
3005 $keyvalue = required_param('key', PARAM_ALPHANUM);
3007 if (!$key = $DB->get_record('user_private_key', array('script'=>$script, 'value'=>$keyvalue, 'instance'=>$instance))) {
3008 print_error('invalidkey');
3011 if (!empty($key->validuntil) and $key->validuntil < time()) {
3012 print_error('expiredkey');
3015 if ($key->iprestriction) {
3016 $remoteaddr = getremoteaddr(null);
3017 if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) {
3018 print_error('ipmismatch');
3022 if (!$user = $DB->get_record('user', array('id'=>$key->userid))) {
3023 print_error('invaliduserid');
3026 /// emulate normal session
3027 enrol_check_plugins($user);
3028 session_set_user($user);
3030 /// note we are not using normal login
3031 if (!defined('USER_KEY_LOGIN')) {
3032 define('USER_KEY_LOGIN', true);
3035 /// return instance id - it might be empty
3036 return $key->instance;
3040 * Creates a new private user access key.
3042 * @global object
3043 * @param string $script unique target identifier
3044 * @param int $userid
3045 * @param int $instance optional instance id
3046 * @param string $iprestriction optional ip restricted access
3047 * @param timestamp $validuntil key valid only until given data
3048 * @return string access key value
3050 function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3051 global $DB;
3053 $key = new stdClass();
3054 $key->script = $script;
3055 $key->userid = $userid;
3056 $key->instance = $instance;
3057 $key->iprestriction = $iprestriction;
3058 $key->validuntil = $validuntil;
3059 $key->timecreated = time();
3061 $key->value = md5($userid.'_'.time().random_string(40)); // something long and unique
3062 while ($DB->record_exists('user_private_key', array('value'=>$key->value))) {
3063 // must be unique
3064 $key->value = md5($userid.'_'.time().random_string(40));
3066 $DB->insert_record('user_private_key', $key);
3067 return $key->value;
3071 * Delete the user's new private user access keys for a particular script.
3073 * @global object
3074 * @param string $script unique target identifier
3075 * @param int $userid
3076 * @return void
3078 function delete_user_key($script,$userid) {
3079 global $DB;
3080 $DB->delete_records('user_private_key', array('script'=>$script, 'userid'=>$userid));
3084 * Gets a private user access key (and creates one if one doesn't exist).
3086 * @global object
3087 * @param string $script unique target identifier
3088 * @param int $userid
3089 * @param int $instance optional instance id
3090 * @param string $iprestriction optional ip restricted access
3091 * @param timestamp $validuntil key valid only until given data
3092 * @return string access key value
3094 function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3095 global $DB;
3097 if ($key = $DB->get_record('user_private_key', array('script'=>$script, 'userid'=>$userid,
3098 'instance'=>$instance, 'iprestriction'=>$iprestriction,
3099 'validuntil'=>$validuntil))) {
3100 return $key->value;
3101 } else {
3102 return create_user_key($script, $userid, $instance, $iprestriction, $validuntil);
3108 * Modify the user table by setting the currently logged in user's
3109 * last login to now.
3111 * @global object
3112 * @global object
3113 * @return bool Always returns true
3115 function update_user_login_times() {
3116 global $USER, $DB;
3118 $user = new stdClass();
3119 $USER->lastlogin = $user->lastlogin = $USER->currentlogin;
3120 $USER->currentlogin = $user->lastaccess = $user->currentlogin = time();
3122 $user->id = $USER->id;
3124 $DB->update_record('user', $user);
3125 return true;
3129 * Determines if a user has completed setting up their account.
3131 * @param user $user A {@link $USER} object to test for the existence of a valid name and email
3132 * @return bool
3134 function user_not_fully_set_up($user) {
3135 if (isguestuser($user)) {
3136 return false;
3138 return (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user));
3142 * Check whether the user has exceeded the bounce threshold
3144 * @global object
3145 * @global object
3146 * @param user $user A {@link $USER} object
3147 * @return bool true=>User has exceeded bounce threshold
3149 function over_bounce_threshold($user) {
3150 global $CFG, $DB;
3152 if (empty($CFG->handlebounces)) {
3153 return false;
3156 if (empty($user->id)) { /// No real (DB) user, nothing to do here.
3157 return false;
3160 // set sensible defaults
3161 if (empty($CFG->minbounces)) {
3162 $CFG->minbounces = 10;
3164 if (empty($CFG->bounceratio)) {
3165 $CFG->bounceratio = .20;
3167 $bouncecount = 0;
3168 $sendcount = 0;
3169 if ($bounce = $DB->get_record('user_preferences', array ('userid'=>$user->id, 'name'=>'email_bounce_count'))) {
3170 $bouncecount = $bounce->value;
3172 if ($send = $DB->get_record('user_preferences', array('userid'=>$user->id, 'name'=>'email_send_count'))) {
3173 $sendcount = $send->value;
3175 return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio);
3179 * Used to increment or reset email sent count
3181 * @global object
3182 * @param user $user object containing an id
3183 * @param bool $reset will reset the count to 0
3184 * @return void
3186 function set_send_count($user,$reset=false) {
3187 global $DB;
3189 if (empty($user->id)) { /// No real (DB) user, nothing to do here.
3190 return;
3193 if ($pref = $DB->get_record('user_preferences', array('userid'=>$user->id, 'name'=>'email_send_count'))) {
3194 $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3195 $DB->update_record('user_preferences', $pref);
3197 else if (!empty($reset)) { // if it's not there and we're resetting, don't bother.
3198 // make a new one
3199 $pref = new stdClass();
3200 $pref->name = 'email_send_count';
3201 $pref->value = 1;
3202 $pref->userid = $user->id;
3203 $DB->insert_record('user_preferences', $pref, false);
3208 * Increment or reset user's email bounce count
3210 * @global object
3211 * @param user $user object containing an id
3212 * @param bool $reset will reset the count to 0
3214 function set_bounce_count($user,$reset=false) {
3215 global $DB;
3217 if ($pref = $DB->get_record('user_preferences', array('userid'=>$user->id, 'name'=>'email_bounce_count'))) {
3218 $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3219 $DB->update_record('user_preferences', $pref);
3221 else if (!empty($reset)) { // if it's not there and we're resetting, don't bother.
3222 // make a new one
3223 $pref = new stdClass();
3224 $pref->name = 'email_bounce_count';
3225 $pref->value = 1;
3226 $pref->userid = $user->id;
3227 $DB->insert_record('user_preferences', $pref, false);
3232 * Keeps track of login attempts
3234 * @global object
3236 function update_login_count() {
3237 global $SESSION;
3239 $max_logins = 10;
3241 if (empty($SESSION->logincount)) {
3242 $SESSION->logincount = 1;
3243 } else {
3244 $SESSION->logincount++;
3247 if ($SESSION->logincount > $max_logins) {
3248 unset($SESSION->wantsurl);
3249 print_error('errortoomanylogins');
3254 * Resets login attempts
3256 * @global object
3258 function reset_login_count() {
3259 global $SESSION;
3261 $SESSION->logincount = 0;
3265 * Determines if the currently logged in user is in editing mode.
3266 * Note: originally this function had $userid parameter - it was not usable anyway
3268 * @deprecated since Moodle 2.0 - use $PAGE->user_is_editing() instead.
3269 * @todo Deprecated function remove when ready
3271 * @global object
3272 * @uses DEBUG_DEVELOPER
3273 * @return bool
3275 function isediting() {
3276 global $PAGE;
3277 debugging('call to deprecated function isediting(). Please use $PAGE->user_is_editing() instead', DEBUG_DEVELOPER);
3278 return $PAGE->user_is_editing();
3282 * Determines if the logged in user is currently moving an activity
3284 * @global object
3285 * @param int $courseid The id of the course being tested
3286 * @return bool
3288 function ismoving($courseid) {
3289 global $USER;
3291 if (!empty($USER->activitycopy)) {
3292 return ($USER->activitycopycourse == $courseid);
3294 return false;
3298 * Returns a persons full name
3300 * Given an object containing firstname and lastname
3301 * values, this function returns a string with the
3302 * full name of the person.
3303 * The result may depend on system settings
3304 * or language. 'override' will force both names
3305 * to be used even if system settings specify one.
3307 * @global object
3308 * @global object
3309 * @param object $user A {@link $USER} object to get full name of
3310 * @param bool $override If true then the name will be first name followed by last name rather than adhering to fullnamedisplay setting.
3311 * @return string
3313 function fullname($user, $override=false) {
3314 global $CFG, $SESSION;
3316 if (!isset($user->firstname) and !isset($user->lastname)) {
3317 return '';
3320 if (!$override) {
3321 if (!empty($CFG->forcefirstname)) {
3322 $user->firstname = $CFG->forcefirstname;
3324 if (!empty($CFG->forcelastname)) {
3325 $user->lastname = $CFG->forcelastname;
3329 if (!empty($SESSION->fullnamedisplay)) {
3330 $CFG->fullnamedisplay = $SESSION->fullnamedisplay;
3333 if (!isset($CFG->fullnamedisplay) or $CFG->fullnamedisplay === 'firstname lastname') {
3334 return $user->firstname .' '. $user->lastname;
3336 } else if ($CFG->fullnamedisplay == 'lastname firstname') {
3337 return $user->lastname .' '. $user->firstname;
3339 } else if ($CFG->fullnamedisplay == 'firstname') {
3340 if ($override) {
3341 return get_string('fullnamedisplay', '', $user);
3342 } else {
3343 return $user->firstname;
3347 return get_string('fullnamedisplay', '', $user);
3351 * Checks if current user is shown any extra fields when listing users.
3352 * @param object $context Context
3353 * @param array $already Array of fields that we're going to show anyway
3354 * so don't bother listing them
3355 * @return array Array of field names from user table, not including anything
3356 * listed in $already
3358 function get_extra_user_fields($context, $already = array()) {
3359 global $CFG;
3361 // Only users with permission get the extra fields
3362 if (!has_capability('moodle/site:viewuseridentity', $context)) {
3363 return array();
3366 // Split showuseridentity on comma
3367 if (empty($CFG->showuseridentity)) {
3368 // Explode gives wrong result with empty string
3369 $extra = array();
3370 } else {
3371 $extra = explode(',', $CFG->showuseridentity);
3373 $renumber = false;
3374 foreach ($extra as $key => $field) {
3375 if (in_array($field, $already)) {
3376 unset($extra[$key]);
3377 $renumber = true;
3380 if ($renumber) {
3381 // For consistency, if entries are removed from array, renumber it
3382 // so they are numbered as you would expect
3383 $extra = array_merge($extra);
3385 return $extra;
3389 * If the current user is to be shown extra user fields when listing or
3390 * selecting users, returns a string suitable for including in an SQL select
3391 * clause to retrieve those fields.
3392 * @param object $context Context
3393 * @param string $alias Alias of user table, e.g. 'u' (default none)
3394 * @param string $prefix Prefix for field names using AS, e.g. 'u_' (default none)
3395 * @param array $already Array of fields that we're going to include anyway
3396 * so don't list them (default none)
3397 * @return string Partial SQL select clause, beginning with comma, for example
3398 * ',u.idnumber,u.department' unless it is blank
3400 function get_extra_user_fields_sql($context, $alias='', $prefix='',
3401 $already = array()) {
3402 $fields = get_extra_user_fields($context, $already);
3403 $result = '';
3404 // Add punctuation for alias
3405 if ($alias !== '') {
3406 $alias .= '.';
3408 foreach ($fields as $field) {
3409 $result .= ', ' . $alias . $field;
3410 if ($prefix) {
3411 $result .= ' AS ' . $prefix . $field;
3414 return $result;
3418 * Returns the display name of a field in the user table. Works for most fields
3419 * that are commonly displayed to users.
3420 * @param string $field Field name, e.g. 'phone1'
3421 * @return string Text description taken from language file, e.g. 'Phone number'
3423 function get_user_field_name($field) {
3424 // Some fields have language strings which are not the same as field name
3425 switch ($field) {
3426 case 'phone1' : return get_string('phone');
3428 // Otherwise just use the same lang string
3429 return get_string($field);
3433 * Returns whether a given authentication plugin exists.
3435 * @global object
3436 * @param string $auth Form of authentication to check for. Defaults to the
3437 * global setting in {@link $CFG}.
3438 * @return boolean Whether the plugin is available.
3440 function exists_auth_plugin($auth) {
3441 global $CFG;
3443 if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) {
3444 return is_readable("{$CFG->dirroot}/auth/$auth/auth.php");
3446 return false;
3450 * Checks if a given plugin is in the list of enabled authentication plugins.
3452 * @param string $auth Authentication plugin.
3453 * @return boolean Whether the plugin is enabled.
3455 function is_enabled_auth($auth) {
3456 if (empty($auth)) {
3457 return false;
3460 $enabled = get_enabled_auth_plugins();
3462 return in_array($auth, $enabled);
3466 * Returns an authentication plugin instance.
3468 * @global object
3469 * @param string $auth name of authentication plugin
3470 * @return auth_plugin_base An instance of the required authentication plugin.
3472 function get_auth_plugin($auth) {
3473 global $CFG;
3475 // check the plugin exists first
3476 if (! exists_auth_plugin($auth)) {
3477 print_error('authpluginnotfound', 'debug', '', $auth);
3480 // return auth plugin instance
3481 require_once "{$CFG->dirroot}/auth/$auth/auth.php";
3482 $class = "auth_plugin_$auth";
3483 return new $class;
3487 * Returns array of active auth plugins.
3489 * @param bool $fix fix $CFG->auth if needed
3490 * @return array
3492 function get_enabled_auth_plugins($fix=false) {
3493 global $CFG;
3495 $default = array('manual', 'nologin');
3497 if (empty($CFG->auth)) {
3498 $auths = array();
3499 } else {
3500 $auths = explode(',', $CFG->auth);
3503 if ($fix) {
3504 $auths = array_unique($auths);
3505 foreach($auths as $k=>$authname) {
3506 if (!exists_auth_plugin($authname) or in_array($authname, $default)) {
3507 unset($auths[$k]);
3510 $newconfig = implode(',', $auths);
3511 if (!isset($CFG->auth) or $newconfig != $CFG->auth) {
3512 set_config('auth', $newconfig);
3516 return (array_merge($default, $auths));
3520 * Returns true if an internal authentication method is being used.
3521 * if method not specified then, global default is assumed
3523 * @param string $auth Form of authentication required
3524 * @return bool
3526 function is_internal_auth($auth) {
3527 $authplugin = get_auth_plugin($auth); // throws error if bad $auth
3528 return $authplugin->is_internal();
3532 * Returns true if the user is a 'restored' one
3534 * Used in the login process to inform the user
3535 * and allow him/her to reset the password
3537 * @uses $CFG
3538 * @uses $DB
3539 * @param string $username username to be checked
3540 * @return bool
3542 function is_restored_user($username) {
3543 global $CFG, $DB;
3545 return $DB->record_exists('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id, 'password'=>'restored'));
3549 * Returns an array of user fields
3551 * @return array User field/column names
3553 function get_user_fieldnames() {
3554 global $DB;
3556 $fieldarray = $DB->get_columns('user');
3557 unset($fieldarray['id']);
3558 $fieldarray = array_keys($fieldarray);
3560 return $fieldarray;
3564 * Creates a bare-bones user record
3566 * @todo Outline auth types and provide code example
3568 * @param string $username New user's username to add to record
3569 * @param string $password New user's password to add to record
3570 * @param string $auth Form of authentication required
3571 * @return stdClass A complete user object
3573 function create_user_record($username, $password, $auth = 'manual') {
3574 global $CFG, $DB;
3576 //just in case check text case
3577 $username = trim(moodle_strtolower($username));
3579 $authplugin = get_auth_plugin($auth);
3581 $newuser = new stdClass();
3583 if ($newinfo = $authplugin->get_userinfo($username)) {
3584 $newinfo = truncate_userinfo($newinfo);
3585 foreach ($newinfo as $key => $value){
3586 $newuser->$key = $value;
3590 if (!empty($newuser->email)) {
3591 if (email_is_not_allowed($newuser->email)) {
3592 unset($newuser->email);
3596 if (!isset($newuser->city)) {
3597 $newuser->city = '';
3600 $newuser->auth = $auth;
3601 $newuser->username = $username;
3603 // fix for MDL-8480
3604 // user CFG lang for user if $newuser->lang is empty
3605 // or $user->lang is not an installed language
3606 if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) {
3607 $newuser->lang = $CFG->lang;
3609 $newuser->confirmed = 1;
3610 $newuser->lastip = getremoteaddr();
3611 $newuser->timecreated = time();
3612 $newuser->timemodified = $newuser->timecreated;
3613 $newuser->mnethostid = $CFG->mnet_localhost_id;
3615 $newuser->id = $DB->insert_record('user', $newuser);
3616 $user = get_complete_user_data('id', $newuser->id);
3617 if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})){
3618 set_user_preference('auth_forcepasswordchange', 1, $user);
3620 update_internal_user_password($user, $password);
3622 // fetch full user record for the event, the complete user data contains too much info
3623 // and we want to be consistent with other places that trigger this event
3624 events_trigger('user_created', $DB->get_record('user', array('id'=>$user->id)));
3626 return $user;
3630 * Will update a local user record from an external source.
3631 * (MNET users can not be updated using this method!)
3633 * @param string $username user's username to update the record
3634 * @return stdClass A complete user object
3636 function update_user_record($username) {
3637 global $DB, $CFG;
3639 $username = trim(moodle_strtolower($username)); /// just in case check text case
3641 $oldinfo = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id), '*', MUST_EXIST);
3642 $newuser = array();
3643 $userauth = get_auth_plugin($oldinfo->auth);
3645 if ($newinfo = $userauth->get_userinfo($username)) {
3646 $newinfo = truncate_userinfo($newinfo);
3647 foreach ($newinfo as $key => $value){
3648 $key = strtolower($key);
3649 if (!property_exists($oldinfo, $key) or $key === 'username' or $key === 'id'
3650 or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') {
3651 // unknown or must not be changed
3652 continue;
3654 $confval = $userauth->config->{'field_updatelocal_' . $key};
3655 $lockval = $userauth->config->{'field_lock_' . $key};
3656 if (empty($confval) || empty($lockval)) {
3657 continue;
3659 if ($confval === 'onlogin') {
3660 // MDL-4207 Don't overwrite modified user profile values with
3661 // empty LDAP values when 'unlocked if empty' is set. The purpose
3662 // of the setting 'unlocked if empty' is to allow the user to fill
3663 // in a value for the selected field _if LDAP is giving
3664 // nothing_ for this field. Thus it makes sense to let this value
3665 // stand in until LDAP is giving a value for this field.
3666 if (!(empty($value) && $lockval === 'unlockedifempty')) {
3667 if ((string)$oldinfo->$key !== (string)$value) {
3668 $newuser[$key] = (string)$value;
3673 if ($newuser) {
3674 $newuser['id'] = $oldinfo->id;
3675 $newuser['timemodified'] = time();
3676 $DB->update_record('user', $newuser);
3677 // fetch full user record for the event, the complete user data contains too much info
3678 // and we want to be consistent with other places that trigger this event
3679 events_trigger('user_updated', $DB->get_record('user', array('id'=>$oldinfo->id)));
3683 return get_complete_user_data('id', $oldinfo->id);
3687 * Will truncate userinfo as it comes from auth_get_userinfo (from external auth)
3688 * which may have large fields
3690 * @todo Add vartype handling to ensure $info is an array
3692 * @param array $info Array of user properties to truncate if needed
3693 * @return array The now truncated information that was passed in
3695 function truncate_userinfo($info) {
3696 // define the limits
3697 $limit = array(
3698 'username' => 100,
3699 'idnumber' => 255,
3700 'firstname' => 100,
3701 'lastname' => 100,
3702 'email' => 100,
3703 'icq' => 15,
3704 'phone1' => 20,
3705 'phone2' => 20,
3706 'institution' => 40,
3707 'department' => 30,
3708 'address' => 70,
3709 'city' => 120,
3710 'country' => 2,
3711 'url' => 255,
3714 $textlib = textlib_get_instance();
3715 // apply where needed
3716 foreach (array_keys($info) as $key) {
3717 if (!empty($limit[$key])) {
3718 $info[$key] = trim($textlib->substr($info[$key],0, $limit[$key]));
3722 return $info;
3726 * Marks user deleted in internal user database and notifies the auth plugin.
3727 * Also unenrols user from all roles and does other cleanup.
3729 * Any plugin that needs to purge user data should register the 'user_deleted' event.
3731 * @param stdClass $user full user object before delete
3732 * @return boolean always true
3734 function delete_user($user) {
3735 global $CFG, $DB;
3736 require_once($CFG->libdir.'/grouplib.php');
3737 require_once($CFG->libdir.'/gradelib.php');
3738 require_once($CFG->dirroot.'/message/lib.php');
3739 require_once($CFG->dirroot.'/tag/lib.php');
3741 // delete all grades - backup is kept in grade_grades_history table
3742 grade_user_delete($user->id);
3744 //move unread messages from this user to read
3745 message_move_userfrom_unread2read($user->id);
3747 // TODO: remove from cohorts using standard API here
3749 // remove user tags
3750 tag_set('user', $user->id, array());
3752 // unconditionally unenrol from all courses
3753 enrol_user_delete($user);
3755 // unenrol from all roles in all contexts
3756 role_unassign_all(array('userid'=>$user->id)); // this might be slow but it is really needed - modules might do some extra cleanup!
3758 //now do a brute force cleanup
3760 // remove from all cohorts
3761 $DB->delete_records('cohort_members', array('userid'=>$user->id));
3763 // remove from all groups
3764 $DB->delete_records('groups_members', array('userid'=>$user->id));
3766 // brute force unenrol from all courses
3767 $DB->delete_records('user_enrolments', array('userid'=>$user->id));
3769 // purge user preferences
3770 $DB->delete_records('user_preferences', array('userid'=>$user->id));
3772 // purge user extra profile info
3773 $DB->delete_records('user_info_data', array('userid'=>$user->id));
3775 // last course access not necessary either
3776 $DB->delete_records('user_lastaccess', array('userid'=>$user->id));
3778 // force logout - may fail if file based sessions used, sorry
3779 session_kill_user($user->id);
3781 // now do a final accesslib cleanup - removes all role assignments in user context and context itself
3782 delete_context(CONTEXT_USER, $user->id);
3784 // workaround for bulk deletes of users with the same email address
3785 $delname = "$user->email.".time();
3786 while ($DB->record_exists('user', array('username'=>$delname))) { // no need to use mnethostid here
3787 $delname++;
3790 // mark internal user record as "deleted"
3791 $updateuser = new stdClass();
3792 $updateuser->id = $user->id;
3793 $updateuser->deleted = 1;
3794 $updateuser->username = $delname; // Remember it just in case
3795 $updateuser->email = md5($user->username);// Store hash of username, useful importing/restoring users
3796 $updateuser->idnumber = ''; // Clear this field to free it up
3797 $updateuser->timemodified = time();
3799 $DB->update_record('user', $updateuser);
3801 // We will update the user's timemodified, as it will be passed to the user_deleted event, which
3802 // should know about this updated property persisted to the user's table.
3803 $user->timemodified = $updateuser->timemodified;
3805 // notify auth plugin - do not block the delete even when plugin fails
3806 $authplugin = get_auth_plugin($user->auth);
3807 $authplugin->user_delete($user);
3809 // any plugin that needs to cleanup should register this event
3810 events_trigger('user_deleted', $user);
3812 return true;
3816 * Retrieve the guest user object
3818 * @global object
3819 * @global object
3820 * @return user A {@link $USER} object
3822 function guest_user() {
3823 global $CFG, $DB;
3825 if ($newuser = $DB->get_record('user', array('id'=>$CFG->siteguest))) {
3826 $newuser->confirmed = 1;
3827 $newuser->lang = $CFG->lang;
3828 $newuser->lastip = getremoteaddr();
3831 return $newuser;
3835 * Authenticates a user against the chosen authentication mechanism
3837 * Given a username and password, this function looks them
3838 * up using the currently selected authentication mechanism,
3839 * and if the authentication is successful, it returns a
3840 * valid $user object from the 'user' table.
3842 * Uses auth_ functions from the currently active auth module
3844 * After authenticate_user_login() returns success, you will need to
3845 * log that the user has logged in, and call complete_user_login() to set
3846 * the session up.
3848 * Note: this function works only with non-mnet accounts!
3850 * @param string $username User's username
3851 * @param string $password User's password
3852 * @return user|flase A {@link $USER} object or false if error
3854 function authenticate_user_login($username, $password) {
3855 global $CFG, $DB;
3857 $authsenabled = get_enabled_auth_plugins();
3859 if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) {
3860 $auth = empty($user->auth) ? 'manual' : $user->auth; // use manual if auth not set
3861 if (!empty($user->suspended)) {
3862 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
3863 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']);
3864 return false;
3866 if ($auth=='nologin' or !is_enabled_auth($auth)) {
3867 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
3868 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Disabled Login: $username ".$_SERVER['HTTP_USER_AGENT']);
3869 return false;
3871 $auths = array($auth);
3873 } else {
3874 // check if there's a deleted record (cheaply)
3875 if ($DB->get_field('user', 'id', array('username'=>$username, 'deleted'=>1))) {
3876 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Deleted Login: $username ".$_SERVER['HTTP_USER_AGENT']);
3877 return false;
3880 // User does not exist
3881 $auths = $authsenabled;
3882 $user = new stdClass();
3883 $user->id = 0;
3886 foreach ($auths as $auth) {
3887 $authplugin = get_auth_plugin($auth);
3889 // on auth fail fall through to the next plugin
3890 if (!$authplugin->user_login($username, $password)) {
3891 continue;
3894 // successful authentication
3895 if ($user->id) { // User already exists in database
3896 if (empty($user->auth)) { // For some reason auth isn't set yet
3897 $DB->set_field('user', 'auth', $auth, array('username'=>$username));
3898 $user->auth = $auth;
3900 if (empty($user->firstaccess)) { //prevent firstaccess from remaining 0 for manual account that never required confirmation
3901 $DB->set_field('user','firstaccess', $user->timemodified, array('id' => $user->id));
3902 $user->firstaccess = $user->timemodified;
3905 update_internal_user_password($user, $password); // just in case salt or encoding were changed (magic quotes too one day)
3907 if ($authplugin->is_synchronised_with_external()) { // update user record from external DB
3908 $user = update_user_record($username);
3910 } else {
3911 // if user not found and user creation is not disabled, create it
3912 if (empty($CFG->authpreventaccountcreation)) {
3913 $user = create_user_record($username, $password, $auth);
3914 } else {
3915 continue;
3919 $authplugin->sync_roles($user);
3921 foreach ($authsenabled as $hau) {
3922 $hauth = get_auth_plugin($hau);
3923 $hauth->user_authenticated_hook($user, $username, $password);
3926 if (empty($user->id)) {
3927 return false;
3930 if (!empty($user->suspended)) {
3931 // just in case some auth plugin suspended account
3932 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
3933 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']);
3934 return false;
3937 return $user;
3940 // failed if all the plugins have failed
3941 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
3942 if (debugging('', DEBUG_ALL)) {
3943 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Failed Login: $username ".$_SERVER['HTTP_USER_AGENT']);
3945 return false;
3949 * Call to complete the user login process after authenticate_user_login()
3950 * has succeeded. It will setup the $USER variable and other required bits
3951 * and pieces.
3953 * NOTE:
3954 * - It will NOT log anything -- up to the caller to decide what to log.
3955 * - this function does not set any cookies any more!
3957 * @param object $user
3958 * @return object A {@link $USER} object - BC only, do not use
3960 function complete_user_login($user) {
3961 global $CFG, $USER;
3963 // regenerate session id and delete old session,
3964 // this helps prevent session fixation attacks from the same domain
3965 session_regenerate_id(true);
3967 // let enrol plugins deal with new enrolments if necessary
3968 enrol_check_plugins($user);
3970 // check enrolments, load caps and setup $USER object
3971 session_set_user($user);
3973 // reload preferences from DB
3974 unset($USER->preference);
3975 check_user_preferences_loaded($USER);
3977 // update login times
3978 update_user_login_times();
3980 // extra session prefs init
3981 set_login_session_preferences();
3983 if (isguestuser()) {
3984 // no need to continue when user is THE guest
3985 return $USER;
3988 /// Select password change url
3989 $userauth = get_auth_plugin($USER->auth);
3991 /// check whether the user should be changing password
3992 if (get_user_preferences('auth_forcepasswordchange', false)){
3993 if ($userauth->can_change_password()) {
3994 if ($changeurl = $userauth->change_password_url()) {
3995 redirect($changeurl);
3996 } else {
3997 redirect($CFG->httpswwwroot.'/login/change_password.php');
3999 } else {
4000 print_error('nopasswordchangeforced', 'auth');
4003 return $USER;
4007 * Compare password against hash stored in internal user table.
4008 * If necessary it also updates the stored hash to new format.
4010 * @param stdClass $user (password property may be updated)
4011 * @param string $password plain text password
4012 * @return bool is password valid?
4014 function validate_internal_user_password($user, $password) {
4015 global $CFG;
4017 if (!isset($CFG->passwordsaltmain)) {
4018 $CFG->passwordsaltmain = '';
4021 $validated = false;
4023 if ($user->password === 'not cached') {
4024 // internal password is not used at all, it can not validate
4026 } else if ($user->password === md5($password.$CFG->passwordsaltmain)
4027 or $user->password === md5($password)
4028 or $user->password === md5(addslashes($password).$CFG->passwordsaltmain)
4029 or $user->password === md5(addslashes($password))) {
4030 // note: we are intentionally using the addslashes() here because we
4031 // need to accept old password hashes of passwords with magic quotes
4032 $validated = true;
4034 } else {
4035 for ($i=1; $i<=20; $i++) { //20 alternative salts should be enough, right?
4036 $alt = 'passwordsaltalt'.$i;
4037 if (!empty($CFG->$alt)) {
4038 if ($user->password === md5($password.$CFG->$alt) or $user->password === md5(addslashes($password).$CFG->$alt)) {
4039 $validated = true;
4040 break;
4046 if ($validated) {
4047 // force update of password hash using latest main password salt and encoding if needed
4048 update_internal_user_password($user, $password);
4051 return $validated;
4055 * Calculate hashed value from password using current hash mechanism.
4057 * @param string $password
4058 * @return string password hash
4060 function hash_internal_user_password($password) {
4061 global $CFG;
4063 if (isset($CFG->passwordsaltmain)) {
4064 return md5($password.$CFG->passwordsaltmain);
4065 } else {
4066 return md5($password);
4071 * Update password hash in user object.
4073 * @param stdClass $user (password property may be updated)
4074 * @param string $password plain text password
4075 * @return bool always returns true
4077 function update_internal_user_password($user, $password) {
4078 global $DB;
4080 $authplugin = get_auth_plugin($user->auth);
4081 if ($authplugin->prevent_local_passwords()) {
4082 $hashedpassword = 'not cached';
4083 } else {
4084 $hashedpassword = hash_internal_user_password($password);
4087 if ($user->password !== $hashedpassword) {
4088 $DB->set_field('user', 'password', $hashedpassword, array('id'=>$user->id));
4089 $user->password = $hashedpassword;
4092 return true;
4096 * Get a complete user record, which includes all the info
4097 * in the user record.
4099 * Intended for setting as $USER session variable
4101 * @param string $field The user field to be checked for a given value.
4102 * @param string $value The value to match for $field.
4103 * @param int $mnethostid
4104 * @return mixed False, or A {@link $USER} object.
4106 function get_complete_user_data($field, $value, $mnethostid = null) {
4107 global $CFG, $DB;
4109 if (!$field || !$value) {
4110 return false;
4113 /// Build the WHERE clause for an SQL query
4114 $params = array('fieldval'=>$value);
4115 $constraints = "$field = :fieldval AND deleted <> 1";
4117 // If we are loading user data based on anything other than id,
4118 // we must also restrict our search based on mnet host.
4119 if ($field != 'id') {
4120 if (empty($mnethostid)) {
4121 // if empty, we restrict to local users
4122 $mnethostid = $CFG->mnet_localhost_id;
4125 if (!empty($mnethostid)) {
4126 $params['mnethostid'] = $mnethostid;
4127 $constraints .= " AND mnethostid = :mnethostid";
4130 /// Get all the basic user data
4132 if (! $user = $DB->get_record_select('user', $constraints, $params)) {
4133 return false;
4136 /// Get various settings and preferences
4138 // preload preference cache
4139 check_user_preferences_loaded($user);
4141 // load course enrolment related stuff
4142 $user->lastcourseaccess = array(); // during last session
4143 $user->currentcourseaccess = array(); // during current session
4144 if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid'=>$user->id))) {
4145 foreach ($lastaccesses as $lastaccess) {
4146 $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess;
4150 $sql = "SELECT g.id, g.courseid
4151 FROM {groups} g, {groups_members} gm
4152 WHERE gm.groupid=g.id AND gm.userid=?";
4154 // this is a special hack to speedup calendar display
4155 $user->groupmember = array();
4156 if (!isguestuser($user)) {
4157 if ($groups = $DB->get_records_sql($sql, array($user->id))) {
4158 foreach ($groups as $group) {
4159 if (!array_key_exists($group->courseid, $user->groupmember)) {
4160 $user->groupmember[$group->courseid] = array();
4162 $user->groupmember[$group->courseid][$group->id] = $group->id;
4167 /// Add the custom profile fields to the user record
4168 $user->profile = array();
4169 if (!isguestuser($user)) {
4170 require_once($CFG->dirroot.'/user/profile/lib.php');
4171 profile_load_custom_fields($user);
4174 /// Rewrite some variables if necessary
4175 if (!empty($user->description)) {
4176 $user->description = true; // No need to cart all of it around
4178 if (isguestuser($user)) {
4179 $user->lang = $CFG->lang; // Guest language always same as site
4180 $user->firstname = get_string('guestuser'); // Name always in current language
4181 $user->lastname = ' ';
4184 return $user;
4188 * Validate a password against the configured password policy
4190 * @global object
4191 * @param string $password the password to be checked against the password policy
4192 * @param string $errmsg the error message to display when the password doesn't comply with the policy.
4193 * @return bool true if the password is valid according to the policy. false otherwise.
4195 function check_password_policy($password, &$errmsg) {
4196 global $CFG;
4198 if (empty($CFG->passwordpolicy)) {
4199 return true;
4202 $textlib = textlib_get_instance();
4203 $errmsg = '';
4204 if ($textlib->strlen($password) < $CFG->minpasswordlength) {
4205 $errmsg .= '<div>'. get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength) .'</div>';
4208 if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) {
4209 $errmsg .= '<div>'. get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits) .'</div>';
4212 if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) {
4213 $errmsg .= '<div>'. get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower) .'</div>';
4216 if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) {
4217 $errmsg .= '<div>'. get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper) .'</div>';
4220 if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) {
4221 $errmsg .= '<div>'. get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum) .'</div>';
4223 if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) {
4224 $errmsg .= '<div>'. get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars) .'</div>';
4227 if ($errmsg == '') {
4228 return true;
4229 } else {
4230 return false;
4236 * When logging in, this function is run to set certain preferences
4237 * for the current SESSION
4239 * @global object
4240 * @global object
4242 function set_login_session_preferences() {
4243 global $SESSION, $CFG;
4245 $SESSION->justloggedin = true;
4247 unset($SESSION->lang);
4252 * Delete a course, including all related data from the database,
4253 * and any associated files.
4255 * @global object
4256 * @global object
4257 * @param mixed $courseorid The id of the course or course object to delete.
4258 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4259 * @return bool true if all the removals succeeded. false if there were any failures. If this
4260 * method returns false, some of the removals will probably have succeeded, and others
4261 * failed, but you have no way of knowing which.
4263 function delete_course($courseorid, $showfeedback = true) {
4264 global $DB;
4266 if (is_object($courseorid)) {
4267 $courseid = $courseorid->id;
4268 $course = $courseorid;
4269 } else {
4270 $courseid = $courseorid;
4271 if (!$course = $DB->get_record('course', array('id'=>$courseid))) {
4272 return false;
4275 $context = get_context_instance(CONTEXT_COURSE, $courseid);
4277 // frontpage course can not be deleted!!
4278 if ($courseid == SITEID) {
4279 return false;
4282 // make the course completely empty
4283 remove_course_contents($courseid, $showfeedback);
4285 // delete the course and related context instance
4286 delete_context(CONTEXT_COURSE, $courseid);
4288 // We will update the course's timemodified, as it will be passed to the course_deleted event,
4289 // which should know about this updated property, as this event is meant to pass the full course record
4290 $course->timemodified = time();
4292 $DB->delete_records("course", array("id"=>$courseid));
4294 //trigger events
4295 $course->context = $context; // you can not fetch context in the event because it was already deleted
4296 events_trigger('course_deleted', $course);
4298 return true;
4302 * Clear a course out completely, deleting all content
4303 * but don't delete the course itself.
4304 * This function does not verify any permissions.
4306 * Please note this function also deletes all user enrolments,
4307 * enrolment instances and role assignments by default.
4309 * $options:
4310 * - 'keep_roles_and_enrolments' - false by default
4311 * - 'keep_groups_and_groupings' - false by default
4313 * @param int $courseid The id of the course that is being deleted
4314 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4315 * @param array $options extra options
4316 * @return bool true if all the removals succeeded. false if there were any failures. If this
4317 * method returns false, some of the removals will probably have succeeded, and others
4318 * failed, but you have no way of knowing which.
4320 function remove_course_contents($courseid, $showfeedback = true, array $options = null) {
4321 global $CFG, $DB, $OUTPUT;
4322 require_once($CFG->libdir.'/completionlib.php');
4323 require_once($CFG->libdir.'/questionlib.php');
4324 require_once($CFG->libdir.'/gradelib.php');
4325 require_once($CFG->dirroot.'/group/lib.php');
4326 require_once($CFG->dirroot.'/tag/coursetagslib.php');
4327 require_once($CFG->dirroot.'/comment/lib.php');
4328 require_once($CFG->dirroot.'/rating/lib.php');
4330 // NOTE: these concatenated strings are suboptimal, but it is just extra info...
4331 $strdeleted = get_string('deleted').' - ';
4333 // Some crazy wishlist of stuff we should skip during purging of course content
4334 $options = (array)$options;
4336 $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST);
4337 $coursecontext = context_course::instance($courseid);
4338 $fs = get_file_storage();
4340 // Delete course completion information, this has to be done before grades and enrols
4341 $cc = new completion_info($course);
4342 $cc->clear_criteria();
4343 if ($showfeedback) {
4344 echo $OUTPUT->notification($strdeleted.get_string('completion', 'completion'), 'notifysuccess');
4347 // Remove all data from gradebook - this needs to be done before course modules
4348 // because while deleting this information, the system may need to reference
4349 // the course modules that own the grades.
4350 remove_course_grades($courseid, $showfeedback);
4351 remove_grade_letters($coursecontext, $showfeedback);
4353 // Delete course blocks in any all child contexts,
4354 // they may depend on modules so delete them first
4355 $childcontexts = $coursecontext->get_child_contexts(); // returns all subcontexts since 2.2
4356 foreach ($childcontexts as $childcontext) {
4357 blocks_delete_all_for_context($childcontext->id);
4359 unset($childcontexts);
4360 blocks_delete_all_for_context($coursecontext->id);
4361 if ($showfeedback) {
4362 echo $OUTPUT->notification($strdeleted.get_string('type_block_plural', 'plugin'), 'notifysuccess');
4365 // Delete every instance of every module,
4366 // this has to be done before deleting of course level stuff
4367 $locations = get_plugin_list('mod');
4368 foreach ($locations as $modname=>$moddir) {
4369 if ($modname === 'NEWMODULE') {
4370 continue;
4372 if ($module = $DB->get_record('modules', array('name'=>$modname))) {
4373 include_once("$moddir/lib.php"); // Shows php warning only if plugin defective
4374 $moddelete = $modname .'_delete_instance'; // Delete everything connected to an instance
4375 $moddeletecourse = $modname .'_delete_course'; // Delete other stray stuff (uncommon)
4377 if ($instances = $DB->get_records($modname, array('course'=>$course->id))) {
4378 foreach ($instances as $instance) {
4379 if ($cm = get_coursemodule_from_instance($modname, $instance->id, $course->id)) {
4380 /// Delete activity context questions and question categories
4381 question_delete_activity($cm, $showfeedback);
4383 if (function_exists($moddelete)) {
4384 // This purges all module data in related tables, extra user prefs, settings, etc.
4385 $moddelete($instance->id);
4386 } else {
4387 // NOTE: we should not allow installation of modules with missing delete support!
4388 debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!");
4389 $DB->delete_records($modname, array('id'=>$instance->id));
4392 if ($cm) {
4393 // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition
4394 context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4395 $DB->delete_records('course_modules', array('id'=>$cm->id));
4399 if (function_exists($moddeletecourse)) {
4400 // Execute ptional course cleanup callback
4401 $moddeletecourse($course, $showfeedback);
4403 if ($instances and $showfeedback) {
4404 echo $OUTPUT->notification($strdeleted.get_string('pluginname', $modname), 'notifysuccess');
4406 } else {
4407 // Ooops, this module is not properly installed, force-delete it in the next block
4410 // We have tried to delete everything the nice way - now let's force-delete any remaining module data
4411 $cms = $DB->get_records('course_modules', array('course'=>$course->id));
4412 foreach ($cms as $cm) {
4413 if ($module = $DB->get_record('modules', array('id'=>$cm->module))) {
4414 try {
4415 $DB->delete_records($module->name, array('id'=>$cm->instance));
4416 } catch (Exception $e) {
4417 // Ignore weird or missing table problems
4420 context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4421 $DB->delete_records('course_modules', array('id'=>$cm->id));
4423 // Remove all data from availability and completion tables that is associated
4424 // with course-modules belonging to this course. Note this is done even if the
4425 // features are not enabled now, in case they were enabled previously
4426 $DB->delete_records_select('course_modules_completion',
4427 'coursemoduleid IN (SELECT id from {course_modules} WHERE course=?)',
4428 array($courseid));
4429 $DB->delete_records_select('course_modules_availability',
4430 'coursemoduleid IN (SELECT id from {course_modules} WHERE course=?)',
4431 array($courseid));
4432 if ($showfeedback) {
4433 echo $OUTPUT->notification($strdeleted.get_string('type_mod_plural', 'plugin'), 'notifysuccess');
4436 // Cleanup the rest of plugins
4437 $cleanuplugintypes = array('report', 'coursereport', 'format');
4438 foreach ($cleanuplugintypes as $type) {
4439 $plugins = get_plugin_list_with_function($type, 'delete_course', 'lib.php');
4440 foreach ($plugins as $plugin=>$pluginfunction) {
4441 $pluginfunction($course->id, $showfeedback);
4443 if ($showfeedback) {
4444 echo $OUTPUT->notification($strdeleted.get_string('type_'.$type.'_plural', 'plugin'), 'notifysuccess');
4448 // Delete questions and question categories
4449 question_delete_course($course, $showfeedback);
4450 if ($showfeedback) {
4451 echo $OUTPUT->notification($strdeleted.get_string('questions', 'question'), 'notifysuccess');
4454 // Make sure there are no subcontexts left - all valid blocks and modules should be already gone
4455 $childcontexts = $coursecontext->get_child_contexts(); // returns all subcontexts since 2.2
4456 foreach ($childcontexts as $childcontext) {
4457 $childcontext->delete();
4459 unset($childcontexts);
4461 // Remove all roles and enrolments by default
4462 if (empty($options['keep_roles_and_enrolments'])) {
4463 // this hack is used in restore when deleting contents of existing course
4464 role_unassign_all(array('contextid'=>$coursecontext->id), true);
4465 enrol_course_delete($course);
4466 if ($showfeedback) {
4467 echo $OUTPUT->notification($strdeleted.get_string('type_enrol_plural', 'plugin'), 'notifysuccess');
4471 // Delete any groups, removing members and grouping/course links first.
4472 if (empty($options['keep_groups_and_groupings'])) {
4473 groups_delete_groupings($course->id, $showfeedback);
4474 groups_delete_groups($course->id, $showfeedback);
4477 // filters be gone!
4478 filter_delete_all_for_context($coursecontext->id);
4480 // die comments!
4481 comment::delete_comments($coursecontext->id);
4483 // ratings are history too
4484 $delopt = new stdclass();
4485 $delopt->contextid = $coursecontext->id;
4486 $rm = new rating_manager();
4487 $rm->delete_ratings($delopt);
4489 // Delete course tags
4490 coursetag_delete_course_tags($course->id, $showfeedback);
4492 // Delete calendar events
4493 $DB->delete_records('event', array('courseid'=>$course->id));
4494 $fs->delete_area_files($coursecontext->id, 'calendar');
4496 // Delete all related records in other core tables that may have a courseid
4497 // This array stores the tables that need to be cleared, as
4498 // table_name => column_name that contains the course id.
4499 $tablestoclear = array(
4500 'log' => 'course', // Course logs (NOTE: this might be changed in the future)
4501 'backup_courses' => 'courseid', // Scheduled backup stuff
4502 'user_lastaccess' => 'courseid', // User access info
4504 foreach ($tablestoclear as $table => $col) {
4505 $DB->delete_records($table, array($col=>$course->id));
4508 // delete all course backup files
4509 $fs->delete_area_files($coursecontext->id, 'backup');
4511 // cleanup course record - remove links to deleted stuff
4512 $oldcourse = new stdClass();
4513 $oldcourse->id = $course->id;
4514 $oldcourse->summary = '';
4515 $oldcourse->modinfo = NULL;
4516 $oldcourse->legacyfiles = 0;
4517 $oldcourse->enablecompletion = 0;
4518 if (!empty($options['keep_groups_and_groupings'])) {
4519 $oldcourse->defaultgroupingid = 0;
4521 $DB->update_record('course', $oldcourse);
4523 // Delete course sections and user selections
4524 $DB->delete_records('course_sections', array('course'=>$course->id));
4525 $DB->delete_records('course_display', array('course'=>$course->id));
4527 // delete legacy, section and any other course files
4528 $fs->delete_area_files($coursecontext->id, 'course'); // files from summary and section
4530 // Delete all remaining stuff linked to context such as files, comments, ratings, etc.
4531 if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) {
4532 // Easy, do not delete the context itself...
4533 $coursecontext->delete_content();
4535 } else {
4536 // Hack alert!!!!
4537 // We can not drop all context stuff because it would bork enrolments and roles,
4538 // there might be also files used by enrol plugins...
4541 // Delete legacy files - just in case some files are still left there after conversion to new file api,
4542 // also some non-standard unsupported plugins may try to store something there
4543 fulldelete($CFG->dataroot.'/'.$course->id);
4545 // Finally trigger the event
4546 $course->context = $coursecontext; // you can not access context in cron event later after course is deleted
4547 $course->options = $options; // not empty if we used any crazy hack
4548 events_trigger('course_content_removed', $course);
4550 return true;
4554 * Change dates in module - used from course reset.
4556 * @global object
4557 * @global object
4558 * @param string $modname forum, assignment, etc
4559 * @param array $fields array of date fields from mod table
4560 * @param int $timeshift time difference
4561 * @param int $courseid
4562 * @return bool success
4564 function shift_course_mod_dates($modname, $fields, $timeshift, $courseid) {
4565 global $CFG, $DB;
4566 include_once($CFG->dirroot.'/mod/'.$modname.'/lib.php');
4568 $return = true;
4569 foreach ($fields as $field) {
4570 $updatesql = "UPDATE {".$modname."}
4571 SET $field = $field + ?
4572 WHERE course=? AND $field<>0 AND $field<>0";
4573 $return = $DB->execute($updatesql, array($timeshift, $courseid)) && $return;
4576 $refreshfunction = $modname.'_refresh_events';
4577 if (function_exists($refreshfunction)) {
4578 $refreshfunction($courseid);
4581 return $return;
4585 * This function will empty a course of user data.
4586 * It will retain the activities and the structure of the course.
4588 * @param object $data an object containing all the settings including courseid (without magic quotes)
4589 * @return array status array of array component, item, error
4591 function reset_course_userdata($data) {
4592 global $CFG, $USER, $DB;
4593 require_once($CFG->libdir.'/gradelib.php');
4594 require_once($CFG->libdir.'/completionlib.php');
4595 require_once($CFG->dirroot.'/group/lib.php');
4597 $data->courseid = $data->id;
4598 $context = get_context_instance(CONTEXT_COURSE, $data->courseid);
4600 // calculate the time shift of dates
4601 if (!empty($data->reset_start_date)) {
4602 // time part of course startdate should be zero
4603 $data->timeshift = $data->reset_start_date - usergetmidnight($data->reset_start_date_old);
4604 } else {
4605 $data->timeshift = 0;
4608 // result array: component, item, error
4609 $status = array();
4611 // start the resetting
4612 $componentstr = get_string('general');
4614 // move the course start time
4615 if (!empty($data->reset_start_date) and $data->timeshift) {
4616 // change course start data
4617 $DB->set_field('course', 'startdate', $data->reset_start_date, array('id'=>$data->courseid));
4618 // update all course and group events - do not move activity events
4619 $updatesql = "UPDATE {event}
4620 SET timestart = timestart + ?
4621 WHERE courseid=? AND instance=0";
4622 $DB->execute($updatesql, array($data->timeshift, $data->courseid));
4624 $status[] = array('component'=>$componentstr, 'item'=>get_string('datechanged'), 'error'=>false);
4627 if (!empty($data->reset_logs)) {
4628 $DB->delete_records('log', array('course'=>$data->courseid));
4629 $status[] = array('component'=>$componentstr, 'item'=>get_string('deletelogs'), 'error'=>false);
4632 if (!empty($data->reset_events)) {
4633 $DB->delete_records('event', array('courseid'=>$data->courseid));
4634 $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteevents', 'calendar'), 'error'=>false);
4637 if (!empty($data->reset_notes)) {
4638 require_once($CFG->dirroot.'/notes/lib.php');
4639 note_delete_all($data->courseid);
4640 $status[] = array('component'=>$componentstr, 'item'=>get_string('deletenotes', 'notes'), 'error'=>false);
4643 if (!empty($data->delete_blog_associations)) {
4644 require_once($CFG->dirroot.'/blog/lib.php');
4645 blog_remove_associations_for_course($data->courseid);
4646 $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteblogassociations', 'blog'), 'error'=>false);
4649 if (!empty($data->reset_course_completion)) {
4650 // Delete course completion information
4651 $course = $DB->get_record('course', array('id'=>$data->courseid));
4652 $cc = new completion_info($course);
4653 $cc->delete_course_completion_data();
4654 $status[] = array('component'=>$componentstr, 'item'=>get_string('deletecoursecompletiondata', 'completion'), 'error'=>false);
4657 $componentstr = get_string('roles');
4659 if (!empty($data->reset_roles_overrides)) {
4660 $children = get_child_contexts($context);
4661 foreach ($children as $child) {
4662 $DB->delete_records('role_capabilities', array('contextid'=>$child->id));
4664 $DB->delete_records('role_capabilities', array('contextid'=>$context->id));
4665 //force refresh for logged in users
4666 mark_context_dirty($context->path);
4667 $status[] = array('component'=>$componentstr, 'item'=>get_string('deletecourseoverrides', 'role'), 'error'=>false);
4670 if (!empty($data->reset_roles_local)) {
4671 $children = get_child_contexts($context);
4672 foreach ($children as $child) {
4673 role_unassign_all(array('contextid'=>$child->id));
4675 //force refresh for logged in users
4676 mark_context_dirty($context->path);
4677 $status[] = array('component'=>$componentstr, 'item'=>get_string('deletelocalroles', 'role'), 'error'=>false);
4680 // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc.
4681 $data->unenrolled = array();
4682 if (!empty($data->unenrol_users)) {
4683 $plugins = enrol_get_plugins(true);
4684 $instances = enrol_get_instances($data->courseid, true);
4685 foreach ($instances as $key=>$instance) {
4686 if (!isset($plugins[$instance->enrol])) {
4687 unset($instances[$key]);
4688 continue;
4690 if (!$plugins[$instance->enrol]->allow_unenrol($instance)) {
4691 unset($instances[$key]);
4695 $sqlempty = $DB->sql_empty();
4696 foreach($data->unenrol_users as $withroleid) {
4697 $sql = "SELECT DISTINCT ue.userid, ue.enrolid
4698 FROM {user_enrolments} ue
4699 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
4700 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
4701 JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)";
4702 $params = array('courseid'=>$data->courseid, 'roleid'=>$withroleid, 'courselevel'=>CONTEXT_COURSE);
4704 $rs = $DB->get_recordset_sql($sql, $params);
4705 foreach ($rs as $ue) {
4706 if (!isset($instances[$ue->enrolid])) {
4707 continue;
4709 $plugins[$instances[$ue->enrolid]->enrol]->unenrol_user($instances[$ue->enrolid], $ue->userid);
4710 $data->unenrolled[$ue->userid] = $ue->userid;
4714 if (!empty($data->unenrolled)) {
4715 $status[] = array('component'=>$componentstr, 'item'=>get_string('unenrol', 'enrol').' ('.count($data->unenrolled).')', 'error'=>false);
4719 $componentstr = get_string('groups');
4721 // remove all group members
4722 if (!empty($data->reset_groups_members)) {
4723 groups_delete_group_members($data->courseid);
4724 $status[] = array('component'=>$componentstr, 'item'=>get_string('removegroupsmembers', 'group'), 'error'=>false);
4727 // remove all groups
4728 if (!empty($data->reset_groups_remove)) {
4729 groups_delete_groups($data->courseid, false);
4730 $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteallgroups', 'group'), 'error'=>false);
4733 // remove all grouping members
4734 if (!empty($data->reset_groupings_members)) {
4735 groups_delete_groupings_groups($data->courseid, false);
4736 $status[] = array('component'=>$componentstr, 'item'=>get_string('removegroupingsmembers', 'group'), 'error'=>false);
4739 // remove all groupings
4740 if (!empty($data->reset_groupings_remove)) {
4741 groups_delete_groupings($data->courseid, false);
4742 $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteallgroupings', 'group'), 'error'=>false);
4745 // Look in every instance of every module for data to delete
4746 $unsupported_mods = array();
4747 if ($allmods = $DB->get_records('modules') ) {
4748 foreach ($allmods as $mod) {
4749 $modname = $mod->name;
4750 if (!$DB->count_records($modname, array('course'=>$data->courseid))) {
4751 continue; // skip mods with no instances
4753 $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php';
4754 $moddeleteuserdata = $modname.'_reset_userdata'; // Function to delete user data
4755 if (file_exists($modfile)) {
4756 include_once($modfile);
4757 if (function_exists($moddeleteuserdata)) {
4758 $modstatus = $moddeleteuserdata($data);
4759 if (is_array($modstatus)) {
4760 $status = array_merge($status, $modstatus);
4761 } else {
4762 debugging('Module '.$modname.' returned incorrect staus - must be an array!');
4764 } else {
4765 $unsupported_mods[] = $mod;
4767 } else {
4768 debugging('Missing lib.php in '.$modname.' module!');
4773 // mention unsupported mods
4774 if (!empty($unsupported_mods)) {
4775 foreach($unsupported_mods as $mod) {
4776 $status[] = array('component'=>get_string('modulenameplural', $mod->name), 'item'=>'', 'error'=>get_string('resetnotimplemented'));
4781 $componentstr = get_string('gradebook', 'grades');
4782 // reset gradebook
4783 if (!empty($data->reset_gradebook_items)) {
4784 remove_course_grades($data->courseid, false);
4785 grade_grab_course_grades($data->courseid);
4786 grade_regrade_final_grades($data->courseid);
4787 $status[] = array('component'=>$componentstr, 'item'=>get_string('removeallcourseitems', 'grades'), 'error'=>false);
4789 } else if (!empty($data->reset_gradebook_grades)) {
4790 grade_course_reset($data->courseid);
4791 $status[] = array('component'=>$componentstr, 'item'=>get_string('removeallcoursegrades', 'grades'), 'error'=>false);
4793 // reset comments
4794 if (!empty($data->reset_comments)) {
4795 require_once($CFG->dirroot.'/comment/lib.php');
4796 comment::reset_course_page_comments($context);
4799 return $status;
4803 * Generate an email processing address
4805 * @param int $modid
4806 * @param string $modargs
4807 * @return string Returns email processing address
4809 function generate_email_processing_address($modid,$modargs) {
4810 global $CFG;
4812 $header = $CFG->mailprefix . substr(base64_encode(pack('C',$modid)),0,2).$modargs;
4813 return $header . substr(md5($header.get_site_identifier()),0,16).'@'.$CFG->maildomain;
4819 * @todo Finish documenting this function
4821 * @global object
4822 * @param string $modargs
4823 * @param string $body Currently unused
4825 function moodle_process_email($modargs,$body) {
4826 global $DB;
4828 // the first char should be an unencoded letter. We'll take this as an action
4829 switch ($modargs{0}) {
4830 case 'B': { // bounce
4831 list(,$userid) = unpack('V',base64_decode(substr($modargs,1,8)));
4832 if ($user = $DB->get_record("user", array('id'=>$userid), "id,email")) {
4833 // check the half md5 of their email
4834 $md5check = substr(md5($user->email),0,16);
4835 if ($md5check == substr($modargs, -16)) {
4836 set_bounce_count($user);
4838 // else maybe they've already changed it?
4841 break;
4842 // maybe more later?
4846 /// CORRESPONDENCE ////////////////////////////////////////////////
4849 * Get mailer instance, enable buffering, flush buffer or disable buffering.
4851 * @global object
4852 * @param string $action 'get', 'buffer', 'close' or 'flush'
4853 * @return object|null mailer instance if 'get' used or nothing
4855 function get_mailer($action='get') {
4856 global $CFG;
4858 static $mailer = null;
4859 static $counter = 0;
4861 if (!isset($CFG->smtpmaxbulk)) {
4862 $CFG->smtpmaxbulk = 1;
4865 if ($action == 'get') {
4866 $prevkeepalive = false;
4868 if (isset($mailer) and $mailer->Mailer == 'smtp') {
4869 if ($counter < $CFG->smtpmaxbulk and !$mailer->IsError()) {
4870 $counter++;
4871 // reset the mailer
4872 $mailer->Priority = 3;
4873 $mailer->CharSet = 'UTF-8'; // our default
4874 $mailer->ContentType = "text/plain";
4875 $mailer->Encoding = "8bit";
4876 $mailer->From = "root@localhost";
4877 $mailer->FromName = "Root User";
4878 $mailer->Sender = "";
4879 $mailer->Subject = "";
4880 $mailer->Body = "";
4881 $mailer->AltBody = "";
4882 $mailer->ConfirmReadingTo = "";
4884 $mailer->ClearAllRecipients();
4885 $mailer->ClearReplyTos();
4886 $mailer->ClearAttachments();
4887 $mailer->ClearCustomHeaders();
4888 return $mailer;
4891 $prevkeepalive = $mailer->SMTPKeepAlive;
4892 get_mailer('flush');
4895 include_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php');
4896 $mailer = new moodle_phpmailer();
4898 $counter = 1;
4900 $mailer->Version = 'Moodle '.$CFG->version; // mailer version
4901 $mailer->PluginDir = $CFG->libdir.'/phpmailer/'; // plugin directory (eg smtp plugin)
4902 $mailer->CharSet = 'UTF-8';
4904 // some MTAs may do double conversion of LF if CRLF used, CRLF is required line ending in RFC 822bis
4905 if (isset($CFG->mailnewline) and $CFG->mailnewline == 'CRLF') {
4906 $mailer->LE = "\r\n";
4907 } else {
4908 $mailer->LE = "\n";
4911 if ($CFG->smtphosts == 'qmail') {
4912 $mailer->IsQmail(); // use Qmail system
4914 } else if (empty($CFG->smtphosts)) {
4915 $mailer->IsMail(); // use PHP mail() = sendmail
4917 } else {
4918 $mailer->IsSMTP(); // use SMTP directly
4919 if (!empty($CFG->debugsmtp)) {
4920 $mailer->SMTPDebug = true;
4922 $mailer->Host = $CFG->smtphosts; // specify main and backup servers
4923 $mailer->SMTPKeepAlive = $prevkeepalive; // use previous keepalive
4925 if ($CFG->smtpuser) { // Use SMTP authentication
4926 $mailer->SMTPAuth = true;
4927 $mailer->Username = $CFG->smtpuser;
4928 $mailer->Password = $CFG->smtppass;
4932 return $mailer;
4935 $nothing = null;
4937 // keep smtp session open after sending
4938 if ($action == 'buffer') {
4939 if (!empty($CFG->smtpmaxbulk)) {
4940 get_mailer('flush');
4941 $m = get_mailer();
4942 if ($m->Mailer == 'smtp') {
4943 $m->SMTPKeepAlive = true;
4946 return $nothing;
4949 // close smtp session, but continue buffering
4950 if ($action == 'flush') {
4951 if (isset($mailer) and $mailer->Mailer == 'smtp') {
4952 if (!empty($mailer->SMTPDebug)) {
4953 echo '<pre>'."\n";
4955 $mailer->SmtpClose();
4956 if (!empty($mailer->SMTPDebug)) {
4957 echo '</pre>';
4960 return $nothing;
4963 // close smtp session, do not buffer anymore
4964 if ($action == 'close') {
4965 if (isset($mailer) and $mailer->Mailer == 'smtp') {
4966 get_mailer('flush');
4967 $mailer->SMTPKeepAlive = false;
4969 $mailer = null; // better force new instance
4970 return $nothing;
4975 * Send an email to a specified user
4977 * @global object
4978 * @global string
4979 * @global string IdentityProvider(IDP) URL user hits to jump to mnet peer.
4980 * @uses SITEID
4981 * @param stdClass $user A {@link $USER} object
4982 * @param stdClass $from A {@link $USER} object
4983 * @param string $subject plain text subject line of the email
4984 * @param string $messagetext plain text version of the message
4985 * @param string $messagehtml complete html version of the message (optional)
4986 * @param string $attachment a file on the filesystem, relative to $CFG->dataroot
4987 * @param string $attachname the name of the file (extension indicates MIME)
4988 * @param bool $usetrueaddress determines whether $from email address should
4989 * be sent out. Will be overruled by user profile setting for maildisplay
4990 * @param string $replyto Email address to reply to
4991 * @param string $replytoname Name of reply to recipient
4992 * @param int $wordwrapwidth custom word wrap width, default 79
4993 * @return bool Returns true if mail was sent OK and false if there was an error.
4995 function email_to_user($user, $from, $subject, $messagetext, $messagehtml='', $attachment='', $attachname='', $usetrueaddress=true, $replyto='', $replytoname='', $wordwrapwidth=79) {
4997 global $CFG, $FULLME;
4999 if (empty($user) || empty($user->email)) {
5000 mtrace('Error: lib/moodlelib.php email_to_user(): User is null or has no email');
5001 return false;
5004 if (!empty($user->deleted)) {
5005 // do not mail delted users
5006 mtrace('Error: lib/moodlelib.php email_to_user(): User is deleted');
5007 return false;
5010 if (!empty($CFG->noemailever)) {
5011 // hidden setting for development sites, set in config.php if needed
5012 mtrace('Error: lib/moodlelib.php email_to_user(): Not sending email due to noemailever config setting');
5013 return true;
5016 if (!empty($CFG->divertallemailsto)) {
5017 $subject = "[DIVERTED {$user->email}] $subject";
5018 $user = clone($user);
5019 $user->email = $CFG->divertallemailsto;
5022 // skip mail to suspended users
5023 if ((isset($user->auth) && $user->auth=='nologin') or (isset($user->suspended) && $user->suspended)) {
5024 return true;
5027 if (!validate_email($user->email)) {
5028 // we can not send emails to invalid addresses - it might create security issue or confuse the mailer
5029 $invalidemail = "User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending.";
5030 error_log($invalidemail);
5031 if (CLI_SCRIPT) {
5032 // do not print this in standard web pages
5033 mtrace($invalidemail);
5035 return false;
5038 if (over_bounce_threshold($user)) {
5039 $bouncemsg = "User $user->id (".fullname($user).") is over bounce threshold! Not sending.";
5040 error_log($bouncemsg);
5041 mtrace('Error: lib/moodlelib.php email_to_user(): '.$bouncemsg);
5042 return false;
5045 // If the user is a remote mnet user, parse the email text for URL to the
5046 // wwwroot and modify the url to direct the user's browser to login at their
5047 // home site (identity provider - idp) before hitting the link itself
5048 if (is_mnet_remote_user($user)) {
5049 require_once($CFG->dirroot.'/mnet/lib.php');
5051 $jumpurl = mnet_get_idp_jump_url($user);
5052 $callback = partial('mnet_sso_apply_indirection', $jumpurl);
5054 $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%",
5055 $callback,
5056 $messagetext);
5057 $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%",
5058 $callback,
5059 $messagehtml);
5061 $mail = get_mailer();
5063 if (!empty($mail->SMTPDebug)) {
5064 echo '<pre>' . "\n";
5067 $temprecipients = array();
5068 $tempreplyto = array();
5070 $supportuser = generate_email_supportuser();
5072 // make up an email address for handling bounces
5073 if (!empty($CFG->handlebounces)) {
5074 $modargs = 'B'.base64_encode(pack('V',$user->id)).substr(md5($user->email),0,16);
5075 $mail->Sender = generate_email_processing_address(0,$modargs);
5076 } else {
5077 $mail->Sender = $supportuser->email;
5080 if (is_string($from)) { // So we can pass whatever we want if there is need
5081 $mail->From = $CFG->noreplyaddress;
5082 $mail->FromName = $from;
5083 } else if ($usetrueaddress and $from->maildisplay) {
5084 $mail->From = $from->email;
5085 $mail->FromName = fullname($from);
5086 } else {
5087 $mail->From = $CFG->noreplyaddress;
5088 $mail->FromName = fullname($from);
5089 if (empty($replyto)) {
5090 $tempreplyto[] = array($CFG->noreplyaddress, get_string('noreplyname'));
5094 if (!empty($replyto)) {
5095 $tempreplyto[] = array($replyto, $replytoname);
5098 $mail->Subject = substr($subject, 0, 900);
5100 $temprecipients[] = array($user->email, fullname($user));
5102 $mail->WordWrap = $wordwrapwidth; // set word wrap
5104 if (!empty($from->customheaders)) { // Add custom headers
5105 if (is_array($from->customheaders)) {
5106 foreach ($from->customheaders as $customheader) {
5107 $mail->AddCustomHeader($customheader);
5109 } else {
5110 $mail->AddCustomHeader($from->customheaders);
5114 if (!empty($from->priority)) {
5115 $mail->Priority = $from->priority;
5118 if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) { // Don't ever send HTML to users who don't want it
5119 $mail->IsHTML(true);
5120 $mail->Encoding = 'quoted-printable'; // Encoding to use
5121 $mail->Body = $messagehtml;
5122 $mail->AltBody = "\n$messagetext\n";
5123 } else {
5124 $mail->IsHTML(false);
5125 $mail->Body = "\n$messagetext\n";
5128 if ($attachment && $attachname) {
5129 if (preg_match( "~\\.\\.~" ,$attachment )) { // Security check for ".." in dir path
5130 $temprecipients[] = array($supportuser->email, fullname($supportuser, true));
5131 $mail->AddStringAttachment('Error in attachment. User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain');
5132 } else {
5133 require_once($CFG->libdir.'/filelib.php');
5134 $mimetype = mimeinfo('type', $attachname);
5135 $mail->AddAttachment($CFG->dataroot .'/'. $attachment, $attachname, 'base64', $mimetype);
5139 // Check if the email should be sent in an other charset then the default UTF-8
5140 if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) {
5142 // use the defined site mail charset or eventually the one preferred by the recipient
5143 $charset = $CFG->sitemailcharset;
5144 if (!empty($CFG->allowusermailcharset)) {
5145 if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) {
5146 $charset = $useremailcharset;
5150 // convert all the necessary strings if the charset is supported
5151 $charsets = get_list_of_charsets();
5152 unset($charsets['UTF-8']);
5153 if (in_array($charset, $charsets)) {
5154 $textlib = textlib_get_instance();
5155 $mail->CharSet = $charset;
5156 $mail->FromName = $textlib->convert($mail->FromName, 'utf-8', strtolower($charset));
5157 $mail->Subject = $textlib->convert($mail->Subject, 'utf-8', strtolower($charset));
5158 $mail->Body = $textlib->convert($mail->Body, 'utf-8', strtolower($charset));
5159 $mail->AltBody = $textlib->convert($mail->AltBody, 'utf-8', strtolower($charset));
5161 foreach ($temprecipients as $key => $values) {
5162 $temprecipients[$key][1] = $textlib->convert($values[1], 'utf-8', strtolower($charset));
5164 foreach ($tempreplyto as $key => $values) {
5165 $tempreplyto[$key][1] = $textlib->convert($values[1], 'utf-8', strtolower($charset));
5170 foreach ($temprecipients as $values) {
5171 $mail->AddAddress($values[0], $values[1]);
5173 foreach ($tempreplyto as $values) {
5174 $mail->AddReplyTo($values[0], $values[1]);
5177 if ($mail->Send()) {
5178 set_send_count($user);
5179 $mail->IsSMTP(); // use SMTP directly
5180 if (!empty($mail->SMTPDebug)) {
5181 echo '</pre>';
5183 return true;
5184 } else {
5185 mtrace('ERROR: '. $mail->ErrorInfo);
5186 add_to_log(SITEID, 'library', 'mailer', $FULLME, 'ERROR: '. $mail->ErrorInfo);
5187 if (!empty($mail->SMTPDebug)) {
5188 echo '</pre>';
5190 return false;
5195 * Generate a signoff for emails based on support settings
5197 * @global object
5198 * @return string
5200 function generate_email_signoff() {
5201 global $CFG;
5203 $signoff = "\n";
5204 if (!empty($CFG->supportname)) {
5205 $signoff .= $CFG->supportname."\n";
5207 if (!empty($CFG->supportemail)) {
5208 $signoff .= $CFG->supportemail."\n";
5210 if (!empty($CFG->supportpage)) {
5211 $signoff .= $CFG->supportpage."\n";
5213 return $signoff;
5217 * Generate a fake user for emails based on support settings
5218 * @global object
5219 * @return object user info
5221 function generate_email_supportuser() {
5222 global $CFG;
5224 static $supportuser;
5226 if (!empty($supportuser)) {
5227 return $supportuser;
5230 $supportuser = new stdClass();
5231 $supportuser->email = $CFG->supportemail ? $CFG->supportemail : $CFG->noreplyaddress;
5232 $supportuser->firstname = $CFG->supportname ? $CFG->supportname : get_string('noreplyname');
5233 $supportuser->lastname = '';
5234 $supportuser->maildisplay = true;
5236 return $supportuser;
5241 * Sets specified user's password and send the new password to the user via email.
5243 * @global object
5244 * @global object
5245 * @param user $user A {@link $USER} object
5246 * @return boolean|string Returns "true" if mail was sent OK and "false" if there was an error
5248 function setnew_password_and_mail($user) {
5249 global $CFG, $DB;
5251 $site = get_site();
5253 $supportuser = generate_email_supportuser();
5255 $newpassword = generate_password();
5257 $DB->set_field('user', 'password', hash_internal_user_password($newpassword), array('id'=>$user->id));
5259 $a = new stdClass();
5260 $a->firstname = fullname($user, true);
5261 $a->sitename = format_string($site->fullname);
5262 $a->username = $user->username;
5263 $a->newpassword = $newpassword;
5264 $a->link = $CFG->wwwroot .'/login/';
5265 $a->signoff = generate_email_signoff();
5267 $message = get_string('newusernewpasswordtext', '', $a);
5269 $subject = format_string($site->fullname) .': '. get_string('newusernewpasswordsubj');
5271 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5272 return email_to_user($user, $supportuser, $subject, $message);
5277 * Resets specified user's password and send the new password to the user via email.
5279 * @param stdClass $user A {@link $USER} object
5280 * @return bool Returns true if mail was sent OK and false if there was an error.
5282 function reset_password_and_mail($user) {
5283 global $CFG;
5285 $site = get_site();
5286 $supportuser = generate_email_supportuser();
5288 $userauth = get_auth_plugin($user->auth);
5289 if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) {
5290 trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth.");
5291 return false;
5294 $newpassword = generate_password();
5296 if (!$userauth->user_update_password($user, $newpassword)) {
5297 print_error("cannotsetpassword");
5300 $a = new stdClass();
5301 $a->firstname = $user->firstname;
5302 $a->lastname = $user->lastname;
5303 $a->sitename = format_string($site->fullname);
5304 $a->username = $user->username;
5305 $a->newpassword = $newpassword;
5306 $a->link = $CFG->httpswwwroot .'/login/change_password.php';
5307 $a->signoff = generate_email_signoff();
5309 $message = get_string('newpasswordtext', '', $a);
5311 $subject = format_string($site->fullname) .': '. get_string('changedpassword');
5313 unset_user_preference('create_password', $user); // prevent cron from generating the password
5315 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5316 return email_to_user($user, $supportuser, $subject, $message);
5321 * Send email to specified user with confirmation text and activation link.
5323 * @global object
5324 * @param user $user A {@link $USER} object
5325 * @return bool Returns true if mail was sent OK and false if there was an error.
5327 function send_confirmation_email($user) {
5328 global $CFG;
5330 $site = get_site();
5331 $supportuser = generate_email_supportuser();
5333 $data = new stdClass();
5334 $data->firstname = fullname($user);
5335 $data->sitename = format_string($site->fullname);
5336 $data->admin = generate_email_signoff();
5338 $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname));
5340 $data->link = $CFG->wwwroot .'/login/confirm.php?data='. $user->secret .'/'. urlencode($user->username);
5341 $message = get_string('emailconfirmation', '', $data);
5342 $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true);
5344 $user->mailformat = 1; // Always send HTML version as well
5346 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5347 return email_to_user($user, $supportuser, $subject, $message, $messagehtml);
5352 * send_password_change_confirmation_email.
5354 * @global object
5355 * @param user $user A {@link $USER} object
5356 * @return bool Returns true if mail was sent OK and false if there was an error.
5358 function send_password_change_confirmation_email($user) {
5359 global $CFG;
5361 $site = get_site();
5362 $supportuser = generate_email_supportuser();
5364 $data = new stdClass();
5365 $data->firstname = $user->firstname;
5366 $data->lastname = $user->lastname;
5367 $data->sitename = format_string($site->fullname);
5368 $data->link = $CFG->httpswwwroot .'/login/forgot_password.php?p='. $user->secret .'&s='. urlencode($user->username);
5369 $data->admin = generate_email_signoff();
5371 $message = get_string('emailpasswordconfirmation', '', $data);
5372 $subject = get_string('emailpasswordconfirmationsubject', '', format_string($site->fullname));
5374 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5375 return email_to_user($user, $supportuser, $subject, $message);
5380 * send_password_change_info.
5382 * @global object
5383 * @param user $user A {@link $USER} object
5384 * @return bool Returns true if mail was sent OK and false if there was an error.
5386 function send_password_change_info($user) {
5387 global $CFG;
5389 $site = get_site();
5390 $supportuser = generate_email_supportuser();
5391 $systemcontext = get_context_instance(CONTEXT_SYSTEM);
5393 $data = new stdClass();
5394 $data->firstname = $user->firstname;
5395 $data->lastname = $user->lastname;
5396 $data->sitename = format_string($site->fullname);
5397 $data->admin = generate_email_signoff();
5399 $userauth = get_auth_plugin($user->auth);
5401 if (!is_enabled_auth($user->auth) or $user->auth == 'nologin') {
5402 $message = get_string('emailpasswordchangeinfodisabled', '', $data);
5403 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
5404 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5405 return email_to_user($user, $supportuser, $subject, $message);
5408 if ($userauth->can_change_password() and $userauth->change_password_url()) {
5409 // we have some external url for password changing
5410 $data->link .= $userauth->change_password_url();
5412 } else {
5413 //no way to change password, sorry
5414 $data->link = '';
5417 if (!empty($data->link) and has_capability('moodle/user:changeownpassword', $systemcontext, $user->id)) {
5418 $message = get_string('emailpasswordchangeinfo', '', $data);
5419 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
5420 } else {
5421 $message = get_string('emailpasswordchangeinfofail', '', $data);
5422 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
5425 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5426 return email_to_user($user, $supportuser, $subject, $message);
5431 * Check that an email is allowed. It returns an error message if there
5432 * was a problem.
5434 * @global object
5435 * @param string $email Content of email
5436 * @return string|false
5438 function email_is_not_allowed($email) {
5439 global $CFG;
5441 if (!empty($CFG->allowemailaddresses)) {
5442 $allowed = explode(' ', $CFG->allowemailaddresses);
5443 foreach ($allowed as $allowedpattern) {
5444 $allowedpattern = trim($allowedpattern);
5445 if (!$allowedpattern) {
5446 continue;
5448 if (strpos($allowedpattern, '.') === 0) {
5449 if (strpos(strrev($email), strrev($allowedpattern)) === 0) {
5450 // subdomains are in a form ".example.com" - matches "xxx@anything.example.com"
5451 return false;
5454 } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) { // Match! (bug 5250)
5455 return false;
5458 return get_string('emailonlyallowed', '', $CFG->allowemailaddresses);
5460 } else if (!empty($CFG->denyemailaddresses)) {
5461 $denied = explode(' ', $CFG->denyemailaddresses);
5462 foreach ($denied as $deniedpattern) {
5463 $deniedpattern = trim($deniedpattern);
5464 if (!$deniedpattern) {
5465 continue;
5467 if (strpos($deniedpattern, '.') === 0) {
5468 if (strpos(strrev($email), strrev($deniedpattern)) === 0) {
5469 // subdomains are in a form ".example.com" - matches "xxx@anything.example.com"
5470 return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
5473 } else if (strpos(strrev($email), strrev('@'.$deniedpattern)) === 0) { // Match! (bug 5250)
5474 return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
5479 return false;
5482 /// FILE HANDLING /////////////////////////////////////////////
5485 * Returns local file storage instance
5487 * @return file_storage
5489 function get_file_storage() {
5490 global $CFG;
5492 static $fs = null;
5494 if ($fs) {
5495 return $fs;
5498 require_once("$CFG->libdir/filelib.php");
5500 if (isset($CFG->filedir)) {
5501 $filedir = $CFG->filedir;
5502 } else {
5503 $filedir = $CFG->dataroot.'/filedir';
5506 if (isset($CFG->trashdir)) {
5507 $trashdirdir = $CFG->trashdir;
5508 } else {
5509 $trashdirdir = $CFG->dataroot.'/trashdir';
5512 $fs = new file_storage($filedir, $trashdirdir, "$CFG->tempdir/filestorage", $CFG->directorypermissions, $CFG->filepermissions);
5514 return $fs;
5518 * Returns local file storage instance
5520 * @return file_browser
5522 function get_file_browser() {
5523 global $CFG;
5525 static $fb = null;
5527 if ($fb) {
5528 return $fb;
5531 require_once("$CFG->libdir/filelib.php");
5533 $fb = new file_browser();
5535 return $fb;
5539 * Returns file packer
5541 * @param string $mimetype default application/zip
5542 * @return file_packer
5544 function get_file_packer($mimetype='application/zip') {
5545 global $CFG;
5547 static $fp = array();;
5549 if (isset($fp[$mimetype])) {
5550 return $fp[$mimetype];
5553 switch ($mimetype) {
5554 case 'application/zip':
5555 case 'application/vnd.moodle.backup':
5556 $classname = 'zip_packer';
5557 break;
5558 case 'application/x-tar':
5559 // $classname = 'tar_packer';
5560 // break;
5561 default:
5562 return false;
5565 require_once("$CFG->libdir/filestorage/$classname.php");
5566 $fp[$mimetype] = new $classname();
5568 return $fp[$mimetype];
5572 * Returns current name of file on disk if it exists.
5574 * @param string $newfile File to be verified
5575 * @return string Current name of file on disk if true
5577 function valid_uploaded_file($newfile) {
5578 if (empty($newfile)) {
5579 return '';
5581 if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) {
5582 return $newfile['tmp_name'];
5583 } else {
5584 return '';
5589 * Returns the maximum size for uploading files.
5591 * There are seven possible upload limits:
5592 * 1. in Apache using LimitRequestBody (no way of checking or changing this)
5593 * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
5594 * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
5595 * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
5596 * 5. by the Moodle admin in $CFG->maxbytes
5597 * 6. by the teacher in the current course $course->maxbytes
5598 * 7. by the teacher for the current module, eg $assignment->maxbytes
5600 * These last two are passed to this function as arguments (in bytes).
5601 * Anything defined as 0 is ignored.
5602 * The smallest of all the non-zero numbers is returned.
5604 * @todo Finish documenting this function
5606 * @param int $sizebytes Set maximum size
5607 * @param int $coursebytes Current course $course->maxbytes (in bytes)
5608 * @param int $modulebytes Current module ->maxbytes (in bytes)
5609 * @return int The maximum size for uploading files.
5611 function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0) {
5613 if (! $filesize = ini_get('upload_max_filesize')) {
5614 $filesize = '5M';
5616 $minimumsize = get_real_size($filesize);
5618 if ($postsize = ini_get('post_max_size')) {
5619 $postsize = get_real_size($postsize);
5620 if ($postsize < $minimumsize) {
5621 $minimumsize = $postsize;
5625 if ($sitebytes and $sitebytes < $minimumsize) {
5626 $minimumsize = $sitebytes;
5629 if ($coursebytes and $coursebytes < $minimumsize) {
5630 $minimumsize = $coursebytes;
5633 if ($modulebytes and $modulebytes < $minimumsize) {
5634 $minimumsize = $modulebytes;
5637 return $minimumsize;
5641 * Returns an array of possible sizes in local language
5643 * Related to {@link get_max_upload_file_size()} - this function returns an
5644 * array of possible sizes in an array, translated to the
5645 * local language.
5647 * @todo Finish documenting this function
5649 * @global object
5650 * @uses SORT_NUMERIC
5651 * @param int $sizebytes Set maximum size
5652 * @param int $coursebytes Current course $course->maxbytes (in bytes)
5653 * @param int $modulebytes Current module ->maxbytes (in bytes)
5654 * @return array
5656 function get_max_upload_sizes($sitebytes=0, $coursebytes=0, $modulebytes=0) {
5657 global $CFG;
5659 if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) {
5660 return array();
5663 $filesize[intval($maxsize)] = display_size($maxsize);
5665 $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152,
5666 5242880, 10485760, 20971520, 52428800, 104857600);
5668 // Allow maxbytes to be selected if it falls outside the above boundaries
5669 if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) {
5670 // note: get_real_size() is used in order to prevent problems with invalid values
5671 $sizelist[] = get_real_size($CFG->maxbytes);
5674 foreach ($sizelist as $sizebytes) {
5675 if ($sizebytes < $maxsize) {
5676 $filesize[intval($sizebytes)] = display_size($sizebytes);
5680 krsort($filesize, SORT_NUMERIC);
5682 return $filesize;
5686 * Returns an array with all the filenames in all subdirectories, relative to the given rootdir.
5688 * If excludefiles is defined, then that file/directory is ignored
5689 * If getdirs is true, then (sub)directories are included in the output
5690 * If getfiles is true, then files are included in the output
5691 * (at least one of these must be true!)
5693 * @todo Finish documenting this function. Add examples of $excludefile usage.
5695 * @param string $rootdir A given root directory to start from
5696 * @param string|array $excludefile If defined then the specified file/directory is ignored
5697 * @param bool $descend If true then subdirectories are recursed as well
5698 * @param bool $getdirs If true then (sub)directories are included in the output
5699 * @param bool $getfiles If true then files are included in the output
5700 * @return array An array with all the filenames in
5701 * all subdirectories, relative to the given rootdir
5703 function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) {
5705 $dirs = array();
5707 if (!$getdirs and !$getfiles) { // Nothing to show
5708 return $dirs;
5711 if (!is_dir($rootdir)) { // Must be a directory
5712 return $dirs;
5715 if (!$dir = opendir($rootdir)) { // Can't open it for some reason
5716 return $dirs;
5719 if (!is_array($excludefiles)) {
5720 $excludefiles = array($excludefiles);
5723 while (false !== ($file = readdir($dir))) {
5724 $firstchar = substr($file, 0, 1);
5725 if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) {
5726 continue;
5728 $fullfile = $rootdir .'/'. $file;
5729 if (filetype($fullfile) == 'dir') {
5730 if ($getdirs) {
5731 $dirs[] = $file;
5733 if ($descend) {
5734 $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles);
5735 foreach ($subdirs as $subdir) {
5736 $dirs[] = $file .'/'. $subdir;
5739 } else if ($getfiles) {
5740 $dirs[] = $file;
5743 closedir($dir);
5745 asort($dirs);
5747 return $dirs;
5752 * Adds up all the files in a directory and works out the size.
5754 * @todo Finish documenting this function
5756 * @param string $rootdir The directory to start from
5757 * @param string $excludefile A file to exclude when summing directory size
5758 * @return int The summed size of all files and subfiles within the root directory
5760 function get_directory_size($rootdir, $excludefile='') {
5761 global $CFG;
5763 // do it this way if we can, it's much faster
5764 if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) {
5765 $command = trim($CFG->pathtodu).' -sk '.escapeshellarg($rootdir);
5766 $output = null;
5767 $return = null;
5768 exec($command,$output,$return);
5769 if (is_array($output)) {
5770 return get_real_size(intval($output[0]).'k'); // we told it to return k.
5774 if (!is_dir($rootdir)) { // Must be a directory
5775 return 0;
5778 if (!$dir = @opendir($rootdir)) { // Can't open it for some reason
5779 return 0;
5782 $size = 0;
5784 while (false !== ($file = readdir($dir))) {
5785 $firstchar = substr($file, 0, 1);
5786 if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) {
5787 continue;
5789 $fullfile = $rootdir .'/'. $file;
5790 if (filetype($fullfile) == 'dir') {
5791 $size += get_directory_size($fullfile, $excludefile);
5792 } else {
5793 $size += filesize($fullfile);
5796 closedir($dir);
5798 return $size;
5802 * Converts bytes into display form
5804 * @todo Finish documenting this function. Verify return type.
5806 * @staticvar string $gb Localized string for size in gigabytes
5807 * @staticvar string $mb Localized string for size in megabytes
5808 * @staticvar string $kb Localized string for size in kilobytes
5809 * @staticvar string $b Localized string for size in bytes
5810 * @param int $size The size to convert to human readable form
5811 * @return string
5813 function display_size($size) {
5815 static $gb, $mb, $kb, $b;
5817 if (empty($gb)) {
5818 $gb = get_string('sizegb');
5819 $mb = get_string('sizemb');
5820 $kb = get_string('sizekb');
5821 $b = get_string('sizeb');
5824 if ($size >= 1073741824) {
5825 $size = round($size / 1073741824 * 10) / 10 . $gb;
5826 } else if ($size >= 1048576) {
5827 $size = round($size / 1048576 * 10) / 10 . $mb;
5828 } else if ($size >= 1024) {
5829 $size = round($size / 1024 * 10) / 10 . $kb;
5830 } else {
5831 $size = intval($size) .' '. $b; // file sizes over 2GB can not work in 32bit PHP anyway
5833 return $size;
5837 * Cleans a given filename by removing suspicious or troublesome characters
5838 * @see clean_param()
5840 * @uses PARAM_FILE
5841 * @param string $string file name
5842 * @return string cleaned file name
5844 function clean_filename($string) {
5845 return clean_param($string, PARAM_FILE);
5849 /// STRING TRANSLATION ////////////////////////////////////////
5852 * Returns the code for the current language
5854 * @return string
5856 function current_language() {
5857 global $CFG, $USER, $SESSION, $COURSE;
5859 if (!empty($COURSE->id) and $COURSE->id != SITEID and !empty($COURSE->lang)) { // Course language can override all other settings for this page
5860 $return = $COURSE->lang;
5862 } else if (!empty($SESSION->lang)) { // Session language can override other settings
5863 $return = $SESSION->lang;
5865 } else if (!empty($USER->lang)) {
5866 $return = $USER->lang;
5868 } else if (isset($CFG->lang)) {
5869 $return = $CFG->lang;
5871 } else {
5872 $return = 'en';
5875 $return = str_replace('_utf8', '', $return); // Just in case this slipped in from somewhere by accident
5877 return $return;
5881 * Returns parent language of current active language if defined
5883 * @uses COURSE
5884 * @uses SESSION
5885 * @param string $lang null means current language
5886 * @return string
5888 function get_parent_language($lang=null) {
5889 global $COURSE, $SESSION;
5891 //let's hack around the current language
5892 if (!empty($lang)) {
5893 $old_course_lang = empty($COURSE->lang) ? '' : $COURSE->lang;
5894 $old_session_lang = empty($SESSION->lang) ? '' : $SESSION->lang;
5895 $COURSE->lang = '';
5896 $SESSION->lang = $lang;
5899 $parentlang = get_string('parentlanguage', 'langconfig');
5900 if ($parentlang === 'en') {
5901 $parentlang = '';
5904 //let's hack around the current language
5905 if (!empty($lang)) {
5906 $COURSE->lang = $old_course_lang;
5907 $SESSION->lang = $old_session_lang;
5910 return $parentlang;
5914 * Returns current string_manager instance.
5916 * The param $forcereload is needed for CLI installer only where the string_manager instance
5917 * must be replaced during the install.php script life time.
5919 * @param bool $forcereload shall the singleton be released and new instance created instead?
5920 * @return string_manager
5922 function get_string_manager($forcereload=false) {
5923 global $CFG;
5925 static $singleton = null;
5927 if ($forcereload) {
5928 $singleton = null;
5930 if ($singleton === null) {
5931 if (empty($CFG->early_install_lang)) {
5933 if (empty($CFG->langcacheroot)) {
5934 $langcacheroot = $CFG->cachedir . '/lang';
5935 } else {
5936 $langcacheroot = $CFG->langcacheroot;
5939 if (empty($CFG->langlist)) {
5940 $translist = array();
5941 } else {
5942 $translist = explode(',', $CFG->langlist);
5945 if (empty($CFG->langmenucachefile)) {
5946 $langmenucache = $CFG->cachedir . '/languages';
5947 } else {
5948 $langmenucache = $CFG->langmenucachefile;
5951 $singleton = new core_string_manager($CFG->langotherroot, $CFG->langlocalroot, $langcacheroot,
5952 !empty($CFG->langstringcache), $translist, $langmenucache);
5954 } else {
5955 $singleton = new install_string_manager();
5959 return $singleton;
5964 * Interface describing class which is responsible for getting
5965 * of localised strings from language packs.
5967 * @package moodlecore
5968 * @copyright 2010 Petr Skoda (http://skodak.org)
5969 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5971 interface string_manager {
5973 * Get String returns a requested string
5975 * @param string $identifier The identifier of the string to search for
5976 * @param string $component The module the string is associated with
5977 * @param string|object|array $a An object, string or number that can be used
5978 * within translation strings
5979 * @param string $lang moodle translation language, NULL means use current
5980 * @return string The String !
5982 public function get_string($identifier, $component = '', $a = NULL, $lang = NULL);
5985 * Does the string actually exist?
5987 * get_string() is throwing debug warnings, sometimes we do not want them
5988 * or we want to display better explanation of the problem.
5990 * Use with care!
5992 * @param string $identifier The identifier of the string to search for
5993 * @param string $component The module the string is associated with
5994 * @return boot true if exists
5996 public function string_exists($identifier, $component);
5999 * Returns a localised list of all country names, sorted by country keys.
6000 * @param bool $returnall return all or just enabled
6001 * @param string $lang moodle translation language, NULL means use current
6002 * @return array two-letter country code => translated name.
6004 public function get_list_of_countries($returnall = false, $lang = NULL);
6007 * Returns a localised list of languages, sorted by code keys.
6009 * @param string $lang moodle translation language, NULL means use current
6010 * @param string $standard language list standard
6011 * iso6392: three-letter language code (ISO 639-2/T) => translated name.
6012 * @return array language code => translated name
6014 public function get_list_of_languages($lang = NULL, $standard = 'iso6392');
6017 * Does the translation exist?
6019 * @param string $lang moodle translation language code
6020 * @param bool include also disabled translations?
6021 * @return boot true if exists
6023 public function translation_exists($lang, $includeall = true);
6026 * Returns localised list of installed translations
6027 * @param bool $returnall return all or just enabled
6028 * @return array moodle translation code => localised translation name
6030 public function get_list_of_translations($returnall = false);
6033 * Returns localised list of currencies.
6035 * @param string $lang moodle translation language, NULL means use current
6036 * @return array currency code => localised currency name
6038 public function get_list_of_currencies($lang = NULL);
6041 * Load all strings for one component
6042 * @param string $component The module the string is associated with
6043 * @param string $lang
6044 * @param bool $disablecache Do not use caches, force fetching the strings from sources
6045 * @param bool $disablelocal Do not use customized strings in xx_local language packs
6046 * @return array of all string for given component and lang
6048 public function load_component_strings($component, $lang, $disablecache=false, $disablelocal=false);
6051 * Invalidates all caches, should the implementation use any
6053 public function reset_caches();
6058 * Standard string_manager implementation
6060 * @package moodlecore
6061 * @copyright 2010 Petr Skoda (http://skodak.org)
6062 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6064 class core_string_manager implements string_manager {
6065 /** @var string location of all packs except 'en' */
6066 protected $otherroot;
6067 /** @var string location of all lang pack local modifications */
6068 protected $localroot;
6069 /** @var string location of on-disk cache of merged strings */
6070 protected $cacheroot;
6071 /** @var array lang string cache - it will be optimised more later */
6072 protected $cache = array();
6073 /** @var int get_string() counter */
6074 protected $countgetstring = 0;
6075 /** @var int in-memory cache hits counter */
6076 protected $countmemcache = 0;
6077 /** @var int on-disk cache hits counter */
6078 protected $countdiskcache = 0;
6079 /** @var bool use disk cache */
6080 protected $usediskcache;
6081 /* @var array limit list of translations */
6082 protected $translist;
6083 /** @var string location of a file that caches the list of available translations */
6084 protected $menucache;
6087 * Create new instance of string manager
6089 * @param string $otherroot location of downlaoded lang packs - usually $CFG->dataroot/lang
6090 * @param string $localroot usually the same as $otherroot
6091 * @param string $cacheroot usually lang dir in cache folder
6092 * @param bool $usediskcache use disk cache
6093 * @param array $translist limit list of visible translations
6094 * @param string $menucache the location of a file that caches the list of available translations
6096 public function __construct($otherroot, $localroot, $cacheroot, $usediskcache, $translist, $menucache) {
6097 $this->otherroot = $otherroot;
6098 $this->localroot = $localroot;
6099 $this->cacheroot = $cacheroot;
6100 $this->usediskcache = $usediskcache;
6101 $this->translist = $translist;
6102 $this->menucache = $menucache;
6106 * Returns dependencies of current language, en is not included.
6107 * @param string $lang
6108 * @return array all parents, the lang itself is last
6110 public function get_language_dependencies($lang) {
6111 if ($lang === 'en') {
6112 return array();
6114 if (!file_exists("$this->otherroot/$lang/langconfig.php")) {
6115 return array();
6117 $string = array();
6118 include("$this->otherroot/$lang/langconfig.php");
6120 if (empty($string['parentlanguage'])) {
6121 return array($lang);
6122 } else {
6123 $parentlang = $string['parentlanguage'];
6124 unset($string);
6125 return array_merge($this->get_language_dependencies($parentlang), array($lang));
6130 * Load all strings for one component
6131 * @param string $component The module the string is associated with
6132 * @param string $lang
6133 * @param bool $disablecache Do not use caches, force fetching the strings from sources
6134 * @param bool $disablelocal Do not use customized strings in xx_local language packs
6135 * @return array of all string for given component and lang
6137 public function load_component_strings($component, $lang, $disablecache=false, $disablelocal=false) {
6138 global $CFG;
6140 list($plugintype, $pluginname) = normalize_component($component);
6141 if ($plugintype == 'core' and is_null($pluginname)) {
6142 $component = 'core';
6143 } else {
6144 $component = $plugintype . '_' . $pluginname;
6147 if (!$disablecache) {
6148 // try in-memory cache first
6149 if (isset($this->cache[$lang][$component])) {
6150 $this->countmemcache++;
6151 return $this->cache[$lang][$component];
6154 // try on-disk cache then
6155 if ($this->usediskcache and file_exists($this->cacheroot . "/$lang/$component.php")) {
6156 $this->countdiskcache++;
6157 include($this->cacheroot . "/$lang/$component.php");
6158 return $this->cache[$lang][$component];
6162 // no cache found - let us merge all possible sources of the strings
6163 if ($plugintype === 'core') {
6164 $file = $pluginname;
6165 if ($file === null) {
6166 $file = 'moodle';
6168 $string = array();
6169 // first load english pack
6170 if (!file_exists("$CFG->dirroot/lang/en/$file.php")) {
6171 return array();
6173 include("$CFG->dirroot/lang/en/$file.php");
6174 $originalkeys = array_keys($string);
6175 $originalkeys = array_flip($originalkeys);
6177 // and then corresponding local if present and allowed
6178 if (!$disablelocal and file_exists("$this->localroot/en_local/$file.php")) {
6179 include("$this->localroot/en_local/$file.php");
6181 // now loop through all langs in correct order
6182 $deps = $this->get_language_dependencies($lang);
6183 foreach ($deps as $dep) {
6184 // the main lang string location
6185 if (file_exists("$this->otherroot/$dep/$file.php")) {
6186 include("$this->otherroot/$dep/$file.php");
6188 if (!$disablelocal and file_exists("$this->localroot/{$dep}_local/$file.php")) {
6189 include("$this->localroot/{$dep}_local/$file.php");
6193 } else {
6194 if (!$location = get_plugin_directory($plugintype, $pluginname) or !is_dir($location)) {
6195 return array();
6197 if ($plugintype === 'mod') {
6198 // bloody mod hack
6199 $file = $pluginname;
6200 } else {
6201 $file = $plugintype . '_' . $pluginname;
6203 $string = array();
6204 // first load English pack
6205 if (!file_exists("$location/lang/en/$file.php")) {
6206 //English pack does not exist, so do not try to load anything else
6207 return array();
6209 include("$location/lang/en/$file.php");
6210 $originalkeys = array_keys($string);
6211 $originalkeys = array_flip($originalkeys);
6212 // and then corresponding local english if present
6213 if (!$disablelocal and file_exists("$this->localroot/en_local/$file.php")) {
6214 include("$this->localroot/en_local/$file.php");
6217 // now loop through all langs in correct order
6218 $deps = $this->get_language_dependencies($lang);
6219 foreach ($deps as $dep) {
6220 // legacy location - used by contrib only
6221 if (file_exists("$location/lang/$dep/$file.php")) {
6222 include("$location/lang/$dep/$file.php");
6224 // the main lang string location
6225 if (file_exists("$this->otherroot/$dep/$file.php")) {
6226 include("$this->otherroot/$dep/$file.php");
6228 // local customisations
6229 if (!$disablelocal and file_exists("$this->localroot/{$dep}_local/$file.php")) {
6230 include("$this->localroot/{$dep}_local/$file.php");
6235 // we do not want any extra strings from other languages - everything must be in en lang pack
6236 $string = array_intersect_key($string, $originalkeys);
6238 // now we have a list of strings from all possible sources. put it into both in-memory and on-disk
6239 // caches so we do not need to do all this merging and dependencies resolving again
6240 $this->cache[$lang][$component] = $string;
6241 if ($this->usediskcache) {
6242 check_dir_exists("$this->cacheroot/$lang");
6243 file_put_contents("$this->cacheroot/$lang/$component.php", "<?php \$this->cache['$lang']['$component'] = ".var_export($string, true).";");
6245 return $string;
6249 * Does the string actually exist?
6251 * get_string() is throwing debug warnings, sometimes we do not want them
6252 * or we want to display better explanation of the problem.
6254 * Use with care!
6256 * @param string $identifier The identifier of the string to search for
6257 * @param string $component The module the string is associated with
6258 * @return boot true if exists
6260 public function string_exists($identifier, $component) {
6261 $identifier = clean_param($identifier, PARAM_STRINGID);
6262 if (empty($identifier)) {
6263 return false;
6265 $lang = current_language();
6266 $string = $this->load_component_strings($component, $lang);
6267 return isset($string[$identifier]);
6271 * Get String returns a requested string
6273 * @param string $identifier The identifier of the string to search for
6274 * @param string $component The module the string is associated with
6275 * @param string|object|array $a An object, string or number that can be used
6276 * within translation strings
6277 * @param string $lang moodle translation language, NULL means use current
6278 * @return string The String !
6280 public function get_string($identifier, $component = '', $a = NULL, $lang = NULL) {
6281 $this->countgetstring++;
6282 // there are very many uses of these time formating strings without the 'langconfig' component,
6283 // it would not be reasonable to expect that all of them would be converted during 2.0 migration
6284 static $langconfigstrs = array(
6285 'strftimedate' => 1,
6286 'strftimedatefullshort' => 1,
6287 'strftimedateshort' => 1,
6288 'strftimedatetime' => 1,
6289 'strftimedatetimeshort' => 1,
6290 'strftimedaydate' => 1,
6291 'strftimedaydatetime' => 1,
6292 'strftimedayshort' => 1,
6293 'strftimedaytime' => 1,
6294 'strftimemonthyear' => 1,
6295 'strftimerecent' => 1,
6296 'strftimerecentfull' => 1,
6297 'strftimetime' => 1);
6299 if (empty($component)) {
6300 if (isset($langconfigstrs[$identifier])) {
6301 $component = 'langconfig';
6302 } else {
6303 $component = 'moodle';
6307 if ($lang === NULL) {
6308 $lang = current_language();
6311 $string = $this->load_component_strings($component, $lang);
6313 if (!isset($string[$identifier])) {
6314 if ($component === 'pix' or $component === 'core_pix') {
6315 // this component contains only alt tags for emoticons,
6316 // not all of them are supposed to be defined
6317 return '';
6319 if ($identifier === 'parentlanguage' and ($component === 'langconfig' or $component === 'core_langconfig')) {
6320 // parentlanguage is a special string, undefined means use English if not defined
6321 return 'en';
6323 if ($this->usediskcache) {
6324 // maybe the on-disk cache is dirty - let the last attempt be to find the string in original sources
6325 $string = $this->load_component_strings($component, $lang, true);
6327 if (!isset($string[$identifier])) {
6328 // the string is still missing - should be fixed by developer
6329 list($plugintype, $pluginname) = normalize_component($component);
6330 if ($plugintype == 'core') {
6331 $file = "lang/en/{$component}.php";
6332 } else if ($plugintype == 'mod') {
6333 $file = "mod/{$pluginname}/lang/en/{$pluginname}.php";
6334 } else {
6335 $path = get_plugin_directory($plugintype, $pluginname);
6336 $file = "{$path}/lang/en/{$plugintype}_{$pluginname}.php";
6338 debugging("Invalid get_string() identifier: '{$identifier}' or component '{$component}'. " .
6339 "Perhaps you are missing \$string['{$identifier}'] = ''; in {$file}?", DEBUG_DEVELOPER);
6340 return "[[$identifier]]";
6344 $string = $string[$identifier];
6346 if ($a !== NULL) {
6347 if (is_object($a) or is_array($a)) {
6348 $a = (array)$a;
6349 $search = array();
6350 $replace = array();
6351 foreach ($a as $key=>$value) {
6352 if (is_int($key)) {
6353 // we do not support numeric keys - sorry!
6354 continue;
6356 if (is_object($value) or is_array($value)) {
6357 // we support just string as value
6358 continue;
6360 $search[] = '{$a->'.$key.'}';
6361 $replace[] = (string)$value;
6363 if ($search) {
6364 $string = str_replace($search, $replace, $string);
6366 } else {
6367 $string = str_replace('{$a}', (string)$a, $string);
6371 return $string;
6375 * Returns information about the string_manager performance
6376 * @return array
6378 public function get_performance_summary() {
6379 return array(array(
6380 'langcountgetstring' => $this->countgetstring,
6381 'langcountmemcache' => $this->countmemcache,
6382 'langcountdiskcache' => $this->countdiskcache,
6383 ), array(
6384 'langcountgetstring' => 'get_string calls',
6385 'langcountmemcache' => 'strings mem cache hits',
6386 'langcountdiskcache' => 'strings disk cache hits',
6391 * Returns a localised list of all country names, sorted by localised name.
6393 * @param bool $returnall return all or just enabled
6394 * @param string $lang moodle translation language, NULL means use current
6395 * @return array two-letter country code => translated name.
6397 public function get_list_of_countries($returnall = false, $lang = NULL) {
6398 global $CFG;
6400 if ($lang === NULL) {
6401 $lang = current_language();
6404 $countries = $this->load_component_strings('core_countries', $lang);
6405 collatorlib::asort($countries);
6406 if (!$returnall and !empty($CFG->allcountrycodes)) {
6407 $enabled = explode(',', $CFG->allcountrycodes);
6408 $return = array();
6409 foreach ($enabled as $c) {
6410 if (isset($countries[$c])) {
6411 $return[$c] = $countries[$c];
6414 return $return;
6417 return $countries;
6421 * Returns a localised list of languages, sorted by code keys.
6423 * @param string $lang moodle translation language, NULL means use current
6424 * @param string $standard language list standard
6425 * - iso6392: three-letter language code (ISO 639-2/T) => translated name
6426 * - iso6391: two-letter langauge code (ISO 639-1) => translated name
6427 * @return array language code => translated name
6429 public function get_list_of_languages($lang = NULL, $standard = 'iso6391') {
6430 if ($lang === NULL) {
6431 $lang = current_language();
6434 if ($standard === 'iso6392') {
6435 $langs = $this->load_component_strings('core_iso6392', $lang);
6436 ksort($langs);
6437 return $langs;
6439 } else if ($standard === 'iso6391') {
6440 $langs2 = $this->load_component_strings('core_iso6392', $lang);
6441 static $mapping = array('aar' => 'aa', 'abk' => 'ab', 'afr' => 'af', 'aka' => 'ak', 'sqi' => 'sq', 'amh' => 'am', 'ara' => 'ar', 'arg' => 'an', 'hye' => 'hy',
6442 'asm' => 'as', 'ava' => 'av', 'ave' => 'ae', 'aym' => 'ay', 'aze' => 'az', 'bak' => 'ba', 'bam' => 'bm', 'eus' => 'eu', 'bel' => 'be', 'ben' => 'bn', 'bih' => 'bh',
6443 'bis' => 'bi', 'bos' => 'bs', 'bre' => 'br', 'bul' => 'bg', 'mya' => 'my', 'cat' => 'ca', 'cha' => 'ch', 'che' => 'ce', 'zho' => 'zh', 'chu' => 'cu', 'chv' => 'cv',
6444 'cor' => 'kw', 'cos' => 'co', 'cre' => 'cr', 'ces' => 'cs', 'dan' => 'da', 'div' => 'dv', 'nld' => 'nl', 'dzo' => 'dz', 'eng' => 'en', 'epo' => 'eo', 'est' => 'et',
6445 'ewe' => 'ee', 'fao' => 'fo', 'fij' => 'fj', 'fin' => 'fi', 'fra' => 'fr', 'fry' => 'fy', 'ful' => 'ff', 'kat' => 'ka', 'deu' => 'de', 'gla' => 'gd', 'gle' => 'ga',
6446 'glg' => 'gl', 'glv' => 'gv', 'ell' => 'el', 'grn' => 'gn', 'guj' => 'gu', 'hat' => 'ht', 'hau' => 'ha', 'heb' => 'he', 'her' => 'hz', 'hin' => 'hi', 'hmo' => 'ho',
6447 'hrv' => 'hr', 'hun' => 'hu', 'ibo' => 'ig', 'isl' => 'is', 'ido' => 'io', 'iii' => 'ii', 'iku' => 'iu', 'ile' => 'ie', 'ina' => 'ia', 'ind' => 'id', 'ipk' => 'ik',
6448 'ita' => 'it', 'jav' => 'jv', 'jpn' => 'ja', 'kal' => 'kl', 'kan' => 'kn', 'kas' => 'ks', 'kau' => 'kr', 'kaz' => 'kk', 'khm' => 'km', 'kik' => 'ki', 'kin' => 'rw',
6449 'kir' => 'ky', 'kom' => 'kv', 'kon' => 'kg', 'kor' => 'ko', 'kua' => 'kj', 'kur' => 'ku', 'lao' => 'lo', 'lat' => 'la', 'lav' => 'lv', 'lim' => 'li', 'lin' => 'ln',
6450 'lit' => 'lt', 'ltz' => 'lb', 'lub' => 'lu', 'lug' => 'lg', 'mkd' => 'mk', 'mah' => 'mh', 'mal' => 'ml', 'mri' => 'mi', 'mar' => 'mr', 'msa' => 'ms', 'mlg' => 'mg',
6451 'mlt' => 'mt', 'mon' => 'mn', 'nau' => 'na', 'nav' => 'nv', 'nbl' => 'nr', 'nde' => 'nd', 'ndo' => 'ng', 'nep' => 'ne', 'nno' => 'nn', 'nob' => 'nb', 'nor' => 'no',
6452 'nya' => 'ny', 'oci' => 'oc', 'oji' => 'oj', 'ori' => 'or', 'orm' => 'om', 'oss' => 'os', 'pan' => 'pa', 'fas' => 'fa', 'pli' => 'pi', 'pol' => 'pl', 'por' => 'pt',
6453 'pus' => 'ps', 'que' => 'qu', 'roh' => 'rm', 'ron' => 'ro', 'run' => 'rn', 'rus' => 'ru', 'sag' => 'sg', 'san' => 'sa', 'sin' => 'si', 'slk' => 'sk', 'slv' => 'sl',
6454 'sme' => 'se', 'smo' => 'sm', 'sna' => 'sn', 'snd' => 'sd', 'som' => 'so', 'sot' => 'st', 'spa' => 'es', 'srd' => 'sc', 'srp' => 'sr', 'ssw' => 'ss', 'sun' => 'su',
6455 'swa' => 'sw', 'swe' => 'sv', 'tah' => 'ty', 'tam' => 'ta', 'tat' => 'tt', 'tel' => 'te', 'tgk' => 'tg', 'tgl' => 'tl', 'tha' => 'th', 'bod' => 'bo', 'tir' => 'ti',
6456 'ton' => 'to', 'tsn' => 'tn', 'tso' => 'ts', 'tuk' => 'tk', 'tur' => 'tr', 'twi' => 'tw', 'uig' => 'ug', 'ukr' => 'uk', 'urd' => 'ur', 'uzb' => 'uz', 'ven' => 've',
6457 'vie' => 'vi', 'vol' => 'vo', 'cym' => 'cy', 'wln' => 'wa', 'wol' => 'wo', 'xho' => 'xh', 'yid' => 'yi', 'yor' => 'yo', 'zha' => 'za', 'zul' => 'zu');
6458 $langs1 = array();
6459 foreach ($mapping as $c2=>$c1) {
6460 $langs1[$c1] = $langs2[$c2];
6462 ksort($langs1);
6463 return $langs1;
6465 } else {
6466 debugging('Unsupported $standard parameter in get_list_of_languages() method: '.$standard);
6469 return array();
6473 * Does the translation exist?
6475 * @param string $lang moodle translation language code
6476 * @param bool include also disabled translations?
6477 * @return boot true if exists
6479 public function translation_exists($lang, $includeall = true) {
6481 if (strpos($lang, '_local') !== false) {
6482 // _local packs are not real translations
6483 return false;
6485 if (!$includeall and !empty($this->translist)) {
6486 if (!in_array($lang, $this->translist)) {
6487 return false;
6490 if ($lang === 'en') {
6491 // part of distribution
6492 return true;
6494 return file_exists("$this->otherroot/$lang/langconfig.php");
6498 * Returns localised list of installed translations
6499 * @param bool $returnall return all or just enabled
6500 * @return array moodle translation code => localised translation name
6502 public function get_list_of_translations($returnall = false) {
6503 global $CFG;
6505 $languages = array();
6507 if (!empty($CFG->langcache) and is_readable($this->menucache)) {
6508 // try to re-use the cached list of all available languages
6509 $cachedlist = json_decode(file_get_contents($this->menucache), true);
6511 if (is_array($cachedlist) and !empty($cachedlist)) {
6512 // the cache file is restored correctly
6514 if (!$returnall and !empty($this->translist)) {
6515 // return just enabled translations
6516 foreach ($cachedlist as $langcode => $langname) {
6517 if (in_array($langcode, $this->translist)) {
6518 $languages[$langcode] = $langname;
6521 return $languages;
6523 } else {
6524 // return all translations
6525 return $cachedlist;
6530 // the cached list of languages is not available, let us populate the list
6532 if (!$returnall and !empty($this->translist)) {
6533 // return only some translations
6534 foreach ($this->translist as $lang) {
6535 $lang = trim($lang); //Just trim spaces to be a bit more permissive
6536 if (strstr($lang, '_local') !== false) {
6537 continue;
6539 if (strstr($lang, '_utf8') !== false) {
6540 continue;
6542 if ($lang !== 'en' and !file_exists("$this->otherroot/$lang/langconfig.php")) {
6543 // some broken or missing lang - can not switch to it anyway
6544 continue;
6546 $string = $this->load_component_strings('langconfig', $lang);
6547 if (!empty($string['thislanguage'])) {
6548 $languages[$lang] = $string['thislanguage'].' ('. $lang .')';
6550 unset($string);
6553 } else {
6554 // return all languages available in system
6555 $langdirs = get_list_of_plugins('', '', $this->otherroot);
6557 $langdirs = array_merge($langdirs, array("$CFG->dirroot/lang/en"=>'en'));
6558 // Sort all
6560 // Loop through all langs and get info
6561 foreach ($langdirs as $lang) {
6562 if (strstr($lang, '_local') !== false) {
6563 continue;
6565 if (strstr($lang, '_utf8') !== false) {
6566 continue;
6568 $string = $this->load_component_strings('langconfig', $lang);
6569 if (!empty($string['thislanguage'])) {
6570 $languages[$lang] = $string['thislanguage'].' ('. $lang .')';
6572 unset($string);
6575 if (!empty($CFG->langcache) and !empty($this->menucache)) {
6576 // cache the list so that it can be used next time
6577 collatorlib::asort($languages);
6578 check_dir_exists(dirname($this->menucache), true, true);
6579 file_put_contents($this->menucache, json_encode($languages));
6583 collatorlib::asort($languages);
6585 return $languages;
6589 * Returns localised list of currencies.
6591 * @param string $lang moodle translation language, NULL means use current
6592 * @return array currency code => localised currency name
6594 public function get_list_of_currencies($lang = NULL) {
6595 if ($lang === NULL) {
6596 $lang = current_language();
6599 $currencies = $this->load_component_strings('core_currencies', $lang);
6600 asort($currencies);
6602 return $currencies;
6606 * Clears both in-memory and on-disk caches
6608 public function reset_caches() {
6609 global $CFG;
6610 require_once("$CFG->libdir/filelib.php");
6612 // clear the on-disk disk with aggregated string files
6613 fulldelete($this->cacheroot);
6615 // clear the in-memory cache of loaded strings
6616 $this->cache = array();
6618 // clear the cache containing the list of available translations
6619 // and re-populate it again
6620 fulldelete($this->menucache);
6621 $this->get_list_of_translations(true);
6627 * Minimalistic string fetching implementation
6628 * that is used in installer before we fetch the wanted
6629 * language pack from moodle.org lang download site.
6631 * @package moodlecore
6632 * @copyright 2010 Petr Skoda (http://skodak.org)
6633 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6635 class install_string_manager implements string_manager {
6636 /** @var string location of pre-install packs for all langs */
6637 protected $installroot;
6640 * Crate new instance of install string manager
6642 public function __construct() {
6643 global $CFG;
6644 $this->installroot = "$CFG->dirroot/install/lang";
6648 * Load all strings for one component
6649 * @param string $component The module the string is associated with
6650 * @param string $lang
6651 * @param bool $disablecache Do not use caches, force fetching the strings from sources
6652 * @param bool $disablelocal Do not use customized strings in xx_local language packs
6653 * @return array of all string for given component and lang
6655 public function load_component_strings($component, $lang, $disablecache=false, $disablelocal=false) {
6656 // not needed in installer
6657 return array();
6661 * Does the string actually exist?
6663 * get_string() is throwing debug warnings, sometimes we do not want them
6664 * or we want to display better explanation of the problem.
6666 * Use with care!
6668 * @param string $identifier The identifier of the string to search for
6669 * @param string $component The module the string is associated with
6670 * @return boot true if exists
6672 public function string_exists($identifier, $component) {
6673 $identifier = clean_param($identifier, PARAM_STRINGID);
6674 if (empty($identifier)) {
6675 return false;
6677 // simple old style hack ;)
6678 $str = get_string($identifier, $component);
6679 return (strpos($str, '[[') === false);
6683 * Get String returns a requested string
6685 * @param string $identifier The identifier of the string to search for
6686 * @param string $component The module the string is associated with
6687 * @param string|object|array $a An object, string or number that can be used
6688 * within translation strings
6689 * @param string $lang moodle translation language, NULL means use current
6690 * @return string The String !
6692 public function get_string($identifier, $component = '', $a = NULL, $lang = NULL) {
6693 if (!$component) {
6694 $component = 'moodle';
6697 if ($lang === NULL) {
6698 $lang = current_language();
6701 //get parent lang
6702 $parent = '';
6703 if ($lang !== 'en' and $identifier !== 'parentlanguage' and $component !== 'langconfig') {
6704 if (file_exists("$this->installroot/$lang/langconfig.php")) {
6705 $string = array();
6706 include("$this->installroot/$lang/langconfig.php");
6707 if (isset($string['parentlanguage'])) {
6708 $parent = $string['parentlanguage'];
6710 unset($string);
6714 // include en string first
6715 if (!file_exists("$this->installroot/en/$component.php")) {
6716 return "[[$identifier]]";
6718 $string = array();
6719 include("$this->installroot/en/$component.php");
6721 // now override en with parent if defined
6722 if ($parent and $parent !== 'en' and file_exists("$this->installroot/$parent/$component.php")) {
6723 include("$this->installroot/$parent/$component.php");
6726 // finally override with requested language
6727 if ($lang !== 'en' and file_exists("$this->installroot/$lang/$component.php")) {
6728 include("$this->installroot/$lang/$component.php");
6731 if (!isset($string[$identifier])) {
6732 return "[[$identifier]]";
6735 $string = $string[$identifier];
6737 if ($a !== NULL) {
6738 if (is_object($a) or is_array($a)) {
6739 $a = (array)$a;
6740 $search = array();
6741 $replace = array();
6742 foreach ($a as $key=>$value) {
6743 if (is_int($key)) {
6744 // we do not support numeric keys - sorry!
6745 continue;
6747 $search[] = '{$a->'.$key.'}';
6748 $replace[] = (string)$value;
6750 if ($search) {
6751 $string = str_replace($search, $replace, $string);
6753 } else {
6754 $string = str_replace('{$a}', (string)$a, $string);
6758 return $string;
6762 * Returns a localised list of all country names, sorted by country keys.
6764 * @param bool $returnall return all or just enabled
6765 * @param string $lang moodle translation language, NULL means use current
6766 * @return array two-letter country code => translated name.
6768 public function get_list_of_countries($returnall = false, $lang = NULL) {
6769 //not used in installer
6770 return array();
6774 * Returns a localised list of languages, sorted by code keys.
6776 * @param string $lang moodle translation language, NULL means use current
6777 * @param string $standard language list standard
6778 * iso6392: three-letter language code (ISO 639-2/T) => translated name.
6779 * @return array language code => translated name
6781 public function get_list_of_languages($lang = NULL, $standard = 'iso6392') {
6782 //not used in installer
6783 return array();
6787 * Does the translation exist?
6789 * @param string $lang moodle translation language code
6790 * @param bool include also disabled translations?
6791 * @return boot true if exists
6793 public function translation_exists($lang, $includeall = true) {
6794 return file_exists($this->installroot.'/'.$lang.'/langconfig.php');
6798 * Returns localised list of installed translations
6799 * @param bool $returnall return all or just enabled
6800 * @return array moodle translation code => localised translation name
6802 public function get_list_of_translations($returnall = false) {
6803 // return all is ignored here - we need to know all langs in installer
6804 $languages = array();
6805 // Get raw list of lang directories
6806 $langdirs = get_list_of_plugins('install/lang');
6807 asort($langdirs);
6808 // Get some info from each lang
6809 foreach ($langdirs as $lang) {
6810 if (file_exists($this->installroot.'/'.$lang.'/langconfig.php')) {
6811 $string = array();
6812 include($this->installroot.'/'.$lang.'/langconfig.php');
6813 if (!empty($string['thislanguage'])) {
6814 $languages[$lang] = $string['thislanguage'].' ('.$lang.')';
6818 // Return array
6819 return $languages;
6823 * Returns localised list of currencies.
6825 * @param string $lang moodle translation language, NULL means use current
6826 * @return array currency code => localised currency name
6828 public function get_list_of_currencies($lang = NULL) {
6829 // not used in installer
6830 return array();
6834 * This implementation does not use any caches
6836 public function reset_caches() {}
6841 * Returns a localized string.
6843 * Returns the translated string specified by $identifier as
6844 * for $module. Uses the same format files as STphp.
6845 * $a is an object, string or number that can be used
6846 * within translation strings
6848 * eg 'hello {$a->firstname} {$a->lastname}'
6849 * or 'hello {$a}'
6851 * If you would like to directly echo the localized string use
6852 * the function {@link print_string()}
6854 * Example usage of this function involves finding the string you would
6855 * like a local equivalent of and using its identifier and module information
6856 * to retrieve it.<br/>
6857 * If you open moodle/lang/en/moodle.php and look near line 278
6858 * you will find a string to prompt a user for their word for 'course'
6859 * <code>
6860 * $string['course'] = 'Course';
6861 * </code>
6862 * So if you want to display the string 'Course'
6863 * in any language that supports it on your site
6864 * you just need to use the identifier 'course'
6865 * <code>
6866 * $mystring = '<strong>'. get_string('course') .'</strong>';
6867 * or
6868 * </code>
6869 * If the string you want is in another file you'd take a slightly
6870 * different approach. Looking in moodle/lang/en/calendar.php you find
6871 * around line 75:
6872 * <code>
6873 * $string['typecourse'] = 'Course event';
6874 * </code>
6875 * If you want to display the string "Course event" in any language
6876 * supported you would use the identifier 'typecourse' and the module 'calendar'
6877 * (because it is in the file calendar.php):
6878 * <code>
6879 * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>';
6880 * </code>
6882 * As a last resort, should the identifier fail to map to a string
6883 * the returned string will be [[ $identifier ]]
6885 * @param string $identifier The key identifier for the localized string
6886 * @param string $component The module where the key identifier is stored,
6887 * usually expressed as the filename in the language pack without the
6888 * .php on the end but can also be written as mod/forum or grade/export/xls.
6889 * If none is specified then moodle.php is used.
6890 * @param string|object|array $a An object, string or number that can be used
6891 * within translation strings
6892 * @return string The localized string.
6894 function get_string($identifier, $component = '', $a = NULL) {
6895 global $CFG;
6897 $identifier = clean_param($identifier, PARAM_STRINGID);
6898 if (empty($identifier)) {
6899 throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please fix your get_string() call and string definition');
6902 if (func_num_args() > 3) {
6903 debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.');
6906 if (strpos($component, '/') !== false) {
6907 debugging('The module name you passed to get_string is the deprecated format ' .
6908 'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER);
6909 $componentpath = explode('/', $component);
6911 switch ($componentpath[0]) {
6912 case 'mod':
6913 $component = $componentpath[1];
6914 break;
6915 case 'blocks':
6916 case 'block':
6917 $component = 'block_'.$componentpath[1];
6918 break;
6919 case 'enrol':
6920 $component = 'enrol_'.$componentpath[1];
6921 break;
6922 case 'format':
6923 $component = 'format_'.$componentpath[1];
6924 break;
6925 case 'grade':
6926 $component = 'grade'.$componentpath[1].'_'.$componentpath[2];
6927 break;
6931 $result = get_string_manager()->get_string($identifier, $component, $a);
6933 // Debugging feature lets you display string identifier and component
6934 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
6935 $result .= ' {' . $identifier . '/' . $component . '}';
6937 return $result;
6941 * Converts an array of strings to their localized value.
6943 * @param array $array An array of strings
6944 * @param string $module The language module that these strings can be found in.
6945 * @return array and array of translated strings.
6947 function get_strings($array, $component = '') {
6948 $string = new stdClass;
6949 foreach ($array as $item) {
6950 $string->$item = get_string($item, $component);
6952 return $string;
6956 * Prints out a translated string.
6958 * Prints out a translated string using the return value from the {@link get_string()} function.
6960 * Example usage of this function when the string is in the moodle.php file:<br/>
6961 * <code>
6962 * echo '<strong>';
6963 * print_string('course');
6964 * echo '</strong>';
6965 * </code>
6967 * Example usage of this function when the string is not in the moodle.php file:<br/>
6968 * <code>
6969 * echo '<h1>';
6970 * print_string('typecourse', 'calendar');
6971 * echo '</h1>';
6972 * </code>
6974 * @param string $identifier The key identifier for the localized string
6975 * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used.
6976 * @param mixed $a An object, string or number that can be used within translation strings
6978 function print_string($identifier, $component = '', $a = NULL) {
6979 echo get_string($identifier, $component, $a);
6983 * Returns a list of charset codes
6985 * Returns a list of charset codes. It's hardcoded, so they should be added manually
6986 * (checking that such charset is supported by the texlib library!)
6988 * @return array And associative array with contents in the form of charset => charset
6990 function get_list_of_charsets() {
6992 $charsets = array(
6993 'EUC-JP' => 'EUC-JP',
6994 'ISO-2022-JP'=> 'ISO-2022-JP',
6995 'ISO-8859-1' => 'ISO-8859-1',
6996 'SHIFT-JIS' => 'SHIFT-JIS',
6997 'GB2312' => 'GB2312',
6998 'GB18030' => 'GB18030', // gb18030 not supported by typo and mbstring
6999 'UTF-8' => 'UTF-8');
7001 asort($charsets);
7003 return $charsets;
7007 * Returns a list of valid and compatible themes
7009 * @return array
7011 function get_list_of_themes() {
7012 global $CFG;
7014 $themes = array();
7016 if (!empty($CFG->themelist)) { // use admin's list of themes
7017 $themelist = explode(',', $CFG->themelist);
7018 } else {
7019 $themelist = array_keys(get_plugin_list("theme"));
7022 foreach ($themelist as $key => $themename) {
7023 $theme = theme_config::load($themename);
7024 $themes[$themename] = $theme;
7027 collatorlib::asort_objects_by_method($themes, 'get_theme_name');
7029 return $themes;
7033 * Returns a list of timezones in the current language
7035 * @global object
7036 * @global object
7037 * @return array
7039 function get_list_of_timezones() {
7040 global $CFG, $DB;
7042 static $timezones;
7044 if (!empty($timezones)) { // This function has been called recently
7045 return $timezones;
7048 $timezones = array();
7050 if ($rawtimezones = $DB->get_records_sql("SELECT MAX(id), name FROM {timezone} GROUP BY name")) {
7051 foreach($rawtimezones as $timezone) {
7052 if (!empty($timezone->name)) {
7053 if (get_string_manager()->string_exists(strtolower($timezone->name), 'timezones')) {
7054 $timezones[$timezone->name] = get_string(strtolower($timezone->name), 'timezones');
7055 } else {
7056 $timezones[$timezone->name] = $timezone->name;
7058 if (substr($timezones[$timezone->name], 0, 1) == '[') { // No translation found
7059 $timezones[$timezone->name] = $timezone->name;
7065 asort($timezones);
7067 for ($i = -13; $i <= 13; $i += .5) {
7068 $tzstring = 'UTC';
7069 if ($i < 0) {
7070 $timezones[sprintf("%.1f", $i)] = $tzstring . $i;
7071 } else if ($i > 0) {
7072 $timezones[sprintf("%.1f", $i)] = $tzstring . '+' . $i;
7073 } else {
7074 $timezones[sprintf("%.1f", $i)] = $tzstring;
7078 return $timezones;
7082 * Factory function for emoticon_manager
7084 * @return emoticon_manager singleton
7086 function get_emoticon_manager() {
7087 static $singleton = null;
7089 if (is_null($singleton)) {
7090 $singleton = new emoticon_manager();
7093 return $singleton;
7097 * Provides core support for plugins that have to deal with
7098 * emoticons (like HTML editor or emoticon filter).
7100 * Whenever this manager mentiones 'emoticon object', the following data
7101 * structure is expected: stdClass with properties text, imagename, imagecomponent,
7102 * altidentifier and altcomponent
7104 * @see admin_setting_emoticons
7106 class emoticon_manager {
7109 * Returns the currently enabled emoticons
7111 * @return array of emoticon objects
7113 public function get_emoticons() {
7114 global $CFG;
7116 if (empty($CFG->emoticons)) {
7117 return array();
7120 $emoticons = $this->decode_stored_config($CFG->emoticons);
7122 if (!is_array($emoticons)) {
7123 // something is wrong with the format of stored setting
7124 debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL);
7125 return array();
7128 return $emoticons;
7132 * Converts emoticon object into renderable pix_emoticon object
7134 * @param stdClass $emoticon emoticon object
7135 * @param array $attributes explicit HTML attributes to set
7136 * @return pix_emoticon
7138 public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array()) {
7139 $stringmanager = get_string_manager();
7140 if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) {
7141 $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent);
7142 } else {
7143 $alt = s($emoticon->text);
7145 return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes);
7149 * Encodes the array of emoticon objects into a string storable in config table
7151 * @see self::decode_stored_config()
7152 * @param array $emoticons array of emtocion objects
7153 * @return string
7155 public function encode_stored_config(array $emoticons) {
7156 return json_encode($emoticons);
7160 * Decodes the string into an array of emoticon objects
7162 * @see self::encode_stored_config()
7163 * @param string $encoded
7164 * @return string|null
7166 public function decode_stored_config($encoded) {
7167 $decoded = json_decode($encoded);
7168 if (!is_array($decoded)) {
7169 return null;
7171 return $decoded;
7175 * Returns default set of emoticons supported by Moodle
7177 * @return array of sdtClasses
7179 public function default_emoticons() {
7180 return array(
7181 $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'),
7182 $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'),
7183 $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'),
7184 $this->prepare_emoticon_object(";-)", 's/wink', 'wink'),
7185 $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'),
7186 $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'),
7187 $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'),
7188 $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'),
7189 $this->prepare_emoticon_object("B-)", 's/cool', 'cool'),
7190 $this->prepare_emoticon_object("^-)", 's/approve', 'approve'),
7191 $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'),
7192 $this->prepare_emoticon_object(":o)", 's/clown', 'clown'),
7193 $this->prepare_emoticon_object(":-(", 's/sad', 'sad'),
7194 $this->prepare_emoticon_object(":(", 's/sad', 'sad'),
7195 $this->prepare_emoticon_object("8-.", 's/shy', 'shy'),
7196 $this->prepare_emoticon_object(":-I", 's/blush', 'blush'),
7197 $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'),
7198 $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'),
7199 $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'),
7200 $this->prepare_emoticon_object("8-[", 's/angry', 'angry'),
7201 $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'),
7202 $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'),
7203 $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'),
7204 $this->prepare_emoticon_object("}-]", 's/evil', 'evil'),
7205 $this->prepare_emoticon_object("(h)", 's/heart', 'heart'),
7206 $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'),
7207 $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'),
7208 $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'),
7209 $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'),
7210 $this->prepare_emoticon_object("( )", 's/egg', 'egg'),
7215 * Helper method preparing the stdClass with the emoticon properties
7217 * @param string|array $text or array of strings
7218 * @param string $imagename to be used by {@see pix_emoticon}
7219 * @param string $altidentifier alternative string identifier, null for no alt
7220 * @param array $altcomponent where the alternative string is defined
7221 * @param string $imagecomponent to be used by {@see pix_emoticon}
7222 * @return stdClass
7224 protected function prepare_emoticon_object($text, $imagename, $altidentifier = null, $altcomponent = 'core_pix', $imagecomponent = 'core') {
7225 return (object)array(
7226 'text' => $text,
7227 'imagename' => $imagename,
7228 'imagecomponent' => $imagecomponent,
7229 'altidentifier' => $altidentifier,
7230 'altcomponent' => $altcomponent,
7235 /// ENCRYPTION ////////////////////////////////////////////////
7238 * rc4encrypt
7240 * @todo Finish documenting this function
7242 * @param string $data Data to encrypt
7243 * @return string The now encrypted data
7245 function rc4encrypt($data) {
7246 $password = get_site_identifier();
7247 return endecrypt($password, $data, '');
7251 * rc4decrypt
7253 * @todo Finish documenting this function
7255 * @param string $data Data to decrypt
7256 * @return string The now decrypted data
7258 function rc4decrypt($data) {
7259 $password = get_site_identifier();
7260 return endecrypt($password, $data, 'de');
7264 * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com]
7266 * @todo Finish documenting this function
7268 * @param string $pwd The password to use when encrypting or decrypting
7269 * @param string $data The data to be decrypted/encrypted
7270 * @param string $case Either 'de' for decrypt or '' for encrypt
7271 * @return string
7273 function endecrypt ($pwd, $data, $case) {
7275 if ($case == 'de') {
7276 $data = urldecode($data);
7279 $key[] = '';
7280 $box[] = '';
7281 $temp_swap = '';
7282 $pwd_length = 0;
7284 $pwd_length = strlen($pwd);
7286 for ($i = 0; $i <= 255; $i++) {
7287 $key[$i] = ord(substr($pwd, ($i % $pwd_length), 1));
7288 $box[$i] = $i;
7291 $x = 0;
7293 for ($i = 0; $i <= 255; $i++) {
7294 $x = ($x + $box[$i] + $key[$i]) % 256;
7295 $temp_swap = $box[$i];
7296 $box[$i] = $box[$x];
7297 $box[$x] = $temp_swap;
7300 $temp = '';
7301 $k = '';
7303 $cipherby = '';
7304 $cipher = '';
7306 $a = 0;
7307 $j = 0;
7309 for ($i = 0; $i < strlen($data); $i++) {
7310 $a = ($a + 1) % 256;
7311 $j = ($j + $box[$a]) % 256;
7312 $temp = $box[$a];
7313 $box[$a] = $box[$j];
7314 $box[$j] = $temp;
7315 $k = $box[(($box[$a] + $box[$j]) % 256)];
7316 $cipherby = ord(substr($data, $i, 1)) ^ $k;
7317 $cipher .= chr($cipherby);
7320 if ($case == 'de') {
7321 $cipher = urldecode(urlencode($cipher));
7322 } else {
7323 $cipher = urlencode($cipher);
7326 return $cipher;
7329 /// ENVIRONMENT CHECKING ////////////////////////////////////////////////////////////
7332 * Returns the exact absolute path to plugin directory.
7334 * @param string $plugintype type of plugin
7335 * @param string $name name of the plugin
7336 * @return string full path to plugin directory; NULL if not found
7338 function get_plugin_directory($plugintype, $name) {
7339 global $CFG;
7341 if ($plugintype === '') {
7342 $plugintype = 'mod';
7345 $types = get_plugin_types(true);
7346 if (!array_key_exists($plugintype, $types)) {
7347 return NULL;
7349 $name = clean_param($name, PARAM_SAFEDIR); // just in case ;-)
7351 if (!empty($CFG->themedir) and $plugintype === 'theme') {
7352 if (!is_dir($types['theme'] . '/' . $name)) {
7353 // ok, so the theme is supposed to be in the $CFG->themedir
7354 return $CFG->themedir . '/' . $name;
7358 return $types[$plugintype].'/'.$name;
7362 * Return exact absolute path to a plugin directory,
7363 * this method support "simpletest_" prefix designed for unit testing.
7365 * @param string $component name such as 'moodle', 'mod_forum' or special simpletest value
7366 * @return string full path to component directory; NULL if not found
7368 function get_component_directory($component) {
7369 global $CFG;
7371 $simpletest = false;
7372 if (strpos($component, 'simpletest_') === 0) {
7373 $subdir = substr($component, strlen('simpletest_'));
7374 //TODO: this looks borked, where is it used actually?
7375 return $subdir;
7378 list($type, $plugin) = normalize_component($component);
7380 if ($type === 'core') {
7381 if ($plugin === NULL ) {
7382 $path = $CFG->libdir;
7383 } else {
7384 $subsystems = get_core_subsystems();
7385 if (isset($subsystems[$plugin])) {
7386 $path = $CFG->dirroot.'/'.$subsystems[$plugin];
7387 } else {
7388 $path = NULL;
7392 } else {
7393 $path = get_plugin_directory($type, $plugin);
7396 return $path;
7400 * Normalize the component name using the "frankenstyle" names.
7401 * @param string $component
7402 * @return array $type+$plugin elements
7404 function normalize_component($component) {
7405 if ($component === 'moodle' or $component === 'core') {
7406 $type = 'core';
7407 $plugin = NULL;
7409 } else if (strpos($component, '_') === false) {
7410 $subsystems = get_core_subsystems();
7411 if (array_key_exists($component, $subsystems)) {
7412 $type = 'core';
7413 $plugin = $component;
7414 } else {
7415 // everything else is a module
7416 $type = 'mod';
7417 $plugin = $component;
7420 } else {
7421 list($type, $plugin) = explode('_', $component, 2);
7422 $plugintypes = get_plugin_types(false);
7423 if ($type !== 'core' and !array_key_exists($type, $plugintypes)) {
7424 $type = 'mod';
7425 $plugin = $component;
7429 return array($type, $plugin);
7433 * List all core subsystems and their location
7435 * This is a whitelist of components that are part of the core and their
7436 * language strings are defined in /lang/en/<<subsystem>>.php. If a given
7437 * plugin is not listed here and it does not have proper plugintype prefix,
7438 * then it is considered as course activity module.
7440 * The location is dirroot relative path. NULL means there is no special
7441 * directory for this subsystem. If the location is set, the subsystem's
7442 * renderer.php is expected to be there.
7444 * @return array of (string)name => (string|null)location
7446 function get_core_subsystems() {
7447 global $CFG;
7449 static $info = null;
7451 if (!$info) {
7452 $info = array(
7453 'access' => NULL,
7454 'admin' => $CFG->admin,
7455 'auth' => 'auth',
7456 'backup' => 'backup/util/ui',
7457 'block' => 'blocks',
7458 'blog' => 'blog',
7459 'bulkusers' => NULL,
7460 'calendar' => 'calendar',
7461 'cohort' => 'cohort',
7462 'condition' => NULL,
7463 'completion' => NULL,
7464 'countries' => NULL,
7465 'course' => 'course',
7466 'currencies' => NULL,
7467 'dbtransfer' => NULL,
7468 'debug' => NULL,
7469 'dock' => NULL,
7470 'editor' => 'lib/editor',
7471 'edufields' => NULL,
7472 'enrol' => 'enrol',
7473 'error' => NULL,
7474 'filepicker' => NULL,
7475 'files' => 'files',
7476 'filters' => NULL,
7477 'fonts' => NULL,
7478 'form' => 'lib/form',
7479 'grades' => 'grade',
7480 'grading' => 'grade/grading',
7481 'group' => 'group',
7482 'help' => NULL,
7483 'hub' => NULL,
7484 'imscc' => NULL,
7485 'install' => NULL,
7486 'iso6392' => NULL,
7487 'langconfig' => NULL,
7488 'license' => NULL,
7489 'mathslib' => NULL,
7490 'message' => 'message',
7491 'message' => 'message',
7492 'mimetypes' => NULL,
7493 'mnet' => 'mnet',
7494 'moodle.org' => NULL, // the dot is nasty, watch out! should be renamed to moodleorg
7495 'my' => 'my',
7496 'notes' => 'notes',
7497 'pagetype' => NULL,
7498 'pix' => NULL,
7499 'plagiarism' => 'plagiarism',
7500 'plugin' => NULL,
7501 'portfolio' => 'portfolio',
7502 'publish' => 'course/publish',
7503 'question' => 'question',
7504 'rating' => 'rating',
7505 'register' => 'admin/registration', //TODO: this is wrong, unfortunately we would need to modify hub code to pass around the correct url
7506 'repository' => 'repository',
7507 'rss' => 'rss',
7508 'role' => $CFG->admin.'/role',
7509 'search' => 'search',
7510 'table' => NULL,
7511 'tag' => 'tag',
7512 'timezones' => NULL,
7513 'user' => 'user',
7514 'userkey' => NULL,
7515 'webservice' => 'webservice',
7519 return $info;
7523 * Lists all plugin types
7524 * @param bool $fullpaths false means relative paths from dirroot
7525 * @return array Array of strings - name=>location
7527 function get_plugin_types($fullpaths=true) {
7528 global $CFG;
7530 static $info = null;
7531 static $fullinfo = null;
7533 if (!$info) {
7534 $info = array('qtype' => 'question/type',
7535 'mod' => 'mod',
7536 'auth' => 'auth',
7537 'enrol' => 'enrol',
7538 'message' => 'message/output',
7539 'block' => 'blocks',
7540 'filter' => 'filter',
7541 'editor' => 'lib/editor',
7542 'format' => 'course/format',
7543 'profilefield' => 'user/profile/field',
7544 'report' => 'report',
7545 'coursereport' => 'course/report', // must be after system reports
7546 'gradeexport' => 'grade/export',
7547 'gradeimport' => 'grade/import',
7548 'gradereport' => 'grade/report',
7549 'gradingform' => 'grade/grading/form',
7550 'mnetservice' => 'mnet/service',
7551 'webservice' => 'webservice',
7552 'repository' => 'repository',
7553 'portfolio' => 'portfolio',
7554 'qbehaviour' => 'question/behaviour',
7555 'qformat' => 'question/format',
7556 'plagiarism' => 'plagiarism',
7557 'tool' => $CFG->admin.'/tool',
7558 'theme' => 'theme', // this is a bit hacky, themes may be in $CFG->themedir too
7561 $mods = get_plugin_list('mod');
7562 foreach ($mods as $mod => $moddir) {
7563 if (file_exists("$moddir/db/subplugins.php")) {
7564 $subplugins = array();
7565 include("$moddir/db/subplugins.php");
7566 foreach ($subplugins as $subtype=>$dir) {
7567 $info[$subtype] = $dir;
7572 // local is always last!
7573 $info['local'] = 'local';
7575 $fullinfo = array();
7576 foreach ($info as $type => $dir) {
7577 $fullinfo[$type] = $CFG->dirroot.'/'.$dir;
7581 return ($fullpaths ? $fullinfo : $info);
7585 * Simplified version of get_list_of_plugins()
7586 * @param string $plugintype type of plugin
7587 * @return array name=>fulllocation pairs of plugins of given type
7589 function get_plugin_list($plugintype) {
7590 global $CFG;
7592 $ignored = array('CVS', '_vti_cnf', 'simpletest', 'db', 'yui', 'phpunit');
7593 if ($plugintype == 'auth') {
7594 // Historically we have had an auth plugin called 'db', so allow a special case.
7595 $key = array_search('db', $ignored);
7596 if ($key !== false) {
7597 unset($ignored[$key]);
7601 if ($plugintype === '') {
7602 $plugintype = 'mod';
7605 $fulldirs = array();
7607 if ($plugintype === 'mod') {
7608 // mod is an exception because we have to call this function from get_plugin_types()
7609 $fulldirs[] = $CFG->dirroot.'/mod';
7611 } else if ($plugintype === 'theme') {
7612 $fulldirs[] = $CFG->dirroot.'/theme';
7613 // themes are special because they may be stored also in separate directory
7614 if (!empty($CFG->themedir) and file_exists($CFG->themedir) and is_dir($CFG->themedir) ) {
7615 $fulldirs[] = $CFG->themedir;
7618 } else {
7619 $types = get_plugin_types(true);
7620 if (!array_key_exists($plugintype, $types)) {
7621 return array();
7623 $fulldir = $types[$plugintype];
7624 if (!file_exists($fulldir)) {
7625 return array();
7627 $fulldirs[] = $fulldir;
7630 $result = array();
7632 foreach ($fulldirs as $fulldir) {
7633 if (!is_dir($fulldir)) {
7634 continue;
7636 $items = new DirectoryIterator($fulldir);
7637 foreach ($items as $item) {
7638 if ($item->isDot() or !$item->isDir()) {
7639 continue;
7641 $pluginname = $item->getFilename();
7642 if (in_array($pluginname, $ignored)) {
7643 continue;
7645 $pluginname = clean_param($pluginname, PARAM_PLUGIN);
7646 if (empty($pluginname)) {
7647 // better ignore plugins with problematic names here
7648 continue;
7650 $result[$pluginname] = $fulldir.'/'.$pluginname;
7651 unset($item);
7653 unset($items);
7656 //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!
7657 ksort($result);
7658 return $result;
7662 * Get a list of all the plugins of a given type that contain a particular file.
7663 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
7664 * @param string $file the name of file that must be present in the plugin.
7665 * (e.g. 'view.php', 'db/install.xml').
7666 * @param bool $include if true (default false), the file will be include_once-ed if found.
7667 * @return array with plugin name as keys (e.g. 'forum', 'courselist') and the path
7668 * to the file relative to dirroot as value (e.g. "$CFG->dirroot/mod/forum/view.php").
7670 function get_plugin_list_with_file($plugintype, $file, $include = false) {
7671 global $CFG; // Necessary in case it is referenced by include()d PHP scripts.
7673 $plugins = array();
7675 foreach(get_plugin_list($plugintype) as $plugin => $dir) {
7676 $path = $dir . '/' . $file;
7677 if (file_exists($path)) {
7678 if ($include) {
7679 include_once($path);
7681 $plugins[$plugin] = $path;
7685 return $plugins;
7689 * Get a list of all the plugins of a given type that define a certain API function
7690 * in a certain file. The plugin component names and function names are returned.
7692 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
7693 * @param string $function the part of the name of the function after the
7694 * frankenstyle prefix. e.g 'hook' if you are looking for functions with
7695 * names like report_courselist_hook.
7696 * @param string $file the name of file within the plugin that defines the
7697 * function. Defaults to lib.php.
7698 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
7699 * and the function names as values (e.g. 'report_courselist_hook', 'forum_hook').
7701 function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php') {
7702 $pluginfunctions = array();
7703 foreach (get_plugin_list_with_file($plugintype, $file, true) as $plugin => $notused) {
7704 $fullfunction = $plugintype . '_' . $plugin . '_' . $function;
7706 if (function_exists($fullfunction)) {
7707 // Function exists with standard name. Store, indexed by
7708 // frankenstyle name of plugin
7709 $pluginfunctions[$plugintype . '_' . $plugin] = $fullfunction;
7711 } else if ($plugintype === 'mod') {
7712 // For modules, we also allow plugin without full frankenstyle
7713 // but just starting with the module name
7714 $shortfunction = $plugin . '_' . $function;
7715 if (function_exists($shortfunction)) {
7716 $pluginfunctions[$plugintype . '_' . $plugin] = $shortfunction;
7720 return $pluginfunctions;
7724 * Get a list of all the plugins of a given type that define a certain class
7725 * in a certain file. The plugin component names and class names are returned.
7727 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
7728 * @param string $class the part of the name of the class after the
7729 * frankenstyle prefix. e.g 'thing' if you are looking for classes with
7730 * names like report_courselist_thing. If you are looking for classes with
7731 * the same name as the plugin name (e.g. qtype_multichoice) then pass ''.
7732 * @param string $file the name of file within the plugin that defines the class.
7733 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
7734 * and the class names as values (e.g. 'report_courselist_thing', 'qtype_multichoice').
7736 function get_plugin_list_with_class($plugintype, $class, $file) {
7737 if ($class) {
7738 $suffix = '_' . $class;
7739 } else {
7740 $suffix = '';
7743 $pluginclasses = array();
7744 foreach (get_plugin_list_with_file($plugintype, $file, true) as $plugin => $notused) {
7745 $classname = $plugintype . '_' . $plugin . $suffix;
7746 if (class_exists($classname)) {
7747 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
7751 return $pluginclasses;
7755 * Lists plugin-like directories within specified directory
7757 * This function was originally used for standard Moodle plugins, please use
7758 * new get_plugin_list() now.
7760 * This function is used for general directory listing and backwards compatility.
7762 * @param string $directory relative directory from root
7763 * @param string $exclude dir name to exclude from the list (defaults to none)
7764 * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot)
7765 * @return array Sorted array of directory names found under the requested parameters
7767 function get_list_of_plugins($directory='mod', $exclude='', $basedir='') {
7768 global $CFG;
7770 $plugins = array();
7772 if (empty($basedir)) {
7773 $basedir = $CFG->dirroot .'/'. $directory;
7775 } else {
7776 $basedir = $basedir .'/'. $directory;
7779 if (file_exists($basedir) && filetype($basedir) == 'dir') {
7780 $dirhandle = opendir($basedir);
7781 while (false !== ($dir = readdir($dirhandle))) {
7782 $firstchar = substr($dir, 0, 1);
7783 if ($firstchar === '.' or $dir === 'CVS' or $dir === '_vti_cnf' or $dir === 'simpletest' or $dir === 'yui' or $dir === 'phpunit' or $dir === $exclude) {
7784 continue;
7786 if (filetype($basedir .'/'. $dir) != 'dir') {
7787 continue;
7789 $plugins[] = $dir;
7791 closedir($dirhandle);
7793 if ($plugins) {
7794 asort($plugins);
7796 return $plugins;
7800 * Invoke plugin's callback functions
7802 * @param string $type plugin type e.g. 'mod'
7803 * @param string $name plugin name
7804 * @param string $feature feature name
7805 * @param string $action feature's action
7806 * @param array $params parameters of callback function, should be an array
7807 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
7808 * @return mixed
7810 * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743
7812 function plugin_callback($type, $name, $feature, $action, $params = null, $default = null) {
7813 return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default);
7817 * Invoke component's callback functions
7819 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
7820 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
7821 * @param array $params parameters of callback function
7822 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
7823 * @return mixed
7825 function component_callback($component, $function, array $params = array(), $default = null) {
7826 global $CFG; // this is needed for require_once() bellow
7828 $cleancomponent = clean_param($component, PARAM_COMPONENT);
7829 if (empty($cleancomponent)) {
7830 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
7832 $component = $cleancomponent;
7834 list($type, $name) = normalize_component($component);
7835 $component = $type . '_' . $name;
7837 $oldfunction = $name.'_'.$function;
7838 $function = $component.'_'.$function;
7840 $dir = get_component_directory($component);
7841 if (empty($dir)) {
7842 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
7845 // Load library and look for function
7846 if (file_exists($dir.'/lib.php')) {
7847 require_once($dir.'/lib.php');
7850 if (!function_exists($function) and function_exists($oldfunction)) {
7851 if ($type !== 'mod' and $type !== 'core') {
7852 debugging("Please use new function name $function instead of legacy $oldfunction");
7854 $function = $oldfunction;
7857 if (function_exists($function)) {
7858 // Function exists, so just return function result
7859 $ret = call_user_func_array($function, $params);
7860 if (is_null($ret)) {
7861 return $default;
7862 } else {
7863 return $ret;
7866 return $default;
7870 * Checks whether a plugin supports a specified feature.
7872 * @param string $type Plugin type e.g. 'mod'
7873 * @param string $name Plugin name e.g. 'forum'
7874 * @param string $feature Feature code (FEATURE_xx constant)
7875 * @param mixed $default default value if feature support unknown
7876 * @return mixed Feature result (false if not supported, null if feature is unknown,
7877 * otherwise usually true but may have other feature-specific value such as array)
7879 function plugin_supports($type, $name, $feature, $default = NULL) {
7880 global $CFG;
7882 if ($type === 'mod' and $name === 'NEWMODULE') {
7883 //somebody forgot to rename the module template
7884 return false;
7887 $component = clean_param($type . '_' . $name, PARAM_COMPONENT);
7888 if (empty($component)) {
7889 throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name);
7892 $function = null;
7894 if ($type === 'mod') {
7895 // we need this special case because we support subplugins in modules,
7896 // otherwise it would end up in infinite loop
7897 if (file_exists("$CFG->dirroot/mod/$name/lib.php")) {
7898 include_once("$CFG->dirroot/mod/$name/lib.php");
7899 $function = $component.'_supports';
7900 if (!function_exists($function)) {
7901 // legacy non-frankenstyle function name
7902 $function = $name.'_supports';
7904 } else {
7905 // invalid module
7908 } else {
7909 if (!$path = get_plugin_directory($type, $name)) {
7910 // non existent plugin type
7911 return false;
7913 if (file_exists("$path/lib.php")) {
7914 include_once("$path/lib.php");
7915 $function = $component.'_supports';
7919 if ($function and function_exists($function)) {
7920 $supports = $function($feature);
7921 if (is_null($supports)) {
7922 // plugin does not know - use default
7923 return $default;
7924 } else {
7925 return $supports;
7929 //plugin does not care, so use default
7930 return $default;
7934 * Returns true if the current version of PHP is greater that the specified one.
7936 * @todo Check PHP version being required here is it too low?
7938 * @param string $version The version of php being tested.
7939 * @return bool
7941 function check_php_version($version='5.2.4') {
7942 return (version_compare(phpversion(), $version) >= 0);
7946 * Checks to see if is the browser operating system matches the specified
7947 * brand.
7949 * Known brand: 'Windows','Linux','Macintosh','SGI','SunOS','HP-UX'
7951 * @uses $_SERVER
7952 * @param string $brand The operating system identifier being tested
7953 * @return bool true if the given brand below to the detected operating system
7955 function check_browser_operating_system($brand) {
7956 if (empty($_SERVER['HTTP_USER_AGENT'])) {
7957 return false;
7960 if (preg_match("/$brand/i", $_SERVER['HTTP_USER_AGENT'])) {
7961 return true;
7964 return false;
7968 * Checks to see if is a browser matches the specified
7969 * brand and is equal or better version.
7971 * @uses $_SERVER
7972 * @param string $brand The browser identifier being tested
7973 * @param int $version The version of the browser, if not specified any version (except 5.5 for IE for BC reasons)
7974 * @return bool true if the given version is below that of the detected browser
7976 function check_browser_version($brand, $version = null) {
7977 if (empty($_SERVER['HTTP_USER_AGENT'])) {
7978 return false;
7981 $agent = $_SERVER['HTTP_USER_AGENT'];
7983 switch ($brand) {
7985 case 'Camino': /// OSX browser using Gecke engine
7986 if (strpos($agent, 'Camino') === false) {
7987 return false;
7989 if (empty($version)) {
7990 return true; // no version specified
7992 if (preg_match("/Camino\/([0-9\.]+)/i", $agent, $match)) {
7993 if (version_compare($match[1], $version) >= 0) {
7994 return true;
7997 break;
8000 case 'Firefox': /// Mozilla Firefox browsers
8001 if (strpos($agent, 'Iceweasel') === false and strpos($agent, 'Firefox') === false) {
8002 return false;
8004 if (empty($version)) {
8005 return true; // no version specified
8007 if (preg_match("/(Iceweasel|Firefox)\/([0-9\.]+)/i", $agent, $match)) {
8008 if (version_compare($match[2], $version) >= 0) {
8009 return true;
8012 break;
8015 case 'Gecko': /// Gecko based browsers
8016 if (empty($version) and substr_count($agent, 'Camino')) {
8017 // MacOS X Camino support
8018 $version = 20041110;
8021 // the proper string - Gecko/CCYYMMDD Vendor/Version
8022 // Faster version and work-a-round No IDN problem.
8023 if (preg_match("/Gecko\/([0-9]+)/i", $agent, $match)) {
8024 if ($match[1] > $version) {
8025 return true;
8028 break;
8031 case 'MSIE': /// Internet Explorer
8032 if (strpos($agent, 'Opera') !== false) { // Reject Opera
8033 return false;
8035 // in case of IE we have to deal with BC of the version parameter
8036 if (is_null($version)) {
8037 $version = 5.5; // anything older is not considered a browser at all!
8040 //see: http://www.useragentstring.com/pages/Internet%20Explorer/
8041 if (preg_match("/MSIE ([0-9\.]+)/", $agent, $match)) {
8042 if (version_compare($match[1], $version) >= 0) {
8043 return true;
8046 break;
8049 case 'Opera': /// Opera
8050 if (strpos($agent, 'Opera') === false) {
8051 return false;
8053 if (empty($version)) {
8054 return true; // no version specified
8056 if (preg_match("/Opera\/([0-9\.]+)/i", $agent, $match)) {
8057 if (version_compare($match[1], $version) >= 0) {
8058 return true;
8061 break;
8064 case 'WebKit': /// WebKit based browser - everything derived from it (Safari, Chrome, iOS, Android and other mobiles)
8065 if (strpos($agent, 'AppleWebKit') === false) {
8066 return false;
8068 if (empty($version)) {
8069 return true; // no version specified
8071 if (preg_match("/AppleWebKit\/([0-9]+)/i", $agent, $match)) {
8072 if (version_compare($match[1], $version) >= 0) {
8073 return true;
8076 break;
8079 case 'Safari': /// Desktop version of Apple Safari browser - no mobile or touch devices
8080 if (strpos($agent, 'AppleWebKit') === false) {
8081 return false;
8083 // Look for AppleWebKit, excluding strings with OmniWeb, Shiira and SymbianOS and any other mobile devices
8084 if (strpos($agent, 'OmniWeb')) { // Reject OmniWeb
8085 return false;
8087 if (strpos($agent, 'Shiira')) { // Reject Shiira
8088 return false;
8090 if (strpos($agent, 'SymbianOS')) { // Reject SymbianOS
8091 return false;
8093 if (strpos($agent, 'Android')) { // Reject Androids too
8094 return false;
8096 if (strpos($agent, 'iPhone') or strpos($agent, 'iPad') or strpos($agent, 'iPod')) {
8097 // No Apple mobile devices here - editor does not work, course ajax is not touch compatible, etc.
8098 return false;
8100 if (strpos($agent, 'Chrome')) { // Reject chrome browsers - it needs to be tested explicitly
8101 return false;
8104 if (empty($version)) {
8105 return true; // no version specified
8107 if (preg_match("/AppleWebKit\/([0-9]+)/i", $agent, $match)) {
8108 if (version_compare($match[1], $version) >= 0) {
8109 return true;
8112 break;
8115 case 'Chrome':
8116 if (strpos($agent, 'Chrome') === false) {
8117 return false;
8119 if (empty($version)) {
8120 return true; // no version specified
8122 if (preg_match("/Chrome\/(.*)[ ]+/i", $agent, $match)) {
8123 if (version_compare($match[1], $version) >= 0) {
8124 return true;
8127 break;
8130 case 'Safari iOS': /// Safari on iPhone, iPad and iPod touch
8131 if (strpos($agent, 'AppleWebKit') === false or strpos($agent, 'Safari') === false) {
8132 return false;
8134 if (!strpos($agent, 'iPhone') and !strpos($agent, 'iPad') and !strpos($agent, 'iPod')) {
8135 return false;
8137 if (empty($version)) {
8138 return true; // no version specified
8140 if (preg_match("/AppleWebKit\/([0-9]+)/i", $agent, $match)) {
8141 if (version_compare($match[1], $version) >= 0) {
8142 return true;
8145 break;
8148 case 'WebKit Android': /// WebKit browser on Android
8149 if (strpos($agent, 'Linux; U; Android') === false) {
8150 return false;
8152 if (empty($version)) {
8153 return true; // no version specified
8155 if (preg_match("/AppleWebKit\/([0-9]+)/i", $agent, $match)) {
8156 if (version_compare($match[1], $version) >= 0) {
8157 return true;
8160 break;
8164 return false;
8168 * Returns whether a device/browser combination is mobile, tablet, legacy, default or the result of
8169 * an optional admin specified regular expression. If enabledevicedetection is set to no or not set
8170 * it returns default
8172 * @return string device type
8174 function get_device_type() {
8175 global $CFG;
8177 if (empty($CFG->enabledevicedetection) || empty($_SERVER['HTTP_USER_AGENT'])) {
8178 return 'default';
8181 $useragent = $_SERVER['HTTP_USER_AGENT'];
8183 if (!empty($CFG->devicedetectregex)) {
8184 $regexes = json_decode($CFG->devicedetectregex);
8186 foreach ($regexes as $value=>$regex) {
8187 if (preg_match($regex, $useragent)) {
8188 return $value;
8193 //mobile detection PHP direct copy from open source detectmobilebrowser.com
8194 $phonesregex = '/android|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';
8195 $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';
8196 if (preg_match($phonesregex,$useragent) || preg_match($modelsregex,substr($useragent, 0, 4))){
8197 return 'mobile';
8200 $tabletregex = '/Tablet browser|iPad|iProd|GT-P1000|GT-I9000|SHW-M180S|SGH-T849|SCH-I800|Build\/ERE27|sholest/i';
8201 if (preg_match($tabletregex, $useragent)) {
8202 return 'tablet';
8205 if (strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE 6.') !== false) {
8206 return 'legacy';
8209 return 'default';
8213 * Returns a list of the device types supporting by Moodle
8215 * @param boolean $incusertypes includes types specified using the devicedetectregex admin setting
8216 * @return array $types
8218 function get_device_type_list($incusertypes = true) {
8219 global $CFG;
8221 $types = array('default', 'legacy', 'mobile', 'tablet');
8223 if ($incusertypes && !empty($CFG->devicedetectregex)) {
8224 $regexes = json_decode($CFG->devicedetectregex);
8226 foreach ($regexes as $value => $regex) {
8227 $types[] = $value;
8231 return $types;
8235 * Returns the theme selected for a particular device or false if none selected.
8237 * @param string $devicetype
8238 * @return string|false The name of the theme to use for the device or the false if not set
8240 function get_selected_theme_for_device_type($devicetype = null) {
8241 global $CFG;
8243 if (empty($devicetype)) {
8244 $devicetype = get_user_device_type();
8247 $themevarname = get_device_cfg_var_name($devicetype);
8248 if (empty($CFG->$themevarname)) {
8249 return false;
8252 return $CFG->$themevarname;
8256 * Returns the name of the device type theme var in $CFG (because there is not a standard convention to allow backwards compatability
8258 * @param string $devicetype
8259 * @return string The config variable to use to determine the theme
8261 function get_device_cfg_var_name($devicetype = null) {
8262 if ($devicetype == 'default' || empty($devicetype)) {
8263 return 'theme';
8266 return 'theme' . $devicetype;
8270 * Allows the user to switch the device they are seeing the theme for.
8271 * This allows mobile users to switch back to the default theme, or theme for any other device.
8273 * @param string $newdevice The device the user is currently using.
8274 * @return string The device the user has switched to
8276 function set_user_device_type($newdevice) {
8277 global $USER;
8279 $devicetype = get_device_type();
8280 $devicetypes = get_device_type_list();
8282 if ($newdevice == $devicetype) {
8283 unset_user_preference('switchdevice'.$devicetype);
8284 } else if (in_array($newdevice, $devicetypes)) {
8285 set_user_preference('switchdevice'.$devicetype, $newdevice);
8290 * Returns the device the user is currently using, or if the user has chosen to switch devices
8291 * for the current device type the type they have switched to.
8293 * @return string The device the user is currently using or wishes to use
8295 function get_user_device_type() {
8296 $device = get_device_type();
8297 $switched = get_user_preferences('switchdevice'.$device, false);
8298 if ($switched != false) {
8299 return $switched;
8301 return $device;
8305 * Returns one or several CSS class names that match the user's browser. These can be put
8306 * in the body tag of the page to apply browser-specific rules without relying on CSS hacks
8308 * @return array An array of browser version classes
8310 function get_browser_version_classes() {
8311 $classes = array();
8313 if (check_browser_version("MSIE", "0")) {
8314 $classes[] = 'ie';
8315 if (check_browser_version("MSIE", 9)) {
8316 $classes[] = 'ie9';
8317 } else if (check_browser_version("MSIE", 8)) {
8318 $classes[] = 'ie8';
8319 } elseif (check_browser_version("MSIE", 7)) {
8320 $classes[] = 'ie7';
8321 } elseif (check_browser_version("MSIE", 6)) {
8322 $classes[] = 'ie6';
8325 } else if (check_browser_version("Firefox") || check_browser_version("Gecko") || check_browser_version("Camino")) {
8326 $classes[] = 'gecko';
8327 if (preg_match('/rv\:([1-2])\.([0-9])/', $_SERVER['HTTP_USER_AGENT'], $matches)) {
8328 $classes[] = "gecko{$matches[1]}{$matches[2]}";
8331 } else if (check_browser_version("WebKit")) {
8332 $classes[] = 'safari';
8333 if (check_browser_version("Safari iOS")) {
8334 $classes[] = 'ios';
8336 } else if (check_browser_version("WebKit Android")) {
8337 $classes[] = 'android';
8340 } else if (check_browser_version("Opera")) {
8341 $classes[] = 'opera';
8345 return $classes;
8349 * Can handle rotated text. Whether it is safe to use the trickery in textrotate.js.
8351 * @return bool True for yes, false for no
8353 function can_use_rotated_text() {
8354 global $USER;
8355 return ajaxenabled(array('Firefox' => 2.0)) && !$USER->screenreader;;
8359 * Hack to find out the GD version by parsing phpinfo output
8361 * @return int GD version (1, 2, or 0)
8363 function check_gd_version() {
8364 $gdversion = 0;
8366 if (function_exists('gd_info')){
8367 $gd_info = gd_info();
8368 if (substr_count($gd_info['GD Version'], '2.')) {
8369 $gdversion = 2;
8370 } else if (substr_count($gd_info['GD Version'], '1.')) {
8371 $gdversion = 1;
8374 } else {
8375 ob_start();
8376 phpinfo(INFO_MODULES);
8377 $phpinfo = ob_get_contents();
8378 ob_end_clean();
8380 $phpinfo = explode("\n", $phpinfo);
8383 foreach ($phpinfo as $text) {
8384 $parts = explode('</td>', $text);
8385 foreach ($parts as $key => $val) {
8386 $parts[$key] = trim(strip_tags($val));
8388 if ($parts[0] == 'GD Version') {
8389 if (substr_count($parts[1], '2.0')) {
8390 $parts[1] = '2.0';
8392 $gdversion = intval($parts[1]);
8397 return $gdversion; // 1, 2 or 0
8401 * Determine if moodle installation requires update
8403 * Checks version numbers of main code and all modules to see
8404 * if there are any mismatches
8406 * @global object
8407 * @global object
8408 * @return bool
8410 function moodle_needs_upgrading() {
8411 global $CFG, $DB, $OUTPUT;
8413 if (empty($CFG->version)) {
8414 return true;
8417 // main versio nfirst
8418 $version = null;
8419 include($CFG->dirroot.'/version.php'); // defines $version and upgrades
8420 if ($version > $CFG->version) {
8421 return true;
8424 // modules
8425 $mods = get_plugin_list('mod');
8426 $installed = $DB->get_records('modules', array(), '', 'name, version');
8427 foreach ($mods as $mod => $fullmod) {
8428 if ($mod === 'NEWMODULE') { // Someone has unzipped the template, ignore it
8429 continue;
8431 $module = new stdClass();
8432 if (!is_readable($fullmod.'/version.php')) {
8433 continue;
8435 include($fullmod.'/version.php'); // defines $module with version etc
8436 if (empty($installed[$mod])) {
8437 return true;
8438 } else if ($module->version > $installed[$mod]->version) {
8439 return true;
8442 unset($installed);
8444 // blocks
8445 $blocks = get_plugin_list('block');
8446 $installed = $DB->get_records('block', array(), '', 'name, version');
8447 require_once($CFG->dirroot.'/blocks/moodleblock.class.php');
8448 foreach ($blocks as $blockname=>$fullblock) {
8449 if ($blockname === 'NEWBLOCK') { // Someone has unzipped the template, ignore it
8450 continue;
8452 if (!is_readable($fullblock.'/version.php')) {
8453 continue;
8455 $plugin = new stdClass();
8456 $plugin->version = NULL;
8457 include($fullblock.'/version.php');
8458 if (empty($installed[$blockname])) {
8459 return true;
8460 } else if ($plugin->version > $installed[$blockname]->version) {
8461 return true;
8464 unset($installed);
8466 // now the rest of plugins
8467 $plugintypes = get_plugin_types();
8468 unset($plugintypes['mod']);
8469 unset($plugintypes['block']);
8470 foreach ($plugintypes as $type=>$unused) {
8471 $plugs = get_plugin_list($type);
8472 foreach ($plugs as $plug=>$fullplug) {
8473 $component = $type.'_'.$plug;
8474 if (!is_readable($fullplug.'/version.php')) {
8475 continue;
8477 $plugin = new stdClass();
8478 include($fullplug.'/version.php'); // defines $plugin with version etc
8479 $installedversion = get_config($component, 'version');
8480 if (empty($installedversion)) { // new installation
8481 return true;
8482 } else if ($installedversion < $plugin->version) { // upgrade
8483 return true;
8488 return false;
8492 * Sets maximum expected time needed for upgrade task.
8493 * Please always make sure that upgrade will not run longer!
8495 * The script may be automatically aborted if upgrade times out.
8497 * @global object
8498 * @param int $max_execution_time in seconds (can not be less than 60 s)
8500 function upgrade_set_timeout($max_execution_time=300) {
8501 global $CFG;
8503 if (!isset($CFG->upgraderunning) or $CFG->upgraderunning < time()) {
8504 $upgraderunning = get_config(null, 'upgraderunning');
8505 } else {
8506 $upgraderunning = $CFG->upgraderunning;
8509 if (!$upgraderunning) {
8510 // upgrade not running or aborted
8511 print_error('upgradetimedout', 'admin', "$CFG->wwwroot/$CFG->admin/");
8512 die;
8515 if ($max_execution_time < 60) {
8516 // protection against 0 here
8517 $max_execution_time = 60;
8520 $expected_end = time() + $max_execution_time;
8522 if ($expected_end < $upgraderunning + 10 and $expected_end > $upgraderunning - 10) {
8523 // no need to store new end, it is nearly the same ;-)
8524 return;
8527 set_time_limit($max_execution_time);
8528 set_config('upgraderunning', $expected_end); // keep upgrade locked until this time
8531 /// MISCELLANEOUS ////////////////////////////////////////////////////////////////////
8534 * Sets the system locale
8536 * @todo Finish documenting this function
8538 * @global object
8539 * @param string $locale Can be used to force a locale
8541 function moodle_setlocale($locale='') {
8542 global $CFG;
8544 static $currentlocale = ''; // last locale caching
8546 $oldlocale = $currentlocale;
8548 /// Fetch the correct locale based on ostype
8549 if ($CFG->ostype == 'WINDOWS') {
8550 $stringtofetch = 'localewin';
8551 } else {
8552 $stringtofetch = 'locale';
8555 /// the priority is the same as in get_string() - parameter, config, course, session, user, global language
8556 if (!empty($locale)) {
8557 $currentlocale = $locale;
8558 } else if (!empty($CFG->locale)) { // override locale for all language packs
8559 $currentlocale = $CFG->locale;
8560 } else {
8561 $currentlocale = get_string($stringtofetch, 'langconfig');
8564 /// do nothing if locale already set up
8565 if ($oldlocale == $currentlocale) {
8566 return;
8569 /// Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values,
8570 /// set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7
8571 /// Some day, numeric, monetary and other categories should be set too, I think. :-/
8573 /// Get current values
8574 $monetary= setlocale (LC_MONETARY, 0);
8575 $numeric = setlocale (LC_NUMERIC, 0);
8576 $ctype = setlocale (LC_CTYPE, 0);
8577 if ($CFG->ostype != 'WINDOWS') {
8578 $messages= setlocale (LC_MESSAGES, 0);
8580 /// Set locale to all
8581 setlocale (LC_ALL, $currentlocale);
8582 /// Set old values
8583 setlocale (LC_MONETARY, $monetary);
8584 setlocale (LC_NUMERIC, $numeric);
8585 if ($CFG->ostype != 'WINDOWS') {
8586 setlocale (LC_MESSAGES, $messages);
8588 if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') { // To workaround a well-known PHP problem with Turkish letter Ii
8589 setlocale (LC_CTYPE, $ctype);
8594 * Converts string to lowercase using most compatible function available.
8596 * @todo Remove this function when no longer in use
8597 * @deprecated Use textlib->strtolower($text) instead.
8599 * @param string $string The string to convert to all lowercase characters.
8600 * @param string $encoding The encoding on the string.
8601 * @return string
8603 function moodle_strtolower ($string, $encoding='') {
8605 //If not specified use utf8
8606 if (empty($encoding)) {
8607 $encoding = 'UTF-8';
8609 //Use text services
8610 $textlib = textlib_get_instance();
8612 return $textlib->strtolower($string, $encoding);
8616 * Count words in a string.
8618 * Words are defined as things between whitespace.
8620 * @param string $string The text to be searched for words.
8621 * @return int The count of words in the specified string
8623 function count_words($string) {
8624 $string = strip_tags($string);
8625 return count(preg_split("/\w\b/", $string)) - 1;
8628 /** Count letters in a string.
8630 * Letters are defined as chars not in tags and different from whitespace.
8632 * @param string $string The text to be searched for letters.
8633 * @return int The count of letters in the specified text.
8635 function count_letters($string) {
8636 /// Loading the textlib singleton instance. We are going to need it.
8637 $textlib = textlib_get_instance();
8639 $string = strip_tags($string); // Tags are out now
8640 $string = preg_replace('/[[:space:]]*/','',$string); //Whitespace are out now
8642 return $textlib->strlen($string);
8646 * Generate and return a random string of the specified length.
8648 * @param int $length The length of the string to be created.
8649 * @return string
8651 function random_string ($length=15) {
8652 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
8653 $pool .= 'abcdefghijklmnopqrstuvwxyz';
8654 $pool .= '0123456789';
8655 $poollen = strlen($pool);
8656 mt_srand ((double) microtime() * 1000000);
8657 $string = '';
8658 for ($i = 0; $i < $length; $i++) {
8659 $string .= substr($pool, (mt_rand()%($poollen)), 1);
8661 return $string;
8665 * Generate a complex random string (useful for md5 salts)
8667 * This function is based on the above {@link random_string()} however it uses a
8668 * larger pool of characters and generates a string between 24 and 32 characters
8670 * @param int $length Optional if set generates a string to exactly this length
8671 * @return string
8673 function complex_random_string($length=null) {
8674 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
8675 $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
8676 $poollen = strlen($pool);
8677 mt_srand ((double) microtime() * 1000000);
8678 if ($length===null) {
8679 $length = floor(rand(24,32));
8681 $string = '';
8682 for ($i = 0; $i < $length; $i++) {
8683 $string .= $pool[(mt_rand()%$poollen)];
8685 return $string;
8689 * Given some text (which may contain HTML) and an ideal length,
8690 * this function truncates the text neatly on a word boundary if possible
8692 * @global object
8693 * @param string $text - text to be shortened
8694 * @param int $ideal - ideal string length
8695 * @param boolean $exact if false, $text will not be cut mid-word
8696 * @param string $ending The string to append if the passed string is truncated
8697 * @return string $truncate - shortened string
8699 function shorten_text($text, $ideal=30, $exact = false, $ending='...') {
8701 global $CFG;
8703 // if the plain text is shorter than the maximum length, return the whole text
8704 if (strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) {
8705 return $text;
8708 // Splits on HTML tags. Each open/close/empty tag will be the first thing
8709 // and only tag in its 'line'
8710 preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
8712 $total_length = strlen($ending);
8713 $truncate = '';
8715 // This array stores information about open and close tags and their position
8716 // in the truncated string. Each item in the array is an object with fields
8717 // ->open (true if open), ->tag (tag name in lower case), and ->pos
8718 // (byte position in truncated text)
8719 $tagdetails = array();
8721 foreach ($lines as $line_matchings) {
8722 // if there is any html-tag in this line, handle it and add it (uncounted) to the output
8723 if (!empty($line_matchings[1])) {
8724 // if it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>)
8725 if (preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $line_matchings[1])) {
8726 // do nothing
8727 // if tag is a closing tag (f.e. </b>)
8728 } else if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $line_matchings[1], $tag_matchings)) {
8729 // record closing tag
8730 $tagdetails[] = (object)array('open'=>false,
8731 'tag'=>strtolower($tag_matchings[1]), 'pos'=>strlen($truncate));
8732 // if tag is an opening tag (f.e. <b>)
8733 } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $line_matchings[1], $tag_matchings)) {
8734 // record opening tag
8735 $tagdetails[] = (object)array('open'=>true,
8736 'tag'=>strtolower($tag_matchings[1]), 'pos'=>strlen($truncate));
8738 // add html-tag to $truncate'd text
8739 $truncate .= $line_matchings[1];
8742 // calculate the length of the plain text part of the line; handle entities as one character
8743 $content_length = strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $line_matchings[2]));
8744 if ($total_length+$content_length > $ideal) {
8745 // the number of characters which are left
8746 $left = $ideal - $total_length;
8747 $entities_length = 0;
8748 // search for html entities
8749 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)) {
8750 // calculate the real length of all entities in the legal range
8751 foreach ($entities[0] as $entity) {
8752 if ($entity[1]+1-$entities_length <= $left) {
8753 $left--;
8754 $entities_length += strlen($entity[0]);
8755 } else {
8756 // no more characters left
8757 break;
8761 $truncate .= substr($line_matchings[2], 0, $left+$entities_length);
8762 // maximum length is reached, so get off the loop
8763 break;
8764 } else {
8765 $truncate .= $line_matchings[2];
8766 $total_length += $content_length;
8769 // if the maximum length is reached, get off the loop
8770 if($total_length >= $ideal) {
8771 break;
8775 // if the words shouldn't be cut in the middle...
8776 if (!$exact) {
8777 // ...search the last occurence of a space...
8778 for ($k=strlen($truncate);$k>0;$k--) {
8779 if (!empty($truncate[$k]) && ($char = $truncate[$k])) {
8780 if ($char == '.' or $char == ' ') {
8781 $breakpos = $k+1;
8782 break;
8783 } else if (ord($char) >= 0xE0) { // Chinese/Japanese/Korean text
8784 $breakpos = $k; // can be truncated at any UTF-8
8785 break; // character boundary.
8790 if (isset($breakpos)) {
8791 // ...and cut the text in this position
8792 $truncate = substr($truncate, 0, $breakpos);
8796 // add the defined ending to the text
8797 $truncate .= $ending;
8799 // Now calculate the list of open html tags based on the truncate position
8800 $open_tags = array();
8801 foreach ($tagdetails as $taginfo) {
8802 if(isset($breakpos) && $taginfo->pos >= $breakpos) {
8803 // Don't include tags after we made the break!
8804 break;
8806 if($taginfo->open) {
8807 // add tag to the beginning of $open_tags list
8808 array_unshift($open_tags, $taginfo->tag);
8809 } else {
8810 $pos = array_search($taginfo->tag, array_reverse($open_tags, true)); // can have multiple exact same open tags, close the last one
8811 if ($pos !== false) {
8812 unset($open_tags[$pos]);
8817 // close all unclosed html-tags
8818 foreach ($open_tags as $tag) {
8819 $truncate .= '</' . $tag . '>';
8822 return $truncate;
8827 * Given dates in seconds, how many weeks is the date from startdate
8828 * The first week is 1, the second 2 etc ...
8830 * @todo Finish documenting this function
8832 * @uses WEEKSECS
8833 * @param int $startdate Timestamp for the start date
8834 * @param int $thedate Timestamp for the end date
8835 * @return string
8837 function getweek ($startdate, $thedate) {
8838 if ($thedate < $startdate) { // error
8839 return 0;
8842 return floor(($thedate - $startdate) / WEEKSECS) + 1;
8846 * returns a randomly generated password of length $maxlen. inspired by
8848 * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and
8849 * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254}
8851 * @global object
8852 * @param int $maxlen The maximum size of the password being generated.
8853 * @return string
8855 function generate_password($maxlen=10) {
8856 global $CFG;
8858 if (empty($CFG->passwordpolicy)) {
8859 $fillers = PASSWORD_DIGITS;
8860 $wordlist = file($CFG->wordlist);
8861 $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8862 $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8863 $filler1 = $fillers[rand(0, strlen($fillers) - 1)];
8864 $password = $word1 . $filler1 . $word2;
8865 } else {
8866 $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0;
8867 $digits = $CFG->minpassworddigits;
8868 $lower = $CFG->minpasswordlower;
8869 $upper = $CFG->minpasswordupper;
8870 $nonalphanum = $CFG->minpasswordnonalphanum;
8871 $total = $lower + $upper + $digits + $nonalphanum;
8872 // minlength should be the greater one of the two ( $minlen and $total )
8873 $minlen = $minlen < $total ? $total : $minlen;
8874 // maxlen can never be smaller than minlen
8875 $maxlen = $minlen > $maxlen ? $minlen : $maxlen;
8876 $additional = $maxlen - $total;
8878 // Make sure we have enough characters to fulfill
8879 // complexity requirements
8880 $passworddigits = PASSWORD_DIGITS;
8881 while ($digits > strlen($passworddigits)) {
8882 $passworddigits .= PASSWORD_DIGITS;
8884 $passwordlower = PASSWORD_LOWER;
8885 while ($lower > strlen($passwordlower)) {
8886 $passwordlower .= PASSWORD_LOWER;
8888 $passwordupper = PASSWORD_UPPER;
8889 while ($upper > strlen($passwordupper)) {
8890 $passwordupper .= PASSWORD_UPPER;
8892 $passwordnonalphanum = PASSWORD_NONALPHANUM;
8893 while ($nonalphanum > strlen($passwordnonalphanum)) {
8894 $passwordnonalphanum .= PASSWORD_NONALPHANUM;
8897 // Now mix and shuffle it all
8898 $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) .
8899 substr(str_shuffle ($passwordupper), 0, $upper) .
8900 substr(str_shuffle ($passworddigits), 0, $digits) .
8901 substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) .
8902 substr(str_shuffle ($passwordlower .
8903 $passwordupper .
8904 $passworddigits .
8905 $passwordnonalphanum), 0 , $additional));
8908 return substr ($password, 0, $maxlen);
8912 * Given a float, prints it nicely.
8913 * Localized floats must not be used in calculations!
8915 * @param float $float The float to print
8916 * @param int $places The number of decimal places to print.
8917 * @param bool $localized use localized decimal separator
8918 * @return string locale float
8920 function format_float($float, $decimalpoints=1, $localized=true) {
8921 if (is_null($float)) {
8922 return '';
8924 if ($localized) {
8925 return number_format($float, $decimalpoints, get_string('decsep', 'langconfig'), '');
8926 } else {
8927 return number_format($float, $decimalpoints, '.', '');
8932 * Converts locale specific floating point/comma number back to standard PHP float value
8933 * Do NOT try to do any math operations before this conversion on any user submitted floats!
8935 * @param string $locale_float locale aware float representation
8936 * @return float
8938 function unformat_float($locale_float) {
8939 $locale_float = trim($locale_float);
8941 if ($locale_float == '') {
8942 return null;
8945 $locale_float = str_replace(' ', '', $locale_float); // no spaces - those might be used as thousand separators
8947 return (float)str_replace(get_string('decsep', 'langconfig'), '.', $locale_float);
8951 * Given a simple array, this shuffles it up just like shuffle()
8952 * Unlike PHP's shuffle() this function works on any machine.
8954 * @param array $array The array to be rearranged
8955 * @return array
8957 function swapshuffle($array) {
8959 srand ((double) microtime() * 10000000);
8960 $last = count($array) - 1;
8961 for ($i=0;$i<=$last;$i++) {
8962 $from = rand(0,$last);
8963 $curr = $array[$i];
8964 $array[$i] = $array[$from];
8965 $array[$from] = $curr;
8967 return $array;
8971 * Like {@link swapshuffle()}, but works on associative arrays
8973 * @param array $array The associative array to be rearranged
8974 * @return array
8976 function swapshuffle_assoc($array) {
8978 $newarray = array();
8979 $newkeys = swapshuffle(array_keys($array));
8981 foreach ($newkeys as $newkey) {
8982 $newarray[$newkey] = $array[$newkey];
8984 return $newarray;
8988 * Given an arbitrary array, and a number of draws,
8989 * this function returns an array with that amount
8990 * of items. The indexes are retained.
8992 * @todo Finish documenting this function
8994 * @param array $array
8995 * @param int $draws
8996 * @return array
8998 function draw_rand_array($array, $draws) {
8999 srand ((double) microtime() * 10000000);
9001 $return = array();
9003 $last = count($array);
9005 if ($draws > $last) {
9006 $draws = $last;
9009 while ($draws > 0) {
9010 $last--;
9012 $keys = array_keys($array);
9013 $rand = rand(0, $last);
9015 $return[$keys[$rand]] = $array[$keys[$rand]];
9016 unset($array[$keys[$rand]]);
9018 $draws--;
9021 return $return;
9025 * Calculate the difference between two microtimes
9027 * @param string $a The first Microtime
9028 * @param string $b The second Microtime
9029 * @return string
9031 function microtime_diff($a, $b) {
9032 list($a_dec, $a_sec) = explode(' ', $a);
9033 list($b_dec, $b_sec) = explode(' ', $b);
9034 return $b_sec - $a_sec + $b_dec - $a_dec;
9038 * Given a list (eg a,b,c,d,e) this function returns
9039 * an array of 1->a, 2->b, 3->c etc
9041 * @param string $list The string to explode into array bits
9042 * @param string $separator The separator used within the list string
9043 * @return array The now assembled array
9045 function make_menu_from_list($list, $separator=',') {
9047 $array = array_reverse(explode($separator, $list), true);
9048 foreach ($array as $key => $item) {
9049 $outarray[$key+1] = trim($item);
9051 return $outarray;
9055 * Creates an array that represents all the current grades that
9056 * can be chosen using the given grading type.
9058 * Negative numbers
9059 * are scales, zero is no grade, and positive numbers are maximum
9060 * grades.
9062 * @todo Finish documenting this function or better deprecated this completely!
9064 * @param int $gradingtype
9065 * @return array
9067 function make_grades_menu($gradingtype) {
9068 global $DB;
9070 $grades = array();
9071 if ($gradingtype < 0) {
9072 if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) {
9073 return make_menu_from_list($scale->scale);
9075 } else if ($gradingtype > 0) {
9076 for ($i=$gradingtype; $i>=0; $i--) {
9077 $grades[$i] = $i .' / '. $gradingtype;
9079 return $grades;
9081 return $grades;
9085 * This function returns the number of activities
9086 * using scaleid in a courseid
9088 * @todo Finish documenting this function
9090 * @global object
9091 * @global object
9092 * @param int $courseid ?
9093 * @param int $scaleid ?
9094 * @return int
9096 function course_scale_used($courseid, $scaleid) {
9097 global $CFG, $DB;
9099 $return = 0;
9101 if (!empty($scaleid)) {
9102 if ($cms = get_course_mods($courseid)) {
9103 foreach ($cms as $cm) {
9104 //Check cm->name/lib.php exists
9105 if (file_exists($CFG->dirroot.'/mod/'.$cm->modname.'/lib.php')) {
9106 include_once($CFG->dirroot.'/mod/'.$cm->modname.'/lib.php');
9107 $function_name = $cm->modname.'_scale_used';
9108 if (function_exists($function_name)) {
9109 if ($function_name($cm->instance,$scaleid)) {
9110 $return++;
9117 // check if any course grade item makes use of the scale
9118 $return += $DB->count_records('grade_items', array('courseid'=>$courseid, 'scaleid'=>$scaleid));
9120 // check if any outcome in the course makes use of the scale
9121 $return += $DB->count_records_sql("SELECT COUNT('x')
9122 FROM {grade_outcomes_courses} goc,
9123 {grade_outcomes} go
9124 WHERE go.id = goc.outcomeid
9125 AND go.scaleid = ? AND goc.courseid = ?",
9126 array($scaleid, $courseid));
9128 return $return;
9132 * This function returns the number of activities
9133 * using scaleid in the entire site
9135 * @param int $scaleid
9136 * @param array $courses
9137 * @return int
9139 function site_scale_used($scaleid, &$courses) {
9140 $return = 0;
9142 if (!is_array($courses) || count($courses) == 0) {
9143 $courses = get_courses("all",false,"c.id,c.shortname");
9146 if (!empty($scaleid)) {
9147 if (is_array($courses) && count($courses) > 0) {
9148 foreach ($courses as $course) {
9149 $return += course_scale_used($course->id,$scaleid);
9153 return $return;
9157 * make_unique_id_code
9159 * @todo Finish documenting this function
9161 * @uses $_SERVER
9162 * @param string $extra Extra string to append to the end of the code
9163 * @return string
9165 function make_unique_id_code($extra='') {
9167 $hostname = 'unknownhost';
9168 if (!empty($_SERVER['HTTP_HOST'])) {
9169 $hostname = $_SERVER['HTTP_HOST'];
9170 } else if (!empty($_ENV['HTTP_HOST'])) {
9171 $hostname = $_ENV['HTTP_HOST'];
9172 } else if (!empty($_SERVER['SERVER_NAME'])) {
9173 $hostname = $_SERVER['SERVER_NAME'];
9174 } else if (!empty($_ENV['SERVER_NAME'])) {
9175 $hostname = $_ENV['SERVER_NAME'];
9178 $date = gmdate("ymdHis");
9180 $random = random_string(6);
9182 if ($extra) {
9183 return $hostname .'+'. $date .'+'. $random .'+'. $extra;
9184 } else {
9185 return $hostname .'+'. $date .'+'. $random;
9191 * Function to check the passed address is within the passed subnet
9193 * The parameter is a comma separated string of subnet definitions.
9194 * Subnet strings can be in one of three formats:
9195 * 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn (number of bits in net mask)
9196 * 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)
9197 * 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. (incomplete address, a bit non-technical ;-)
9198 * Code for type 1 modified from user posted comments by mediator at
9199 * {@link http://au.php.net/manual/en/function.ip2long.php}
9201 * @param string $addr The address you are checking
9202 * @param string $subnetstr The string of subnet addresses
9203 * @return bool
9205 function address_in_subnet($addr, $subnetstr) {
9207 if ($addr == '0.0.0.0') {
9208 return false;
9210 $subnets = explode(',', $subnetstr);
9211 $found = false;
9212 $addr = trim($addr);
9213 $addr = cleanremoteaddr($addr, false); // normalise
9214 if ($addr === null) {
9215 return false;
9217 $addrparts = explode(':', $addr);
9219 $ipv6 = strpos($addr, ':');
9221 foreach ($subnets as $subnet) {
9222 $subnet = trim($subnet);
9223 if ($subnet === '') {
9224 continue;
9227 if (strpos($subnet, '/') !== false) {
9228 ///1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn
9229 list($ip, $mask) = explode('/', $subnet);
9230 $mask = trim($mask);
9231 if (!is_number($mask)) {
9232 continue; // incorect mask number, eh?
9234 $ip = cleanremoteaddr($ip, false); // normalise
9235 if ($ip === null) {
9236 continue;
9238 if (strpos($ip, ':') !== false) {
9239 // IPv6
9240 if (!$ipv6) {
9241 continue;
9243 if ($mask > 128 or $mask < 0) {
9244 continue; // nonsense
9246 if ($mask == 0) {
9247 return true; // any address
9249 if ($mask == 128) {
9250 if ($ip === $addr) {
9251 return true;
9253 continue;
9255 $ipparts = explode(':', $ip);
9256 $modulo = $mask % 16;
9257 $ipnet = array_slice($ipparts, 0, ($mask-$modulo)/16);
9258 $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16);
9259 if (implode(':', $ipnet) === implode(':', $addrnet)) {
9260 if ($modulo == 0) {
9261 return true;
9263 $pos = ($mask-$modulo)/16;
9264 $ipnet = hexdec($ipparts[$pos]);
9265 $addrnet = hexdec($addrparts[$pos]);
9266 $mask = 0xffff << (16 - $modulo);
9267 if (($addrnet & $mask) == ($ipnet & $mask)) {
9268 return true;
9272 } else {
9273 // IPv4
9274 if ($ipv6) {
9275 continue;
9277 if ($mask > 32 or $mask < 0) {
9278 continue; // nonsense
9280 if ($mask == 0) {
9281 return true;
9283 if ($mask == 32) {
9284 if ($ip === $addr) {
9285 return true;
9287 continue;
9289 $mask = 0xffffffff << (32 - $mask);
9290 if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
9291 return true;
9295 } else if (strpos($subnet, '-') !== false) {
9296 /// 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.
9297 $parts = explode('-', $subnet);
9298 if (count($parts) != 2) {
9299 continue;
9302 if (strpos($subnet, ':') !== false) {
9303 // IPv6
9304 if (!$ipv6) {
9305 continue;
9307 $ipstart = cleanremoteaddr(trim($parts[0]), false); // normalise
9308 if ($ipstart === null) {
9309 continue;
9311 $ipparts = explode(':', $ipstart);
9312 $start = hexdec(array_pop($ipparts));
9313 $ipparts[] = trim($parts[1]);
9314 $ipend = cleanremoteaddr(implode(':', $ipparts), false); // normalise
9315 if ($ipend === null) {
9316 continue;
9318 $ipparts[7] = '';
9319 $ipnet = implode(':', $ipparts);
9320 if (strpos($addr, $ipnet) !== 0) {
9321 continue;
9323 $ipparts = explode(':', $ipend);
9324 $end = hexdec($ipparts[7]);
9326 $addrend = hexdec($addrparts[7]);
9328 if (($addrend >= $start) and ($addrend <= $end)) {
9329 return true;
9332 } else {
9333 // IPv4
9334 if ($ipv6) {
9335 continue;
9337 $ipstart = cleanremoteaddr(trim($parts[0]), false); // normalise
9338 if ($ipstart === null) {
9339 continue;
9341 $ipparts = explode('.', $ipstart);
9342 $ipparts[3] = trim($parts[1]);
9343 $ipend = cleanremoteaddr(implode('.', $ipparts), false); // normalise
9344 if ($ipend === null) {
9345 continue;
9348 if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
9349 return true;
9353 } else {
9354 /// 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
9355 if (strpos($subnet, ':') !== false) {
9356 // IPv6
9357 if (!$ipv6) {
9358 continue;
9360 $parts = explode(':', $subnet);
9361 $count = count($parts);
9362 if ($parts[$count-1] === '') {
9363 unset($parts[$count-1]); // trim trailing :
9364 $count--;
9365 $subnet = implode('.', $parts);
9367 $isip = cleanremoteaddr($subnet, false); // normalise
9368 if ($isip !== null) {
9369 if ($isip === $addr) {
9370 return true;
9372 continue;
9373 } else if ($count > 8) {
9374 continue;
9376 $zeros = array_fill(0, 8-$count, '0');
9377 $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16);
9378 if (address_in_subnet($addr, $subnet)) {
9379 return true;
9382 } else {
9383 // IPv4
9384 if ($ipv6) {
9385 continue;
9387 $parts = explode('.', $subnet);
9388 $count = count($parts);
9389 if ($parts[$count-1] === '') {
9390 unset($parts[$count-1]); // trim trailing .
9391 $count--;
9392 $subnet = implode('.', $parts);
9394 if ($count == 4) {
9395 $subnet = cleanremoteaddr($subnet, false); // normalise
9396 if ($subnet === $addr) {
9397 return true;
9399 continue;
9400 } else if ($count > 4) {
9401 continue;
9403 $zeros = array_fill(0, 4-$count, '0');
9404 $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8);
9405 if (address_in_subnet($addr, $subnet)) {
9406 return true;
9412 return false;
9416 * For outputting debugging info
9418 * @uses STDOUT
9419 * @param string $string The string to write
9420 * @param string $eol The end of line char(s) to use
9421 * @param string $sleep Period to make the application sleep
9422 * This ensures any messages have time to display before redirect
9424 function mtrace($string, $eol="\n", $sleep=0) {
9426 if (defined('STDOUT')) {
9427 fwrite(STDOUT, $string.$eol);
9428 } else {
9429 echo $string . $eol;
9432 flush();
9434 //delay to keep message on user's screen in case of subsequent redirect
9435 if ($sleep) {
9436 sleep($sleep);
9441 * Replace 1 or more slashes or backslashes to 1 slash
9443 * @param string $path The path to strip
9444 * @return string the path with double slashes removed
9446 function cleardoubleslashes ($path) {
9447 return preg_replace('/(\/|\\\){1,}/','/',$path);
9451 * Is current ip in give list?
9453 * @param string $list
9454 * @return bool
9456 function remoteip_in_list($list){
9457 $inlist = false;
9458 $client_ip = getremoteaddr(null);
9460 if(!$client_ip){
9461 // ensure access on cli
9462 return true;
9465 $list = explode("\n", $list);
9466 foreach($list as $subnet) {
9467 $subnet = trim($subnet);
9468 if (address_in_subnet($client_ip, $subnet)) {
9469 $inlist = true;
9470 break;
9473 return $inlist;
9477 * Returns most reliable client address
9479 * @global object
9480 * @param string $default If an address can't be determined, then return this
9481 * @return string The remote IP address
9483 function getremoteaddr($default='0.0.0.0') {
9484 global $CFG;
9486 if (empty($CFG->getremoteaddrconf)) {
9487 // This will happen, for example, before just after the upgrade, as the
9488 // user is redirected to the admin screen.
9489 $variablestoskip = 0;
9490 } else {
9491 $variablestoskip = $CFG->getremoteaddrconf;
9493 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) {
9494 if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
9495 $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']);
9496 return $address ? $address : $default;
9499 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
9500 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
9501 $address = cleanremoteaddr($_SERVER['HTTP_X_FORWARDED_FOR']);
9502 return $address ? $address : $default;
9505 if (!empty($_SERVER['REMOTE_ADDR'])) {
9506 $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']);
9507 return $address ? $address : $default;
9508 } else {
9509 return $default;
9514 * Cleans an ip address. Internal addresses are now allowed.
9515 * (Originally local addresses were not allowed.)
9517 * @param string $addr IPv4 or IPv6 address
9518 * @param bool $compress use IPv6 address compression
9519 * @return string normalised ip address string, null if error
9521 function cleanremoteaddr($addr, $compress=false) {
9522 $addr = trim($addr);
9524 //TODO: maybe add a separate function is_addr_public() or something like this
9526 if (strpos($addr, ':') !== false) {
9527 // can be only IPv6
9528 $parts = explode(':', $addr);
9529 $count = count($parts);
9531 if (strpos($parts[$count-1], '.') !== false) {
9532 //legacy ipv4 notation
9533 $last = array_pop($parts);
9534 $ipv4 = cleanremoteaddr($last, true);
9535 if ($ipv4 === null) {
9536 return null;
9538 $bits = explode('.', $ipv4);
9539 $parts[] = dechex($bits[0]).dechex($bits[1]);
9540 $parts[] = dechex($bits[2]).dechex($bits[3]);
9541 $count = count($parts);
9542 $addr = implode(':', $parts);
9545 if ($count < 3 or $count > 8) {
9546 return null; // severly malformed
9549 if ($count != 8) {
9550 if (strpos($addr, '::') === false) {
9551 return null; // malformed
9553 // uncompress ::
9554 $insertat = array_search('', $parts, true);
9555 $missing = array_fill(0, 1 + 8 - $count, '0');
9556 array_splice($parts, $insertat, 1, $missing);
9557 foreach ($parts as $key=>$part) {
9558 if ($part === '') {
9559 $parts[$key] = '0';
9564 $adr = implode(':', $parts);
9565 if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) {
9566 return null; // incorrect format - sorry
9569 // normalise 0s and case
9570 $parts = array_map('hexdec', $parts);
9571 $parts = array_map('dechex', $parts);
9573 $result = implode(':', $parts);
9575 if (!$compress) {
9576 return $result;
9579 if ($result === '0:0:0:0:0:0:0:0') {
9580 return '::'; // all addresses
9583 $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1);
9584 if ($compressed !== $result) {
9585 return $compressed;
9588 $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
9589 if ($compressed !== $result) {
9590 return $compressed;
9593 $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
9594 if ($compressed !== $result) {
9595 return $compressed;
9598 return $result;
9601 // first get all things that look like IPv4 addresses
9602 $parts = array();
9603 if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
9604 return null;
9606 unset($parts[0]);
9608 foreach ($parts as $key=>$match) {
9609 if ($match > 255) {
9610 return null;
9612 $parts[$key] = (int)$match; // normalise 0s
9615 return implode('.', $parts);
9619 * This function will make a complete copy of anything it's given,
9620 * regardless of whether it's an object or not.
9622 * @param mixed $thing Something you want cloned
9623 * @return mixed What ever it is you passed it
9625 function fullclone($thing) {
9626 return unserialize(serialize($thing));
9631 * This function expects to called during shutdown
9632 * should be set via register_shutdown_function()
9633 * in lib/setup.php .
9635 * @return void
9637 function moodle_request_shutdown() {
9638 global $CFG;
9640 // help apache server if possible
9641 $apachereleasemem = false;
9642 if (function_exists('apache_child_terminate') && function_exists('memory_get_usage')
9643 && ini_get_bool('child_terminate')) {
9645 $limit = (empty($CFG->apachemaxmem) ? 64*1024*1024 : $CFG->apachemaxmem); //64MB default
9646 if (memory_get_usage() > get_real_size($limit)) {
9647 $apachereleasemem = $limit;
9648 @apache_child_terminate();
9652 // deal with perf logging
9653 if (defined('MDL_PERF') || (!empty($CFG->perfdebug) and $CFG->perfdebug > 7)) {
9654 if ($apachereleasemem) {
9655 error_log('Mem usage over '.$apachereleasemem.': marking Apache child for reaping.');
9657 if (defined('MDL_PERFTOLOG')) {
9658 $perf = get_performance_info();
9659 error_log("PERF: " . $perf['txt']);
9661 if (defined('MDL_PERFINC')) {
9662 $inc = get_included_files();
9663 $ts = 0;
9664 foreach($inc as $f) {
9665 if (preg_match(':^/:', $f)) {
9666 $fs = filesize($f);
9667 $ts += $fs;
9668 $hfs = display_size($fs);
9669 error_log(substr($f,strlen($CFG->dirroot)) . " size: $fs ($hfs)"
9670 , NULL, NULL, 0);
9671 } else {
9672 error_log($f , NULL, NULL, 0);
9675 if ($ts > 0 ) {
9676 $hts = display_size($ts);
9677 error_log("Total size of files included: $ts ($hts)");
9684 * If new messages are waiting for the current user, then insert
9685 * JavaScript to pop up the messaging window into the page
9687 * @global moodle_page $PAGE
9688 * @return void
9690 function message_popup_window() {
9691 global $USER, $DB, $PAGE, $CFG, $SITE;
9693 if (!$PAGE->get_popup_notification_allowed() || empty($CFG->messaging)) {
9694 return;
9697 if (!isloggedin() || isguestuser()) {
9698 return;
9701 if (!isset($USER->message_lastpopup)) {
9702 $USER->message_lastpopup = 0;
9703 } else if ($USER->message_lastpopup > (time()-120)) {
9704 //dont run the query to check whether to display a popup if its been run in the last 2 minutes
9705 return;
9708 //a quick query to check whether the user has new messages
9709 $messagecount = $DB->count_records('message', array('useridto' => $USER->id));
9710 if ($messagecount<1) {
9711 return;
9714 //got unread messages so now do another query that joins with the user table
9715 $messagesql = "SELECT m.id, m.smallmessage, m.fullmessageformat, m.notification, u.firstname, u.lastname
9716 FROM {message} m
9717 JOIN {message_working} mw ON m.id=mw.unreadmessageid
9718 JOIN {message_processors} p ON mw.processorid=p.id
9719 JOIN {user} u ON m.useridfrom=u.id
9720 WHERE m.useridto = :userid
9721 AND p.name='popup'";
9723 //if the user was last notified over an hour ago we can renotify them of old messages
9724 //so don't worry about when the new message was sent
9725 $lastnotifiedlongago = $USER->message_lastpopup < (time()-3600);
9726 if (!$lastnotifiedlongago) {
9727 $messagesql .= 'AND m.timecreated > :lastpopuptime';
9730 $message_users = $DB->get_records_sql($messagesql, array('userid'=>$USER->id, 'lastpopuptime'=>$USER->message_lastpopup));
9732 //if we have new messages to notify the user about
9733 if (!empty($message_users)) {
9735 $strmessages = '';
9736 if (count($message_users)>1) {
9737 $strmessages = get_string('unreadnewmessages', 'message', count($message_users));
9738 } else {
9739 $message_users = reset($message_users);
9741 //show who the message is from if its not a notification
9742 if (!$message_users->notification) {
9743 $strmessages = get_string('unreadnewmessage', 'message', fullname($message_users) );
9746 //try to display the small version of the message
9747 $smallmessage = null;
9748 if (!empty($message_users->smallmessage)) {
9749 //display the first 200 chars of the message in the popup
9750 $textlib = textlib_get_instance();
9751 $smallmessage = null;
9752 if ($textlib->strlen($message_users->smallmessage) > 200) {
9753 $smallmessage = $textlib->substr($message_users->smallmessage,0,200).'...';
9754 } else {
9755 $smallmessage = $message_users->smallmessage;
9758 //prevent html symbols being displayed
9759 if ($message_users->fullmessageformat == FORMAT_HTML) {
9760 $smallmessage = html_to_text($smallmessage);
9761 } else {
9762 $smallmessage = s($smallmessage);
9764 } else if ($message_users->notification) {
9765 //its a notification with no smallmessage so just say they have a notification
9766 $smallmessage = get_string('unreadnewnotification', 'message');
9768 if (!empty($smallmessage)) {
9769 $strmessages .= '<div id="usermessage">'.s($smallmessage).'</div>';
9773 $strgomessage = get_string('gotomessages', 'message');
9774 $strstaymessage = get_string('ignore','admin');
9776 $url = $CFG->wwwroot.'/message/index.php';
9777 $content = html_writer::start_tag('div', array('id'=>'newmessageoverlay','class'=>'mdl-align')).
9778 html_writer::start_tag('div', array('id'=>'newmessagetext')).
9779 $strmessages.
9780 html_writer::end_tag('div').
9782 html_writer::start_tag('div', array('id'=>'newmessagelinks')).
9783 html_writer::link($url, $strgomessage, array('id'=>'notificationyes')).'&nbsp;&nbsp;&nbsp;'.
9784 html_writer::link('', $strstaymessage, array('id'=>'notificationno')).
9785 html_writer::end_tag('div');
9786 html_writer::end_tag('div');
9788 $PAGE->requires->js_init_call('M.core_message.init_notification', array('', $content, $url));
9790 $USER->message_lastpopup = time();
9795 * Used to make sure that $min <= $value <= $max
9797 * Make sure that value is between min, and max
9799 * @param int $min The minimum value
9800 * @param int $value The value to check
9801 * @param int $max The maximum value
9803 function bounded_number($min, $value, $max) {
9804 if($value < $min) {
9805 return $min;
9807 if($value > $max) {
9808 return $max;
9810 return $value;
9814 * Check if there is a nested array within the passed array
9816 * @param array $array
9817 * @return bool true if there is a nested array false otherwise
9819 function array_is_nested($array) {
9820 foreach ($array as $value) {
9821 if (is_array($value)) {
9822 return true;
9825 return false;
9829 * get_performance_info() pairs up with init_performance_info()
9830 * loaded in setup.php. Returns an array with 'html' and 'txt'
9831 * values ready for use, and each of the individual stats provided
9832 * separately as well.
9834 * @global object
9835 * @global object
9836 * @global object
9837 * @return array
9839 function get_performance_info() {
9840 global $CFG, $PERF, $DB, $PAGE;
9842 $info = array();
9843 $info['html'] = ''; // holds userfriendly HTML representation
9844 $info['txt'] = me() . ' '; // holds log-friendly representation
9846 $info['realtime'] = microtime_diff($PERF->starttime, microtime());
9848 $info['html'] .= '<span class="timeused">'.$info['realtime'].' secs</span> ';
9849 $info['txt'] .= 'time: '.$info['realtime'].'s ';
9851 if (function_exists('memory_get_usage')) {
9852 $info['memory_total'] = memory_get_usage();
9853 $info['memory_growth'] = memory_get_usage() - $PERF->startmemory;
9854 $info['html'] .= '<span class="memoryused">RAM: '.display_size($info['memory_total']).'</span> ';
9855 $info['txt'] .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '.$info['memory_growth'].'B ('.display_size($info['memory_growth']).') ';
9858 if (function_exists('memory_get_peak_usage')) {
9859 $info['memory_peak'] = memory_get_peak_usage();
9860 $info['html'] .= '<span class="memoryused">RAM peak: '.display_size($info['memory_peak']).'</span> ';
9861 $info['txt'] .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') ';
9864 $inc = get_included_files();
9865 //error_log(print_r($inc,1));
9866 $info['includecount'] = count($inc);
9867 $info['html'] .= '<span class="included">Included '.$info['includecount'].' files</span> ';
9868 $info['txt'] .= 'includecount: '.$info['includecount'].' ';
9870 $filtermanager = filter_manager::instance();
9871 if (method_exists($filtermanager, 'get_performance_summary')) {
9872 list($filterinfo, $nicenames) = $filtermanager->get_performance_summary();
9873 $info = array_merge($filterinfo, $info);
9874 foreach ($filterinfo as $key => $value) {
9875 $info['html'] .= "<span class='$key'>$nicenames[$key]: $value </span> ";
9876 $info['txt'] .= "$key: $value ";
9880 $stringmanager = get_string_manager();
9881 if (method_exists($stringmanager, 'get_performance_summary')) {
9882 list($filterinfo, $nicenames) = $stringmanager->get_performance_summary();
9883 $info = array_merge($filterinfo, $info);
9884 foreach ($filterinfo as $key => $value) {
9885 $info['html'] .= "<span class='$key'>$nicenames[$key]: $value </span> ";
9886 $info['txt'] .= "$key: $value ";
9890 $jsmodules = $PAGE->requires->get_loaded_modules();
9891 if ($jsmodules) {
9892 $yuicount = 0;
9893 $othercount = 0;
9894 $details = '';
9895 foreach ($jsmodules as $module => $backtraces) {
9896 if (strpos($module, 'yui') === 0) {
9897 $yuicount += 1;
9898 } else {
9899 $othercount += 1;
9901 $details .= "<div class='yui-module'><p>$module</p>";
9902 foreach ($backtraces as $backtrace) {
9903 $details .= "<div class='backtrace'>$backtrace</div>";
9905 $details .= '</div>';
9907 $info['html'] .= "<span class='includedyuimodules'>Included YUI modules: $yuicount</span> ";
9908 $info['txt'] .= "includedyuimodules: $yuicount ";
9909 $info['html'] .= "<span class='includedjsmodules'>Other JavaScript modules: $othercount</span> ";
9910 $info['txt'] .= "includedjsmodules: $othercount ";
9911 // Slightly odd to output the details in a display: none div. The point
9912 // Is that it takes a lot of space, and if you care you can reveal it
9913 // using firebug.
9914 $info['html'] .= '<div id="yui-module-debug" class="notifytiny">'.$details.'</div>';
9917 if (!empty($PERF->logwrites)) {
9918 $info['logwrites'] = $PERF->logwrites;
9919 $info['html'] .= '<span class="logwrites">Log DB writes '.$info['logwrites'].'</span> ';
9920 $info['txt'] .= 'logwrites: '.$info['logwrites'].' ';
9923 $info['dbqueries'] = $DB->perf_get_reads().'/'.($DB->perf_get_writes() - $PERF->logwrites);
9924 $info['html'] .= '<span class="dbqueries">DB reads/writes: '.$info['dbqueries'].'</span> ';
9925 $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' ';
9927 if (function_exists('posix_times')) {
9928 $ptimes = posix_times();
9929 if (is_array($ptimes)) {
9930 foreach ($ptimes as $key => $val) {
9931 $info[$key] = $ptimes[$key] - $PERF->startposixtimes[$key];
9933 $info['html'] .= "<span class=\"posixtimes\">ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</span> ";
9934 $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] ";
9938 // Grab the load average for the last minute
9939 // /proc will only work under some linux configurations
9940 // while uptime is there under MacOSX/Darwin and other unices
9941 if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) {
9942 list($server_load) = explode(' ', $loadavg[0]);
9943 unset($loadavg);
9944 } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) {
9945 if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) {
9946 $server_load = $matches[1];
9947 } else {
9948 trigger_error('Could not parse uptime output!');
9951 if (!empty($server_load)) {
9952 $info['serverload'] = $server_load;
9953 $info['html'] .= '<span class="serverload">Load average: '.$info['serverload'].'</span> ';
9954 $info['txt'] .= "serverload: {$info['serverload']} ";
9957 // Display size of session if session started
9958 if (session_id()) {
9959 $info['sessionsize'] = display_size(strlen(session_encode()));
9960 $info['html'] .= '<span class="sessionsize">Session: ' . $info['sessionsize'] . '</span> ';
9961 $info['txt'] .= "Session: {$info['sessionsize']} ";
9964 /* if (isset($rcache->hits) && isset($rcache->misses)) {
9965 $info['rcachehits'] = $rcache->hits;
9966 $info['rcachemisses'] = $rcache->misses;
9967 $info['html'] .= '<span class="rcache">Record cache hit/miss ratio : '.
9968 "{$rcache->hits}/{$rcache->misses}</span> ";
9969 $info['txt'] .= 'rcache: '.
9970 "{$rcache->hits}/{$rcache->misses} ";
9972 $info['html'] = '<div class="performanceinfo siteinfo">'.$info['html'].'</div>';
9973 return $info;
9977 * @todo Document this function linux people
9979 function apd_get_profiling() {
9980 return shell_exec('pprofp -u ' . ini_get('apd.dumpdir') . '/pprof.' . getmypid() . '.*');
9984 * Delete directory or only it's content
9986 * @param string $dir directory path
9987 * @param bool $content_only
9988 * @return bool success, true also if dir does not exist
9990 function remove_dir($dir, $content_only=false) {
9991 if (!file_exists($dir)) {
9992 // nothing to do
9993 return true;
9995 $handle = opendir($dir);
9996 $result = true;
9997 while (false!==($item = readdir($handle))) {
9998 if($item != '.' && $item != '..') {
9999 if(is_dir($dir.'/'.$item)) {
10000 $result = remove_dir($dir.'/'.$item) && $result;
10001 }else{
10002 $result = unlink($dir.'/'.$item) && $result;
10006 closedir($handle);
10007 if ($content_only) {
10008 clearstatcache(); // make sure file stat cache is properly invalidated
10009 return $result;
10011 $result = rmdir($dir); // if anything left the result will be false, no need for && $result
10012 clearstatcache(); // make sure file stat cache is properly invalidated
10013 return $result;
10017 * Detect if an object or a class contains a given property
10018 * will take an actual object or the name of a class
10020 * @param mix $obj Name of class or real object to test
10021 * @param string $property name of property to find
10022 * @return bool true if property exists
10024 function object_property_exists( $obj, $property ) {
10025 if (is_string( $obj )) {
10026 $properties = get_class_vars( $obj );
10028 else {
10029 $properties = get_object_vars( $obj );
10031 return array_key_exists( $property, $properties );
10036 * Detect a custom script replacement in the data directory that will
10037 * replace an existing moodle script
10039 * @return string|bool full path name if a custom script exists, false if no custom script exists
10041 function custom_script_path() {
10042 global $CFG, $SCRIPT;
10044 if ($SCRIPT === null) {
10045 // Probably some weird external script
10046 return false;
10049 $scriptpath = $CFG->customscripts . $SCRIPT;
10051 // check the custom script exists
10052 if (file_exists($scriptpath) and is_file($scriptpath)) {
10053 return $scriptpath;
10054 } else {
10055 return false;
10060 * Returns whether or not the user object is a remote MNET user. This function
10061 * is in moodlelib because it does not rely on loading any of the MNET code.
10063 * @global object
10064 * @param object $user A valid user object
10065 * @return bool True if the user is from a remote Moodle.
10067 function is_mnet_remote_user($user) {
10068 global $CFG;
10070 if (!isset($CFG->mnet_localhost_id)) {
10071 include_once $CFG->dirroot . '/mnet/lib.php';
10072 $env = new mnet_environment();
10073 $env->init();
10074 unset($env);
10077 return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id);
10081 * This function will search for browser prefereed languages, setting Moodle
10082 * to use the best one available if $SESSION->lang is undefined
10084 * @global object
10085 * @global object
10086 * @global object
10088 function setup_lang_from_browser() {
10090 global $CFG, $SESSION, $USER;
10092 if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) {
10093 // Lang is defined in session or user profile, nothing to do
10094 return;
10097 if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do
10098 return;
10101 /// Extract and clean langs from headers
10102 $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
10103 $rawlangs = str_replace('-', '_', $rawlangs); // we are using underscores
10104 $rawlangs = explode(',', $rawlangs); // Convert to array
10105 $langs = array();
10107 $order = 1.0;
10108 foreach ($rawlangs as $lang) {
10109 if (strpos($lang, ';') === false) {
10110 $langs[(string)$order] = $lang;
10111 $order = $order-0.01;
10112 } else {
10113 $parts = explode(';', $lang);
10114 $pos = strpos($parts[1], '=');
10115 $langs[substr($parts[1], $pos+1)] = $parts[0];
10118 krsort($langs, SORT_NUMERIC);
10120 /// Look for such langs under standard locations
10121 foreach ($langs as $lang) {
10122 $lang = strtolower(clean_param($lang, PARAM_SAFEDIR)); // clean it properly for include
10123 if (get_string_manager()->translation_exists($lang, false)) {
10124 $SESSION->lang = $lang; /// Lang exists, set it in session
10125 break; /// We have finished. Go out
10128 return;
10132 * check if $url matches anything in proxybypass list
10134 * any errors just result in the proxy being used (least bad)
10136 * @global object
10137 * @param string $url url to check
10138 * @return boolean true if we should bypass the proxy
10140 function is_proxybypass( $url ) {
10141 global $CFG;
10143 // sanity check
10144 if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) {
10145 return false;
10148 // get the host part out of the url
10149 if (!$host = parse_url( $url, PHP_URL_HOST )) {
10150 return false;
10153 // get the possible bypass hosts into an array
10154 $matches = explode( ',', $CFG->proxybypass );
10156 // check for a match
10157 // (IPs need to match the left hand side and hosts the right of the url,
10158 // but we can recklessly check both as there can't be a false +ve)
10159 $bypass = false;
10160 foreach ($matches as $match) {
10161 $match = trim($match);
10163 // try for IP match (Left side)
10164 $lhs = substr($host,0,strlen($match));
10165 if (strcasecmp($match,$lhs)==0) {
10166 return true;
10169 // try for host match (Right side)
10170 $rhs = substr($host,-strlen($match));
10171 if (strcasecmp($match,$rhs)==0) {
10172 return true;
10176 // nothing matched.
10177 return false;
10181 ////////////////////////////////////////////////////////////////////////////////
10184 * Check if the passed navigation is of the new style
10186 * @param mixed $navigation
10187 * @return bool true for yes false for no
10189 function is_newnav($navigation) {
10190 if (is_array($navigation) && !empty($navigation['newnav'])) {
10191 return true;
10192 } else {
10193 return false;
10198 * Checks whether the given variable name is defined as a variable within the given object.
10200 * This will NOT work with stdClass objects, which have no class variables.
10202 * @param string $var The variable name
10203 * @param object $object The object to check
10204 * @return boolean
10206 function in_object_vars($var, $object) {
10207 $class_vars = get_class_vars(get_class($object));
10208 $class_vars = array_keys($class_vars);
10209 return in_array($var, $class_vars);
10213 * Returns an array without repeated objects.
10214 * This function is similar to array_unique, but for arrays that have objects as values
10216 * @param array $array
10217 * @param bool $keep_key_assoc
10218 * @return array
10220 function object_array_unique($array, $keep_key_assoc = true) {
10221 $duplicate_keys = array();
10222 $tmp = array();
10224 foreach ($array as $key=>$val) {
10225 // convert objects to arrays, in_array() does not support objects
10226 if (is_object($val)) {
10227 $val = (array)$val;
10230 if (!in_array($val, $tmp)) {
10231 $tmp[] = $val;
10232 } else {
10233 $duplicate_keys[] = $key;
10237 foreach ($duplicate_keys as $key) {
10238 unset($array[$key]);
10241 return $keep_key_assoc ? $array : array_values($array);
10245 * Is a userid the primary administrator?
10247 * @param int $userid int id of user to check
10248 * @return boolean
10250 function is_primary_admin($userid){
10251 $primaryadmin = get_admin();
10253 if($userid == $primaryadmin->id){
10254 return true;
10255 }else{
10256 return false;
10261 * Returns the site identifier
10263 * @global object
10264 * @return string $CFG->siteidentifier, first making sure it is properly initialised.
10266 function get_site_identifier() {
10267 global $CFG;
10268 // Check to see if it is missing. If so, initialise it.
10269 if (empty($CFG->siteidentifier)) {
10270 set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']);
10272 // Return it.
10273 return $CFG->siteidentifier;
10277 * Check whether the given password has no more than the specified
10278 * number of consecutive identical characters.
10280 * @param string $password password to be checked against the password policy
10281 * @param integer $maxchars maximum number of consecutive identical characters
10283 function check_consecutive_identical_characters($password, $maxchars) {
10285 if ($maxchars < 1) {
10286 return true; // 0 is to disable this check
10288 if (strlen($password) <= $maxchars) {
10289 return true; // too short to fail this test
10292 $previouschar = '';
10293 $consecutivecount = 1;
10294 foreach (str_split($password) as $char) {
10295 if ($char != $previouschar) {
10296 $consecutivecount = 1;
10298 else {
10299 $consecutivecount++;
10300 if ($consecutivecount > $maxchars) {
10301 return false; // check failed already
10305 $previouschar = $char;
10308 return true;
10312 * helper function to do partial function binding
10313 * so we can use it for preg_replace_callback, for example
10314 * this works with php functions, user functions, static methods and class methods
10315 * it returns you a callback that you can pass on like so:
10317 * $callback = partial('somefunction', $arg1, $arg2);
10318 * or
10319 * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
10320 * or even
10321 * $obj = new someclass();
10322 * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2);
10324 * and then the arguments that are passed through at calltime are appended to the argument list.
10326 * @param mixed $function a php callback
10327 * $param mixed $arg1.. $argv arguments to partially bind with
10329 * @return callback
10331 function partial() {
10332 if (!class_exists('partial')) {
10333 class partial{
10334 var $values = array();
10335 var $func;
10337 function __construct($func, $args) {
10338 $this->values = $args;
10339 $this->func = $func;
10342 function method() {
10343 $args = func_get_args();
10344 return call_user_func_array($this->func, array_merge($this->values, $args));
10348 $args = func_get_args();
10349 $func = array_shift($args);
10350 $p = new partial($func, $args);
10351 return array($p, 'method');
10355 * helper function to load up and initialise the mnet environment
10356 * this must be called before you use mnet functions.
10358 * @return mnet_environment the equivalent of old $MNET global
10360 function get_mnet_environment() {
10361 global $CFG;
10362 require_once($CFG->dirroot . '/mnet/lib.php');
10363 static $instance = null;
10364 if (empty($instance)) {
10365 $instance = new mnet_environment();
10366 $instance->init();
10368 return $instance;
10372 * during xmlrpc server code execution, any code wishing to access
10373 * information about the remote peer must use this to get it.
10375 * @return mnet_remote_client the equivalent of old $MNET_REMOTE_CLIENT global
10377 function get_mnet_remote_client() {
10378 if (!defined('MNET_SERVER')) {
10379 debugging(get_string('notinxmlrpcserver', 'mnet'));
10380 return false;
10382 global $MNET_REMOTE_CLIENT;
10383 if (isset($MNET_REMOTE_CLIENT)) {
10384 return $MNET_REMOTE_CLIENT;
10386 return false;
10390 * during the xmlrpc server code execution, this will be called
10391 * to setup the object returned by {@see get_mnet_remote_client}
10393 * @param mnet_remote_client $client the client to set up
10395 function set_mnet_remote_client($client) {
10396 if (!defined('MNET_SERVER')) {
10397 throw new moodle_exception('notinxmlrpcserver', 'mnet');
10399 global $MNET_REMOTE_CLIENT;
10400 $MNET_REMOTE_CLIENT = $client;
10404 * return the jump url for a given remote user
10405 * this is used for rewriting forum post links in emails, etc
10407 * @param stdclass $user the user to get the idp url for
10409 function mnet_get_idp_jump_url($user) {
10410 global $CFG;
10412 static $mnetjumps = array();
10413 if (!array_key_exists($user->mnethostid, $mnetjumps)) {
10414 $idp = mnet_get_peer_host($user->mnethostid);
10415 $idpjumppath = mnet_get_app_jumppath($idp->applicationid);
10416 $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl=';
10418 return $mnetjumps[$user->mnethostid];
10422 * Gets the homepage to use for the current user
10424 * @return int One of HOMEPAGE_*
10426 function get_home_page() {
10427 global $CFG;
10429 if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) {
10430 if ($CFG->defaulthomepage == HOMEPAGE_MY) {
10431 return HOMEPAGE_MY;
10432 } else {
10433 return (int)get_user_preferences('user_home_page_preference', HOMEPAGE_MY);
10436 return HOMEPAGE_SITE;