MDL-32985 cron: prevent notices on "new" sites.
[moodle.git] / lib / moodlelib.php
blob0d46fe4ef1c3912cb6ffebd66c8fc52dd28073bf
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
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');
287 /// Web Services ///
290 * VALUE_REQUIRED - if the parameter is not supplied, there is an error
292 define('VALUE_REQUIRED', 1);
295 * VALUE_OPTIONAL - if the parameter is not supplied, then the param has no value
297 define('VALUE_OPTIONAL', 2);
300 * VALUE_DEFAULT - if the parameter is not supplied, then the default value is used
302 define('VALUE_DEFAULT', 0);
305 * NULL_NOT_ALLOWED - the parameter can not be set to null in the database
307 define('NULL_NOT_ALLOWED', false);
310 * NULL_ALLOWED - the parameter can be set to null in the database
312 define('NULL_ALLOWED', true);
314 /// Page types ///
316 * PAGE_COURSE_VIEW is a definition of a page type. For more information on the page class see moodle/lib/pagelib.php.
318 define('PAGE_COURSE_VIEW', 'course-view');
320 /** Get remote addr constant */
321 define('GETREMOTEADDR_SKIP_HTTP_CLIENT_IP', '1');
322 /** Get remote addr constant */
323 define('GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR', '2');
325 /// Blog access level constant declaration ///
326 define ('BLOG_USER_LEVEL', 1);
327 define ('BLOG_GROUP_LEVEL', 2);
328 define ('BLOG_COURSE_LEVEL', 3);
329 define ('BLOG_SITE_LEVEL', 4);
330 define ('BLOG_GLOBAL_LEVEL', 5);
333 ///Tag constants///
335 * To prevent problems with multibytes strings,Flag updating in nav not working on the review page. this should not exceed the
336 * length of "varchar(255) / 3 (bytes / utf-8 character) = 85".
337 * TODO: this is not correct, varchar(255) are 255 unicode chars ;-)
339 * @todo define(TAG_MAX_LENGTH) this is not correct, varchar(255) are 255 unicode chars ;-)
341 define('TAG_MAX_LENGTH', 50);
343 /// Password policy constants ///
344 define ('PASSWORD_LOWER', 'abcdefghijklmnopqrstuvwxyz');
345 define ('PASSWORD_UPPER', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
346 define ('PASSWORD_DIGITS', '0123456789');
347 define ('PASSWORD_NONALPHANUM', '.,;:!?_-+/*@#&$');
349 /// Feature constants ///
350 // Used for plugin_supports() to report features that are, or are not, supported by a module.
352 /** True if module can provide a grade */
353 define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade');
354 /** True if module supports outcomes */
355 define('FEATURE_GRADE_OUTCOMES', 'outcomes');
357 /** True if module has code to track whether somebody viewed it */
358 define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views');
359 /** True if module has custom completion rules */
360 define('FEATURE_COMPLETION_HAS_RULES', 'completion_has_rules');
362 /** True if module has no 'view' page (like label) */
363 define('FEATURE_NO_VIEW_LINK', 'viewlink');
364 /** True if module supports outcomes */
365 define('FEATURE_IDNUMBER', 'idnumber');
366 /** True if module supports groups */
367 define('FEATURE_GROUPS', 'groups');
368 /** True if module supports groupings */
369 define('FEATURE_GROUPINGS', 'groupings');
370 /** True if module supports groupmembersonly */
371 define('FEATURE_GROUPMEMBERSONLY', 'groupmembersonly');
373 /** Type of module */
374 define('FEATURE_MOD_ARCHETYPE', 'mod_archetype');
375 /** True if module supports intro editor */
376 define('FEATURE_MOD_INTRO', 'mod_intro');
377 /** True if module has default completion */
378 define('FEATURE_MODEDIT_DEFAULT_COMPLETION', 'modedit_default_completion');
380 define('FEATURE_COMMENT', 'comment');
382 define('FEATURE_RATE', 'rate');
383 /** True if module supports backup/restore of moodle2 format */
384 define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2');
386 /** Unspecified module archetype */
387 define('MOD_ARCHETYPE_OTHER', 0);
388 /** Resource-like type module */
389 define('MOD_ARCHETYPE_RESOURCE', 1);
390 /** Assignment module archetype */
391 define('MOD_ARCHETYPE_ASSIGNMENT', 2);
394 * Security token used for allowing access
395 * from external application such as web services.
396 * Scripts do not use any session, performance is relatively
397 * low because we need to load access info in each request.
398 * Scripts are executed in parallel.
400 define('EXTERNAL_TOKEN_PERMANENT', 0);
403 * Security token used for allowing access
404 * of embedded applications, the code is executed in the
405 * active user session. Token is invalidated after user logs out.
406 * Scripts are executed serially - normal session locking is used.
408 define('EXTERNAL_TOKEN_EMBEDDED', 1);
411 * The home page should be the site home
413 define('HOMEPAGE_SITE', 0);
415 * The home page should be the users my page
417 define('HOMEPAGE_MY', 1);
419 * The home page can be chosen by the user
421 define('HOMEPAGE_USER', 2);
424 * Hub directory url (should be moodle.org)
426 define('HUB_HUBDIRECTORYURL', "http://hubdirectory.moodle.org");
430 * Moodle.org url (should be moodle.org)
432 define('HUB_MOODLEORGHUBURL', "http://hub.moodle.org");
435 * Moodle mobile app service name
437 define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app');
439 /// PARAMETER HANDLING ////////////////////////////////////////////////////
442 * Returns a particular value for the named variable, taken from
443 * POST or GET. If the parameter doesn't exist then an error is
444 * thrown because we require this variable.
446 * This function should be used to initialise all required values
447 * in a script that are based on parameters. Usually it will be
448 * used like this:
449 * $id = required_param('id', PARAM_INT);
451 * Please note the $type parameter is now required,
452 * for now PARAM_CLEAN is used for backwards compatibility only.
454 * @param string $parname the name of the page parameter we want
455 * @param string $type expected type of parameter
456 * @return mixed
458 function required_param($parname, $type) {
459 if (!isset($type)) {
460 debugging('required_param() requires $type to be specified.');
461 $type = PARAM_CLEAN; // for now let's use this deprecated type
463 if (isset($_POST[$parname])) { // POST has precedence
464 $param = $_POST[$parname];
465 } else if (isset($_GET[$parname])) {
466 $param = $_GET[$parname];
467 } else {
468 print_error('missingparam', '', '', $parname);
471 return clean_param($param, $type);
475 * Returns a particular value for the named variable, taken from
476 * POST or GET, otherwise returning a given default.
478 * This function should be used to initialise all optional values
479 * in a script that are based on parameters. Usually it will be
480 * used like this:
481 * $name = optional_param('name', 'Fred', PARAM_TEXT);
483 * Please note $default and $type parameters are now required,
484 * for now PARAM_CLEAN is used for backwards compatibility only.
486 * @param string $parname the name of the page parameter we want
487 * @param mixed $default the default value to return if nothing is found
488 * @param string $type expected type of parameter
489 * @return mixed
491 function optional_param($parname, $default, $type) {
492 if (!isset($type)) {
493 debugging('optional_param() requires $default and $type to be specified.');
494 $type = PARAM_CLEAN; // for now let's use this deprecated type
496 if (!isset($default)) {
497 $default = null;
500 if (isset($_POST[$parname])) { // POST has precedence
501 $param = $_POST[$parname];
502 } else if (isset($_GET[$parname])) {
503 $param = $_GET[$parname];
504 } else {
505 return $default;
508 return clean_param($param, $type);
512 * Strict validation of parameter values, the values are only converted
513 * to requested PHP type. Internally it is using clean_param, the values
514 * before and after cleaning must be equal - otherwise
515 * an invalid_parameter_exception is thrown.
516 * Objects and classes are not accepted.
518 * @param mixed $param
519 * @param int $type PARAM_ constant
520 * @param bool $allownull are nulls valid value?
521 * @param string $debuginfo optional debug information
522 * @return mixed the $param value converted to PHP type or invalid_parameter_exception
524 function validate_param($param, $type, $allownull=NULL_NOT_ALLOWED, $debuginfo='') {
525 if (is_null($param)) {
526 if ($allownull == NULL_ALLOWED) {
527 return null;
528 } else {
529 throw new invalid_parameter_exception($debuginfo);
532 if (is_array($param) or is_object($param)) {
533 throw new invalid_parameter_exception($debuginfo);
536 $cleaned = clean_param($param, $type);
537 if ((string)$param !== (string)$cleaned) {
538 // conversion to string is usually lossless
539 throw new invalid_parameter_exception($debuginfo);
542 return $cleaned;
546 * Used by {@link optional_param()} and {@link required_param()} to
547 * clean the variables and/or cast to specific types, based on
548 * an options field.
549 * <code>
550 * $course->format = clean_param($course->format, PARAM_ALPHA);
551 * $selectedgrade_item = clean_param($selectedgrade_item, PARAM_INT);
552 * </code>
554 * @param mixed $param the variable we are cleaning
555 * @param string $type expected format of param after cleaning.
556 * @return mixed
558 function clean_param($param, $type) {
560 global $CFG;
562 if (is_array($param)) { // Let's loop
563 $newparam = array();
564 foreach ($param as $key => $value) {
565 $newparam[$key] = clean_param($value, $type);
567 return $newparam;
570 switch ($type) {
571 case PARAM_RAW: // no cleaning at all
572 return $param;
574 case PARAM_RAW_TRIMMED: // no cleaning, but strip leading and trailing whitespace.
575 return trim($param);
577 case PARAM_CLEAN: // General HTML cleaning, try to use more specific type if possible
578 // this is deprecated!, please use more specific type instead
579 if (is_numeric($param)) {
580 return $param;
582 return clean_text($param); // Sweep for scripts, etc
584 case PARAM_CLEANHTML: // clean html fragment
585 $param = clean_text($param, FORMAT_HTML); // Sweep for scripts, etc
586 return trim($param);
588 case PARAM_INT:
589 return (int)$param; // Convert to integer
591 case PARAM_FLOAT:
592 case PARAM_NUMBER:
593 return (float)$param; // Convert to float
595 case PARAM_ALPHA: // Remove everything not a-z
596 return preg_replace('/[^a-zA-Z]/i', '', $param);
598 case PARAM_ALPHAEXT: // Remove everything not a-zA-Z_- (originally allowed "/" too)
599 return preg_replace('/[^a-zA-Z_-]/i', '', $param);
601 case PARAM_ALPHANUM: // Remove everything not a-zA-Z0-9
602 return preg_replace('/[^A-Za-z0-9]/i', '', $param);
604 case PARAM_ALPHANUMEXT: // Remove everything not a-zA-Z0-9_-
605 return preg_replace('/[^A-Za-z0-9_-]/i', '', $param);
607 case PARAM_SEQUENCE: // Remove everything not 0-9,
608 return preg_replace('/[^0-9,]/i', '', $param);
610 case PARAM_BOOL: // Convert to 1 or 0
611 $tempstr = strtolower($param);
612 if ($tempstr === 'on' or $tempstr === 'yes' or $tempstr === 'true') {
613 $param = 1;
614 } else if ($tempstr === 'off' or $tempstr === 'no' or $tempstr === 'false') {
615 $param = 0;
616 } else {
617 $param = empty($param) ? 0 : 1;
619 return $param;
621 case PARAM_NOTAGS: // Strip all tags
622 return strip_tags($param);
624 case PARAM_TEXT: // leave only tags needed for multilang
625 // if the multilang syntax is not correct we strip all tags
626 // because it would break xhtml strict which is required for accessibility standards
627 // please note this cleaning does not strip unbalanced '>' for BC compatibility reasons
628 do {
629 if (strpos($param, '</lang>') !== false) {
630 // old and future mutilang syntax
631 $param = strip_tags($param, '<lang>');
632 if (!preg_match_all('/<.*>/suU', $param, $matches)) {
633 break;
635 $open = false;
636 foreach ($matches[0] as $match) {
637 if ($match === '</lang>') {
638 if ($open) {
639 $open = false;
640 continue;
641 } else {
642 break 2;
645 if (!preg_match('/^<lang lang="[a-zA-Z0-9_-]+"\s*>$/u', $match)) {
646 break 2;
647 } else {
648 $open = true;
651 if ($open) {
652 break;
654 return $param;
656 } else if (strpos($param, '</span>') !== false) {
657 // current problematic multilang syntax
658 $param = strip_tags($param, '<span>');
659 if (!preg_match_all('/<.*>/suU', $param, $matches)) {
660 break;
662 $open = false;
663 foreach ($matches[0] as $match) {
664 if ($match === '</span>') {
665 if ($open) {
666 $open = false;
667 continue;
668 } else {
669 break 2;
672 if (!preg_match('/^<span(\s+lang="[a-zA-Z0-9_-]+"|\s+class="multilang"){2}\s*>$/u', $match)) {
673 break 2;
674 } else {
675 $open = true;
678 if ($open) {
679 break;
681 return $param;
683 } while (false);
684 // easy, just strip all tags, if we ever want to fix orphaned '&' we have to do that in format_string()
685 return strip_tags($param);
687 case PARAM_SAFEDIR: // Remove everything not a-zA-Z0-9_-
688 return preg_replace('/[^a-zA-Z0-9_-]/i', '', $param);
690 case PARAM_SAFEPATH: // Remove everything not a-zA-Z0-9/_-
691 return preg_replace('/[^a-zA-Z0-9\/_-]/i', '', $param);
693 case PARAM_FILE: // Strip all suspicious characters from filename
694 $param = preg_replace('~[[:cntrl:]]|[&<>"`\|\':\\\\/]~u', '', $param);
695 $param = preg_replace('~\.\.+~', '', $param);
696 if ($param === '.') {
697 $param = '';
699 return $param;
701 case PARAM_PATH: // Strip all suspicious characters from file path
702 $param = str_replace('\\', '/', $param);
703 $param = preg_replace('~[[:cntrl:]]|[&<>"`\|\':]~u', '', $param);
704 $param = preg_replace('~\.\.+~', '', $param);
705 $param = preg_replace('~//+~', '/', $param);
706 return preg_replace('~/(\./)+~', '/', $param);
708 case PARAM_HOST: // allow FQDN or IPv4 dotted quad
709 $param = preg_replace('/[^\.\d\w-]/','', $param ); // only allowed chars
710 // match ipv4 dotted quad
711 if (preg_match('/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/',$param, $match)){
712 // confirm values are ok
713 if ( $match[0] > 255
714 || $match[1] > 255
715 || $match[3] > 255
716 || $match[4] > 255 ) {
717 // hmmm, what kind of dotted quad is this?
718 $param = '';
720 } elseif ( preg_match('/^[\w\d\.-]+$/', $param) // dots, hyphens, numbers
721 && !preg_match('/^[\.-]/', $param) // no leading dots/hyphens
722 && !preg_match('/[\.-]$/', $param) // no trailing dots/hyphens
724 // all is ok - $param is respected
725 } else {
726 // all is not ok...
727 $param='';
729 return $param;
731 case PARAM_URL: // allow safe ftp, http, mailto urls
732 include_once($CFG->dirroot . '/lib/validateurlsyntax.php');
733 if (!empty($param) && validateUrlSyntax($param, 's?H?S?F?E?u-P-a?I?p?f?q?r?')) {
734 // all is ok, param is respected
735 } else {
736 $param =''; // not really ok
738 return $param;
740 case PARAM_LOCALURL: // allow http absolute, root relative and relative URLs within wwwroot
741 $param = clean_param($param, PARAM_URL);
742 if (!empty($param)) {
743 if (preg_match(':^/:', $param)) {
744 // root-relative, ok!
745 } elseif (preg_match('/^'.preg_quote($CFG->wwwroot, '/').'/i',$param)) {
746 // absolute, and matches our wwwroot
747 } else {
748 // relative - let's make sure there are no tricks
749 if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?')) {
750 // looks ok.
751 } else {
752 $param = '';
756 return $param;
758 case PARAM_PEM:
759 $param = trim($param);
760 // PEM formatted strings may contain letters/numbers and the symbols
761 // forward slash: /
762 // plus sign: +
763 // equal sign: =
764 // , surrounded by BEGIN and END CERTIFICATE prefix and suffixes
765 if (preg_match('/^-----BEGIN CERTIFICATE-----([\s\w\/\+=]+)-----END CERTIFICATE-----$/', trim($param), $matches)) {
766 list($wholething, $body) = $matches;
767 unset($wholething, $matches);
768 $b64 = clean_param($body, PARAM_BASE64);
769 if (!empty($b64)) {
770 return "-----BEGIN CERTIFICATE-----\n$b64\n-----END CERTIFICATE-----\n";
771 } else {
772 return '';
775 return '';
777 case PARAM_BASE64:
778 if (!empty($param)) {
779 // PEM formatted strings may contain letters/numbers and the symbols
780 // forward slash: /
781 // plus sign: +
782 // equal sign: =
783 if (0 >= preg_match('/^([\s\w\/\+=]+)$/', trim($param))) {
784 return '';
786 $lines = preg_split('/[\s]+/', $param, -1, PREG_SPLIT_NO_EMPTY);
787 // Each line of base64 encoded data must be 64 characters in
788 // length, except for the last line which may be less than (or
789 // equal to) 64 characters long.
790 for ($i=0, $j=count($lines); $i < $j; $i++) {
791 if ($i + 1 == $j) {
792 if (64 < strlen($lines[$i])) {
793 return '';
795 continue;
798 if (64 != strlen($lines[$i])) {
799 return '';
802 return implode("\n",$lines);
803 } else {
804 return '';
807 case PARAM_TAG:
808 // Please note it is not safe to use the tag name directly anywhere,
809 // it must be processed with s(), urlencode() before embedding anywhere.
810 // remove some nasties
811 $param = preg_replace('~[[:cntrl:]]|[<>`]~u', '', $param);
812 //convert many whitespace chars into one
813 $param = preg_replace('/\s+/', ' ', $param);
814 $textlib = textlib_get_instance();
815 $param = $textlib->substr(trim($param), 0, TAG_MAX_LENGTH);
816 return $param;
818 case PARAM_TAGLIST:
819 $tags = explode(',', $param);
820 $result = array();
821 foreach ($tags as $tag) {
822 $res = clean_param($tag, PARAM_TAG);
823 if ($res !== '') {
824 $result[] = $res;
827 if ($result) {
828 return implode(',', $result);
829 } else {
830 return '';
833 case PARAM_CAPABILITY:
834 if (get_capability_info($param)) {
835 return $param;
836 } else {
837 return '';
840 case PARAM_PERMISSION:
841 $param = (int)$param;
842 if (in_array($param, array(CAP_INHERIT, CAP_ALLOW, CAP_PREVENT, CAP_PROHIBIT))) {
843 return $param;
844 } else {
845 return CAP_INHERIT;
848 case PARAM_AUTH:
849 $param = clean_param($param, PARAM_SAFEDIR);
850 if (exists_auth_plugin($param)) {
851 return $param;
852 } else {
853 return '';
856 case PARAM_LANG:
857 $param = clean_param($param, PARAM_SAFEDIR);
858 if (get_string_manager()->translation_exists($param)) {
859 return $param;
860 } else {
861 return ''; // Specified language is not installed or param malformed
864 case PARAM_THEME:
865 $param = clean_param($param, PARAM_SAFEDIR);
866 if (file_exists("$CFG->dirroot/theme/$param/config.php")) {
867 return $param;
868 } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$param/config.php")) {
869 return $param;
870 } else {
871 return ''; // Specified theme is not installed
874 case PARAM_USERNAME:
875 $param = str_replace(" " , "", $param);
876 $param = moodle_strtolower($param); // Convert uppercase to lowercase MDL-16919
877 if (empty($CFG->extendedusernamechars)) {
878 // regular expression, eliminate all chars EXCEPT:
879 // alphanum, dash (-), underscore (_), at sign (@) and period (.) characters.
880 $param = preg_replace('/[^-\.@_a-z0-9]/', '', $param);
882 return $param;
884 case PARAM_EMAIL:
885 if (validate_email($param)) {
886 return $param;
887 } else {
888 return '';
891 case PARAM_STRINGID:
892 if (preg_match('|^[a-zA-Z][a-zA-Z0-9\.:/_-]*$|', $param)) {
893 return $param;
894 } else {
895 return '';
898 case PARAM_TIMEZONE: //can be int, float(with .5 or .0) or string seperated by '/' and can have '-_'
899 $timezonepattern = '/^(([+-]?(0?[0-9](\.[5|0])?|1[0-3]|1[0-2]\.5))|(99)|[[:alnum:]]+(\/?[[:alpha:]_-])+)$/';
900 if (preg_match($timezonepattern, $param)) {
901 return $param;
902 } else {
903 return '';
906 default: // throw error, switched parameters in optional_param or another serious problem
907 print_error("unknownparamtype", '', '', $type);
912 * Return true if given value is integer or string with integer value
914 * @param mixed $value String or Int
915 * @return bool true if number, false if not
917 function is_number($value) {
918 if (is_int($value)) {
919 return true;
920 } else if (is_string($value)) {
921 return ((string)(int)$value) === $value;
922 } else {
923 return false;
928 * Returns host part from url
929 * @param string $url full url
930 * @return string host, null if not found
932 function get_host_from_url($url) {
933 preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches);
934 if ($matches) {
935 return $matches[1];
937 return null;
941 * Tests whether anything was returned by text editor
943 * This function is useful for testing whether something you got back from
944 * the HTML editor actually contains anything. Sometimes the HTML editor
945 * appear to be empty, but actually you get back a <br> tag or something.
947 * @param string $string a string containing HTML.
948 * @return boolean does the string contain any actual content - that is text,
949 * images, objects, etc.
951 function html_is_blank($string) {
952 return trim(strip_tags($string, '<img><object><applet><input><select><textarea><hr>')) == '';
956 * Set a key in global configuration
958 * Set a key/value pair in both this session's {@link $CFG} global variable
959 * and in the 'config' database table for future sessions.
961 * Can also be used to update keys for plugin-scoped configs in config_plugin table.
962 * In that case it doesn't affect $CFG.
964 * A NULL value will delete the entry.
966 * @global object
967 * @global object
968 * @param string $name the key to set
969 * @param string $value the value to set (without magic quotes)
970 * @param string $plugin (optional) the plugin scope, default NULL
971 * @return bool true or exception
973 function set_config($name, $value, $plugin=NULL) {
974 global $CFG, $DB;
976 if (empty($plugin)) {
977 if (!array_key_exists($name, $CFG->config_php_settings)) {
978 // So it's defined for this invocation at least
979 if (is_null($value)) {
980 unset($CFG->$name);
981 } else {
982 $CFG->$name = (string)$value; // settings from db are always strings
986 if ($DB->get_field('config', 'name', array('name'=>$name))) {
987 if ($value === null) {
988 $DB->delete_records('config', array('name'=>$name));
989 } else {
990 $DB->set_field('config', 'value', $value, array('name'=>$name));
992 } else {
993 if ($value !== null) {
994 $config = new stdClass();
995 $config->name = $name;
996 $config->value = $value;
997 $DB->insert_record('config', $config, false);
1001 } else { // plugin scope
1002 if ($id = $DB->get_field('config_plugins', 'id', array('name'=>$name, 'plugin'=>$plugin))) {
1003 if ($value===null) {
1004 $DB->delete_records('config_plugins', array('name'=>$name, 'plugin'=>$plugin));
1005 } else {
1006 $DB->set_field('config_plugins', 'value', $value, array('id'=>$id));
1008 } else {
1009 if ($value !== null) {
1010 $config = new stdClass();
1011 $config->plugin = $plugin;
1012 $config->name = $name;
1013 $config->value = $value;
1014 $DB->insert_record('config_plugins', $config, false);
1019 return true;
1023 * Get configuration values from the global config table
1024 * or the config_plugins table.
1026 * If called with one parameter, it will load all the config
1027 * variables for one plugin, and return them as an object.
1029 * If called with 2 parameters it will return a string single
1030 * value or false if the value is not found.
1032 * @param string $plugin full component name
1033 * @param string $name default NULL
1034 * @return mixed hash-like object or single value, return false no config found
1036 function get_config($plugin, $name = NULL) {
1037 global $CFG, $DB;
1039 // normalise component name
1040 if ($plugin === 'moodle' or $plugin === 'core') {
1041 $plugin = NULL;
1044 if (!empty($name)) { // the user is asking for a specific value
1045 if (!empty($plugin)) {
1046 if (isset($CFG->forced_plugin_settings[$plugin]) and array_key_exists($name, $CFG->forced_plugin_settings[$plugin])) {
1047 // setting forced in config file
1048 return $CFG->forced_plugin_settings[$plugin][$name];
1049 } else {
1050 return $DB->get_field('config_plugins', 'value', array('plugin'=>$plugin, 'name'=>$name));
1052 } else {
1053 if (array_key_exists($name, $CFG->config_php_settings)) {
1054 // setting force in config file
1055 return $CFG->config_php_settings[$name];
1056 } else {
1057 return $DB->get_field('config', 'value', array('name'=>$name));
1062 // the user is after a recordset
1063 if ($plugin) {
1064 $localcfg = $DB->get_records_menu('config_plugins', array('plugin'=>$plugin), '', 'name,value');
1065 if (isset($CFG->forced_plugin_settings[$plugin])) {
1066 foreach($CFG->forced_plugin_settings[$plugin] as $n=>$v) {
1067 if (is_null($v) or is_array($v) or is_object($v)) {
1068 // we do not want any extra mess here, just real settings that could be saved in db
1069 unset($localcfg[$n]);
1070 } else {
1071 //convert to string as if it went through the DB
1072 $localcfg[$n] = (string)$v;
1076 if ($localcfg) {
1077 return (object)$localcfg;
1078 } else {
1079 return new stdClass();
1082 } else {
1083 // this part is not really used any more, but anyway...
1084 $localcfg = $DB->get_records_menu('config', array(), '', 'name,value');
1085 foreach($CFG->config_php_settings as $n=>$v) {
1086 if (is_null($v) or is_array($v) or is_object($v)) {
1087 // we do not want any extra mess here, just real settings that could be saved in db
1088 unset($localcfg[$n]);
1089 } else {
1090 //convert to string as if it went through the DB
1091 $localcfg[$n] = (string)$v;
1094 return (object)$localcfg;
1099 * Removes a key from global configuration
1101 * @param string $name the key to set
1102 * @param string $plugin (optional) the plugin scope
1103 * @global object
1104 * @return boolean whether the operation succeeded.
1106 function unset_config($name, $plugin=NULL) {
1107 global $CFG, $DB;
1109 if (empty($plugin)) {
1110 unset($CFG->$name);
1111 $DB->delete_records('config', array('name'=>$name));
1112 } else {
1113 $DB->delete_records('config_plugins', array('name'=>$name, 'plugin'=>$plugin));
1116 return true;
1120 * Remove all the config variables for a given plugin.
1122 * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice';
1123 * @return boolean whether the operation succeeded.
1125 function unset_all_config_for_plugin($plugin) {
1126 global $DB;
1127 $DB->delete_records('config_plugins', array('plugin' => $plugin));
1128 $like = $DB->sql_like('name', '?', true, true, false, '|');
1129 $params = array($DB->sql_like_escape($plugin.'_', '|') . '%');
1130 $DB->delete_records_select('config', $like, $params);
1131 return true;
1135 * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability.
1137 * All users are verified if they still have the necessary capability.
1139 * @param string $value the value of the config setting.
1140 * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor.
1141 * @param bool $include admins, include administrators
1142 * @return array of user objects.
1144 function get_users_from_config($value, $capability, $includeadmins = true) {
1145 global $CFG, $DB;
1147 if (empty($value) or $value === '$@NONE@$') {
1148 return array();
1151 // we have to make sure that users still have the necessary capability,
1152 // it should be faster to fetch them all first and then test if they are present
1153 // instead of validating them one-by-one
1154 $users = get_users_by_capability(get_context_instance(CONTEXT_SYSTEM), $capability);
1155 if ($includeadmins) {
1156 $admins = get_admins();
1157 foreach ($admins as $admin) {
1158 $users[$admin->id] = $admin;
1162 if ($value === '$@ALL@$') {
1163 return $users;
1166 $result = array(); // result in correct order
1167 $allowed = explode(',', $value);
1168 foreach ($allowed as $uid) {
1169 if (isset($users[$uid])) {
1170 $user = $users[$uid];
1171 $result[$user->id] = $user;
1175 return $result;
1180 * Invalidates browser caches and cached data in temp
1181 * @return void
1183 function purge_all_caches() {
1184 global $CFG;
1186 reset_text_filters_cache();
1187 js_reset_all_caches();
1188 theme_reset_all_caches();
1189 get_string_manager()->reset_caches();
1191 // purge all other caches: rss, simplepie, etc.
1192 remove_dir($CFG->dataroot.'/cache', true);
1194 // make sure cache dir is writable, throws exception if not
1195 make_upload_directory('cache');
1197 // hack: this script may get called after the purifier was initialised,
1198 // but we do not want to verify repeatedly this exists in each call
1199 make_upload_directory('cache/htmlpurifier');
1201 clearstatcache();
1205 * Get volatile flags
1207 * @param string $type
1208 * @param int $changedsince default null
1209 * @return records array
1211 function get_cache_flags($type, $changedsince=NULL) {
1212 global $DB;
1214 $params = array('type'=>$type, 'expiry'=>time());
1215 $sqlwhere = "flagtype = :type AND expiry >= :expiry";
1216 if ($changedsince !== NULL) {
1217 $params['changedsince'] = $changedsince;
1218 $sqlwhere .= " AND timemodified > :changedsince";
1220 $cf = array();
1222 if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) {
1223 foreach ($flags as $flag) {
1224 $cf[$flag->name] = $flag->value;
1227 return $cf;
1231 * Get volatile flags
1233 * @param string $type
1234 * @param string $name
1235 * @param int $changedsince default null
1236 * @return records array
1238 function get_cache_flag($type, $name, $changedsince=NULL) {
1239 global $DB;
1241 $params = array('type'=>$type, 'name'=>$name, 'expiry'=>time());
1243 $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry";
1244 if ($changedsince !== NULL) {
1245 $params['changedsince'] = $changedsince;
1246 $sqlwhere .= " AND timemodified > :changedsince";
1249 return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params);
1253 * Set a volatile flag
1255 * @param string $type the "type" namespace for the key
1256 * @param string $name the key to set
1257 * @param string $value the value to set (without magic quotes) - NULL will remove the flag
1258 * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs
1259 * @return bool Always returns true
1261 function set_cache_flag($type, $name, $value, $expiry=NULL) {
1262 global $DB;
1264 $timemodified = time();
1265 if ($expiry===NULL || $expiry < $timemodified) {
1266 $expiry = $timemodified + 24 * 60 * 60;
1267 } else {
1268 $expiry = (int)$expiry;
1271 if ($value === NULL) {
1272 unset_cache_flag($type,$name);
1273 return true;
1276 if ($f = $DB->get_record('cache_flags', array('name'=>$name, 'flagtype'=>$type), '*', IGNORE_MULTIPLE)) { // this is a potential problem in DEBUG_DEVELOPER
1277 if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) {
1278 return true; //no need to update; helps rcache too
1280 $f->value = $value;
1281 $f->expiry = $expiry;
1282 $f->timemodified = $timemodified;
1283 $DB->update_record('cache_flags', $f);
1284 } else {
1285 $f = new stdClass();
1286 $f->flagtype = $type;
1287 $f->name = $name;
1288 $f->value = $value;
1289 $f->expiry = $expiry;
1290 $f->timemodified = $timemodified;
1291 $DB->insert_record('cache_flags', $f);
1293 return true;
1297 * Removes a single volatile flag
1299 * @global object
1300 * @param string $type the "type" namespace for the key
1301 * @param string $name the key to set
1302 * @return bool
1304 function unset_cache_flag($type, $name) {
1305 global $DB;
1306 $DB->delete_records('cache_flags', array('name'=>$name, 'flagtype'=>$type));
1307 return true;
1311 * Garbage-collect volatile flags
1313 * @return bool Always returns true
1315 function gc_cache_flags() {
1316 global $DB;
1317 $DB->delete_records_select('cache_flags', 'expiry < ?', array(time()));
1318 return true;
1321 /// FUNCTIONS FOR HANDLING USER PREFERENCES ////////////////////////////////////
1324 * Refresh user preference cache. This is used most often for $USER
1325 * object that is stored in session, but it also helps with performance in cron script.
1327 * Preferences for each user are loaded on first use on every page, then again after the timeout expires.
1329 * @param stdClass $user user object, preferences are preloaded into ->preference property
1330 * @param int $cachelifetime cache life time on the current page (ins seconds)
1331 * @return void
1333 function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120) {
1334 global $DB;
1335 static $loadedusers = array(); // Static cache, we need to check on each page load, not only every 2 minutes.
1337 if (!isset($user->id)) {
1338 throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field');
1341 if (empty($user->id) or isguestuser($user->id)) {
1342 // No permanent storage for not-logged-in users and guest
1343 if (!isset($user->preference)) {
1344 $user->preference = array();
1346 return;
1349 $timenow = time();
1351 if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) {
1352 // Already loaded at least once on this page. Are we up to date?
1353 if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) {
1354 // no need to reload - we are on the same page and we loaded prefs just a moment ago
1355 return;
1357 } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) {
1358 // no change since the lastcheck on this page
1359 $user->preference['_lastloaded'] = $timenow;
1360 return;
1364 // OK, so we have to reload all preferences
1365 $loadedusers[$user->id] = true;
1366 $user->preference = $DB->get_records_menu('user_preferences', array('userid'=>$user->id), '', 'name,value'); // All values
1367 $user->preference['_lastloaded'] = $timenow;
1371 * Called from set/delete_user_preferences, so that the prefs can
1372 * be correctly reloaded in different sessions.
1374 * NOTE: internal function, do not call from other code.
1376 * @param integer $userid the user whose prefs were changed.
1377 * @return void
1379 function mark_user_preferences_changed($userid) {
1380 global $CFG;
1382 if (empty($userid) or isguestuser($userid)) {
1383 // no cache flags for guest and not-logged-in users
1384 return;
1387 set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout);
1391 * Sets a preference for the specified user.
1393 * If user object submitted, 'preference' property contains the preferences cache.
1395 * @param string $name The key to set as preference for the specified user
1396 * @param string $value The value to set for the $name key in the specified user's record,
1397 * null means delete current value
1398 * @param stdClass|int $user A moodle user object or id, null means current user
1399 * @return bool always true or exception
1401 function set_user_preference($name, $value, $user = null) {
1402 global $USER, $DB;
1404 if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1405 throw new coding_exception('Invalid preference name in set_user_preference() call');
1408 if (is_null($value)) {
1409 // null means delete current
1410 return unset_user_preference($name, $user);
1411 } else if (is_object($value)) {
1412 throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed');
1413 } else if (is_array($value)) {
1414 throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed');
1416 $value = (string)$value;
1418 if (is_null($user)) {
1419 $user = $USER;
1420 } else if (isset($user->id)) {
1421 // $user is valid object
1422 } else if (is_numeric($user)) {
1423 $user = (object)array('id'=>(int)$user);
1424 } else {
1425 throw new coding_exception('Invalid $user parameter in set_user_preference() call');
1428 check_user_preferences_loaded($user);
1430 if (empty($user->id) or isguestuser($user->id)) {
1431 // no permanent storage for not-logged-in users and guest
1432 $user->preference[$name] = $value;
1433 return true;
1436 if ($preference = $DB->get_record('user_preferences', array('userid'=>$user->id, 'name'=>$name))) {
1437 if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) {
1438 // preference already set to this value
1439 return true;
1441 $DB->set_field('user_preferences', 'value', $value, array('id'=>$preference->id));
1443 } else {
1444 $preference = new stdClass();
1445 $preference->userid = $user->id;
1446 $preference->name = $name;
1447 $preference->value = $value;
1448 $DB->insert_record('user_preferences', $preference);
1451 // update value in cache
1452 $user->preference[$name] = $value;
1454 // set reload flag for other sessions
1455 mark_user_preferences_changed($user->id);
1457 return true;
1461 * Sets a whole array of preferences for the current user
1463 * If user object submitted, 'preference' property contains the preferences cache.
1465 * @param array $prefarray An array of key/value pairs to be set
1466 * @param stdClass|int $user A moodle user object or id, null means current user
1467 * @return bool always true or exception
1469 function set_user_preferences(array $prefarray, $user = null) {
1470 foreach ($prefarray as $name => $value) {
1471 set_user_preference($name, $value, $user);
1473 return true;
1477 * Unsets a preference completely by deleting it from the database
1479 * If user object submitted, 'preference' property contains the preferences cache.
1481 * @param string $name The key to unset as preference for the specified user
1482 * @param stdClass|int $user A moodle user object or id, null means current user
1483 * @return bool always true or exception
1485 function unset_user_preference($name, $user = null) {
1486 global $USER, $DB;
1488 if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1489 throw new coding_exception('Invalid preference name in unset_user_preference() call');
1492 if (is_null($user)) {
1493 $user = $USER;
1494 } else if (isset($user->id)) {
1495 // $user is valid object
1496 } else if (is_numeric($user)) {
1497 $user = (object)array('id'=>(int)$user);
1498 } else {
1499 throw new coding_exception('Invalid $user parameter in unset_user_preference() call');
1502 check_user_preferences_loaded($user);
1504 if (empty($user->id) or isguestuser($user->id)) {
1505 // no permanent storage for not-logged-in user and guest
1506 unset($user->preference[$name]);
1507 return true;
1510 // delete from DB
1511 $DB->delete_records('user_preferences', array('userid'=>$user->id, 'name'=>$name));
1513 // delete the preference from cache
1514 unset($user->preference[$name]);
1516 // set reload flag for other sessions
1517 mark_user_preferences_changed($user->id);
1519 return true;
1523 * Used to fetch user preference(s)
1525 * If no arguments are supplied this function will return
1526 * all of the current user preferences as an array.
1528 * If a name is specified then this function
1529 * attempts to return that particular preference value. If
1530 * none is found, then the optional value $default is returned,
1531 * otherwise NULL.
1533 * If user object submitted, 'preference' property contains the preferences cache.
1535 * @param string $name Name of the key to use in finding a preference value
1536 * @param mixed $default Value to be returned if the $name key is not set in the user preferences
1537 * @param stdClass|int $user A moodle user object or id, null means current user
1538 * @return mixed string value or default
1540 function get_user_preferences($name = null, $default = null, $user = null) {
1541 global $USER;
1543 if (is_null($name)) {
1544 // all prefs
1545 } else if (is_numeric($name) or $name === '_lastloaded') {
1546 throw new coding_exception('Invalid preference name in get_user_preferences() call');
1549 if (is_null($user)) {
1550 $user = $USER;
1551 } else if (isset($user->id)) {
1552 // $user is valid object
1553 } else if (is_numeric($user)) {
1554 $user = (object)array('id'=>(int)$user);
1555 } else {
1556 throw new coding_exception('Invalid $user parameter in get_user_preferences() call');
1559 check_user_preferences_loaded($user);
1561 if (empty($name)) {
1562 return $user->preference; // All values
1563 } else if (isset($user->preference[$name])) {
1564 return $user->preference[$name]; // The single string value
1565 } else {
1566 return $default; // Default value (null if not specified)
1570 /// FUNCTIONS FOR HANDLING TIME ////////////////////////////////////////////
1573 * Given date parts in user time produce a GMT timestamp.
1575 * @todo Finish documenting this function
1576 * @param int $year The year part to create timestamp of
1577 * @param int $month The month part to create timestamp of
1578 * @param int $day The day part to create timestamp of
1579 * @param int $hour The hour part to create timestamp of
1580 * @param int $minute The minute part to create timestamp of
1581 * @param int $second The second part to create timestamp of
1582 * @param mixed $timezone Timezone modifier, if 99 then use default user's timezone
1583 * @param bool $applydst Toggle Daylight Saving Time, default true, will be
1584 * applied only if timezone is 99 or string.
1585 * @return int timestamp
1587 function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) {
1589 //save input timezone, required for dst offset check.
1590 $passedtimezone = $timezone;
1592 $timezone = get_user_timezone_offset($timezone);
1594 if (abs($timezone) > 13) { //server time
1595 $time = mktime((int)$hour, (int)$minute, (int)$second, (int)$month, (int)$day, (int)$year);
1596 } else {
1597 $time = gmmktime((int)$hour, (int)$minute, (int)$second, (int)$month, (int)$day, (int)$year);
1598 $time = usertime($time, $timezone);
1600 //Apply dst for string timezones or if 99 then try dst offset with user's default timezone
1601 if ($applydst && ((99 == $passedtimezone) || !is_numeric($passedtimezone))) {
1602 $time -= dst_offset_on($time, $passedtimezone);
1606 return $time;
1611 * Format a date/time (seconds) as weeks, days, hours etc as needed
1613 * Given an amount of time in seconds, returns string
1614 * formatted nicely as weeks, days, hours etc as needed
1616 * @uses MINSECS
1617 * @uses HOURSECS
1618 * @uses DAYSECS
1619 * @uses YEARSECS
1620 * @param int $totalsecs Time in seconds
1621 * @param object $str Should be a time object
1622 * @return string A nicely formatted date/time string
1624 function format_time($totalsecs, $str=NULL) {
1626 $totalsecs = abs($totalsecs);
1628 if (!$str) { // Create the str structure the slow way
1629 $str = new stdClass();
1630 $str->day = get_string('day');
1631 $str->days = get_string('days');
1632 $str->hour = get_string('hour');
1633 $str->hours = get_string('hours');
1634 $str->min = get_string('min');
1635 $str->mins = get_string('mins');
1636 $str->sec = get_string('sec');
1637 $str->secs = get_string('secs');
1638 $str->year = get_string('year');
1639 $str->years = get_string('years');
1643 $years = floor($totalsecs/YEARSECS);
1644 $remainder = $totalsecs - ($years*YEARSECS);
1645 $days = floor($remainder/DAYSECS);
1646 $remainder = $totalsecs - ($days*DAYSECS);
1647 $hours = floor($remainder/HOURSECS);
1648 $remainder = $remainder - ($hours*HOURSECS);
1649 $mins = floor($remainder/MINSECS);
1650 $secs = $remainder - ($mins*MINSECS);
1652 $ss = ($secs == 1) ? $str->sec : $str->secs;
1653 $sm = ($mins == 1) ? $str->min : $str->mins;
1654 $sh = ($hours == 1) ? $str->hour : $str->hours;
1655 $sd = ($days == 1) ? $str->day : $str->days;
1656 $sy = ($years == 1) ? $str->year : $str->years;
1658 $oyears = '';
1659 $odays = '';
1660 $ohours = '';
1661 $omins = '';
1662 $osecs = '';
1664 if ($years) $oyears = $years .' '. $sy;
1665 if ($days) $odays = $days .' '. $sd;
1666 if ($hours) $ohours = $hours .' '. $sh;
1667 if ($mins) $omins = $mins .' '. $sm;
1668 if ($secs) $osecs = $secs .' '. $ss;
1670 if ($years) return trim($oyears .' '. $odays);
1671 if ($days) return trim($odays .' '. $ohours);
1672 if ($hours) return trim($ohours .' '. $omins);
1673 if ($mins) return trim($omins .' '. $osecs);
1674 if ($secs) return $osecs;
1675 return get_string('now');
1679 * Returns a formatted string that represents a date in user time
1681 * Returns a formatted string that represents a date in user time
1682 * <b>WARNING: note that the format is for strftime(), not date().</b>
1683 * Because of a bug in most Windows time libraries, we can't use
1684 * the nicer %e, so we have to use %d which has leading zeroes.
1685 * A lot of the fuss in the function is just getting rid of these leading
1686 * zeroes as efficiently as possible.
1688 * If parameter fixday = true (default), then take off leading
1689 * zero from %d, else maintain it.
1691 * @param int $date the timestamp in UTC, as obtained from the database.
1692 * @param string $format strftime format. You should probably get this using
1693 * get_string('strftime...', 'langconfig');
1694 * @param mixed $timezone by default, uses the user's time zone. if numeric and
1695 * not 99 then daylight saving will not be added.
1696 * @param bool $fixday If true (default) then the leading zero from %d is removed.
1697 * If false then the leading zero is maintained.
1698 * @return string the formatted date/time.
1700 function userdate($date, $format = '', $timezone = 99, $fixday = true) {
1702 global $CFG;
1704 if (empty($format)) {
1705 $format = get_string('strftimedaydatetime', 'langconfig');
1708 if (!empty($CFG->nofixday)) { // Config.php can force %d not to be fixed.
1709 $fixday = false;
1710 } else if ($fixday) {
1711 $formatnoday = str_replace('%d', 'DD', $format);
1712 $fixday = ($formatnoday != $format);
1715 //add daylight saving offset for string timezones only, as we can't get dst for
1716 //float values. if timezone is 99 (user default timezone), then try update dst.
1717 if ((99 == $timezone) || !is_numeric($timezone)) {
1718 $date += dst_offset_on($date, $timezone);
1721 $timezone = get_user_timezone_offset($timezone);
1723 if (abs($timezone) > 13) { /// Server time
1724 if ($fixday) {
1725 $datestring = strftime($formatnoday, $date);
1726 $daystring = ltrim(str_replace(array(' 0', ' '), '', strftime(' %d', $date)));
1727 $datestring = str_replace('DD', $daystring, $datestring);
1728 } else {
1729 $datestring = strftime($format, $date);
1731 } else {
1732 $date += (int)($timezone * 3600);
1733 if ($fixday) {
1734 $datestring = gmstrftime($formatnoday, $date);
1735 $daystring = ltrim(str_replace(array(' 0', ' '), '', gmstrftime(' %d', $date)));
1736 $datestring = str_replace('DD', $daystring, $datestring);
1737 } else {
1738 $datestring = gmstrftime($format, $date);
1742 /// If we are running under Windows convert from windows encoding to UTF-8
1743 /// (because it's impossible to specify UTF-8 to fetch locale info in Win32)
1745 if ($CFG->ostype == 'WINDOWS') {
1746 if ($localewincharset = get_string('localewincharset', 'langconfig')) {
1747 $textlib = textlib_get_instance();
1748 $datestring = $textlib->convert($datestring, $localewincharset, 'utf-8');
1752 return $datestring;
1756 * Given a $time timestamp in GMT (seconds since epoch),
1757 * returns an array that represents the date in user time
1759 * @todo Finish documenting this function
1760 * @uses HOURSECS
1761 * @param int $time Timestamp in GMT
1762 * @param mixed $timezone offset time with timezone, if float and not 99, then no
1763 * dst offset is applyed
1764 * @return array An array that represents the date in user time
1766 function usergetdate($time, $timezone=99) {
1768 //save input timezone, required for dst offset check.
1769 $passedtimezone = $timezone;
1771 $timezone = get_user_timezone_offset($timezone);
1773 if (abs($timezone) > 13) { // Server time
1774 return getdate($time);
1777 //add daylight saving offset for string timezones only, as we can't get dst for
1778 //float values. if timezone is 99 (user default timezone), then try update dst.
1779 if ($passedtimezone == 99 || !is_numeric($passedtimezone)) {
1780 $time += dst_offset_on($time, $passedtimezone);
1783 $time += intval((float)$timezone * HOURSECS);
1785 $datestring = gmstrftime('%B_%A_%j_%Y_%m_%w_%d_%H_%M_%S', $time);
1787 //be careful to ensure the returned array matches that produced by getdate() above
1788 list(
1789 $getdate['month'],
1790 $getdate['weekday'],
1791 $getdate['yday'],
1792 $getdate['year'],
1793 $getdate['mon'],
1794 $getdate['wday'],
1795 $getdate['mday'],
1796 $getdate['hours'],
1797 $getdate['minutes'],
1798 $getdate['seconds']
1799 ) = explode('_', $datestring);
1801 // set correct datatype to match with getdate()
1802 $getdate['seconds'] = (int)$getdate['seconds'];
1803 $getdate['yday'] = (int)$getdate['yday'] - 1; // gettime returns 0 through 365
1804 $getdate['year'] = (int)$getdate['year'];
1805 $getdate['mon'] = (int)$getdate['mon'];
1806 $getdate['wday'] = (int)$getdate['wday'];
1807 $getdate['mday'] = (int)$getdate['mday'];
1808 $getdate['hours'] = (int)$getdate['hours'];
1809 $getdate['minutes'] = (int)$getdate['minutes'];
1810 return $getdate;
1814 * Given a GMT timestamp (seconds since epoch), offsets it by
1815 * the timezone. eg 3pm in India is 3pm GMT - 7 * 3600 seconds
1817 * @uses HOURSECS
1818 * @param int $date Timestamp in GMT
1819 * @param float $timezone
1820 * @return int
1822 function usertime($date, $timezone=99) {
1824 $timezone = get_user_timezone_offset($timezone);
1826 if (abs($timezone) > 13) {
1827 return $date;
1829 return $date - (int)($timezone * HOURSECS);
1833 * Given a time, return the GMT timestamp of the most recent midnight
1834 * for the current user.
1836 * @param int $date Timestamp in GMT
1837 * @param float $timezone Defaults to user's timezone
1838 * @return int Returns a GMT timestamp
1840 function usergetmidnight($date, $timezone=99) {
1842 $userdate = usergetdate($date, $timezone);
1844 // Time of midnight of this user's day, in GMT
1845 return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone);
1850 * Returns a string that prints the user's timezone
1852 * @param float $timezone The user's timezone
1853 * @return string
1855 function usertimezone($timezone=99) {
1857 $tz = get_user_timezone($timezone);
1859 if (!is_float($tz)) {
1860 return $tz;
1863 if(abs($tz) > 13) { // Server time
1864 return get_string('serverlocaltime');
1867 if($tz == intval($tz)) {
1868 // Don't show .0 for whole hours
1869 $tz = intval($tz);
1872 if($tz == 0) {
1873 return 'UTC';
1875 else if($tz > 0) {
1876 return 'UTC+'.$tz;
1878 else {
1879 return 'UTC'.$tz;
1885 * Returns a float which represents the user's timezone difference from GMT in hours
1886 * Checks various settings and picks the most dominant of those which have a value
1888 * @global object
1889 * @global object
1890 * @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
1891 * @return float
1893 function get_user_timezone_offset($tz = 99) {
1895 global $USER, $CFG;
1897 $tz = get_user_timezone($tz);
1899 if (is_float($tz)) {
1900 return $tz;
1901 } else {
1902 $tzrecord = get_timezone_record($tz);
1903 if (empty($tzrecord)) {
1904 return 99.0;
1906 return (float)$tzrecord->gmtoff / HOURMINS;
1911 * Returns an int which represents the systems's timezone difference from GMT in seconds
1913 * @global object
1914 * @param mixed $tz timezone
1915 * @return int if found, false is timezone 99 or error
1917 function get_timezone_offset($tz) {
1918 global $CFG;
1920 if ($tz == 99) {
1921 return false;
1924 if (is_numeric($tz)) {
1925 return intval($tz * 60*60);
1928 if (!$tzrecord = get_timezone_record($tz)) {
1929 return false;
1931 return intval($tzrecord->gmtoff * 60);
1935 * Returns a float or a string which denotes the user's timezone
1936 * 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)
1937 * means that for this timezone there are also DST rules to be taken into account
1938 * Checks various settings and picks the most dominant of those which have a value
1940 * @global object
1941 * @global object
1942 * @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
1943 * @return mixed
1945 function get_user_timezone($tz = 99) {
1946 global $USER, $CFG;
1948 $timezones = array(
1949 $tz,
1950 isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99,
1951 isset($USER->timezone) ? $USER->timezone : 99,
1952 isset($CFG->timezone) ? $CFG->timezone : 99,
1955 $tz = 99;
1957 while(($tz == '' || $tz == 99 || $tz == NULL) && $next = each($timezones)) {
1958 $tz = $next['value'];
1961 return is_numeric($tz) ? (float) $tz : $tz;
1965 * Returns cached timezone record for given $timezonename
1967 * @global object
1968 * @global object
1969 * @param string $timezonename
1970 * @return mixed timezonerecord object or false
1972 function get_timezone_record($timezonename) {
1973 global $CFG, $DB;
1974 static $cache = NULL;
1976 if ($cache === NULL) {
1977 $cache = array();
1980 if (isset($cache[$timezonename])) {
1981 return $cache[$timezonename];
1984 return $cache[$timezonename] = $DB->get_record_sql('SELECT * FROM {timezone}
1985 WHERE name = ? ORDER BY year DESC', array($timezonename), IGNORE_MULTIPLE);
1989 * Build and store the users Daylight Saving Time (DST) table
1991 * @global object
1992 * @global object
1993 * @global object
1994 * @param mixed $from_year Start year for the table, defaults to 1971
1995 * @param mixed $to_year End year for the table, defaults to 2035
1996 * @param mixed $strtimezone, if null or 99 then user's default timezone is used
1997 * @return bool
1999 function calculate_user_dst_table($from_year = NULL, $to_year = NULL, $strtimezone = NULL) {
2000 global $CFG, $SESSION, $DB;
2002 $usertz = get_user_timezone($strtimezone);
2004 if (is_float($usertz)) {
2005 // Trivial timezone, no DST
2006 return false;
2009 if (!empty($SESSION->dst_offsettz) && $SESSION->dst_offsettz != $usertz) {
2010 // We have precalculated values, but the user's effective TZ has changed in the meantime, so reset
2011 unset($SESSION->dst_offsets);
2012 unset($SESSION->dst_range);
2015 if (!empty($SESSION->dst_offsets) && empty($from_year) && empty($to_year)) {
2016 // Repeat calls which do not request specific year ranges stop here, we have already calculated the table
2017 // This will be the return path most of the time, pretty light computationally
2018 return true;
2021 // Reaching here means we either need to extend our table or create it from scratch
2023 // Remember which TZ we calculated these changes for
2024 $SESSION->dst_offsettz = $usertz;
2026 if(empty($SESSION->dst_offsets)) {
2027 // If we 're creating from scratch, put the two guard elements in there
2028 $SESSION->dst_offsets = array(1 => NULL, 0 => NULL);
2030 if(empty($SESSION->dst_range)) {
2031 // If creating from scratch
2032 $from = max((empty($from_year) ? intval(date('Y')) - 3 : $from_year), 1971);
2033 $to = min((empty($to_year) ? intval(date('Y')) + 3 : $to_year), 2035);
2035 // Fill in the array with the extra years we need to process
2036 $yearstoprocess = array();
2037 for($i = $from; $i <= $to; ++$i) {
2038 $yearstoprocess[] = $i;
2041 // Take note of which years we have processed for future calls
2042 $SESSION->dst_range = array($from, $to);
2044 else {
2045 // If needing to extend the table, do the same
2046 $yearstoprocess = array();
2048 $from = max((empty($from_year) ? $SESSION->dst_range[0] : $from_year), 1971);
2049 $to = min((empty($to_year) ? $SESSION->dst_range[1] : $to_year), 2035);
2051 if($from < $SESSION->dst_range[0]) {
2052 // Take note of which years we need to process and then note that we have processed them for future calls
2053 for($i = $from; $i < $SESSION->dst_range[0]; ++$i) {
2054 $yearstoprocess[] = $i;
2056 $SESSION->dst_range[0] = $from;
2058 if($to > $SESSION->dst_range[1]) {
2059 // Take note of which years we need to process and then note that we have processed them for future calls
2060 for($i = $SESSION->dst_range[1] + 1; $i <= $to; ++$i) {
2061 $yearstoprocess[] = $i;
2063 $SESSION->dst_range[1] = $to;
2067 if(empty($yearstoprocess)) {
2068 // This means that there was a call requesting a SMALLER range than we have already calculated
2069 return true;
2072 // From now on, we know that the array has at least the two guard elements, and $yearstoprocess has the years we need
2073 // Also, the array is sorted in descending timestamp order!
2075 // Get DB data
2077 static $presets_cache = array();
2078 if (!isset($presets_cache[$usertz])) {
2079 $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');
2081 if(empty($presets_cache[$usertz])) {
2082 return false;
2085 // Remove ending guard (first element of the array)
2086 reset($SESSION->dst_offsets);
2087 unset($SESSION->dst_offsets[key($SESSION->dst_offsets)]);
2089 // Add all required change timestamps
2090 foreach($yearstoprocess as $y) {
2091 // Find the record which is in effect for the year $y
2092 foreach($presets_cache[$usertz] as $year => $preset) {
2093 if($year <= $y) {
2094 break;
2098 $changes = dst_changes_for_year($y, $preset);
2100 if($changes === NULL) {
2101 continue;
2103 if($changes['dst'] != 0) {
2104 $SESSION->dst_offsets[$changes['dst']] = $preset->dstoff * MINSECS;
2106 if($changes['std'] != 0) {
2107 $SESSION->dst_offsets[$changes['std']] = 0;
2111 // Put in a guard element at the top
2112 $maxtimestamp = max(array_keys($SESSION->dst_offsets));
2113 $SESSION->dst_offsets[($maxtimestamp + DAYSECS)] = NULL; // DAYSECS is arbitrary, any "small" number will do
2115 // Sort again
2116 krsort($SESSION->dst_offsets);
2118 return true;
2122 * Calculates the required DST change and returns a Timestamp Array
2124 * @uses HOURSECS
2125 * @uses MINSECS
2126 * @param mixed $year Int or String Year to focus on
2127 * @param object $timezone Instatiated Timezone object
2128 * @return mixed Null, or Array dst=>xx, 0=>xx, std=>yy, 1=>yy
2130 function dst_changes_for_year($year, $timezone) {
2132 if($timezone->dst_startday == 0 && $timezone->dst_weekday == 0 && $timezone->std_startday == 0 && $timezone->std_weekday == 0) {
2133 return NULL;
2136 $monthdaydst = find_day_in_month($timezone->dst_startday, $timezone->dst_weekday, $timezone->dst_month, $year);
2137 $monthdaystd = find_day_in_month($timezone->std_startday, $timezone->std_weekday, $timezone->std_month, $year);
2139 list($dst_hour, $dst_min) = explode(':', $timezone->dst_time);
2140 list($std_hour, $std_min) = explode(':', $timezone->std_time);
2142 $timedst = make_timestamp($year, $timezone->dst_month, $monthdaydst, 0, 0, 0, 99, false);
2143 $timestd = make_timestamp($year, $timezone->std_month, $monthdaystd, 0, 0, 0, 99, false);
2145 // Instead of putting hour and minute in make_timestamp(), we add them afterwards.
2146 // This has the advantage of being able to have negative values for hour, i.e. for timezones
2147 // where GMT time would be in the PREVIOUS day than the local one on which DST changes.
2149 $timedst += $dst_hour * HOURSECS + $dst_min * MINSECS;
2150 $timestd += $std_hour * HOURSECS + $std_min * MINSECS;
2152 return array('dst' => $timedst, 0 => $timedst, 'std' => $timestd, 1 => $timestd);
2156 * Calculates the Daylight Saving Offset for a given date/time (timestamp)
2157 * - Note: Daylight saving only works for string timezones and not for float.
2159 * @global object
2160 * @param int $time must NOT be compensated at all, it has to be a pure timestamp
2161 * @param mixed $strtimezone timezone for which offset is expected, if 99 or null
2162 * then user's default timezone is used.
2163 * @return int
2165 function dst_offset_on($time, $strtimezone = NULL) {
2166 global $SESSION;
2168 if(!calculate_user_dst_table(NULL, NULL, $strtimezone) || empty($SESSION->dst_offsets)) {
2169 return 0;
2172 reset($SESSION->dst_offsets);
2173 while(list($from, $offset) = each($SESSION->dst_offsets)) {
2174 if($from <= $time) {
2175 break;
2179 // This is the normal return path
2180 if($offset !== NULL) {
2181 return $offset;
2184 // Reaching this point means we haven't calculated far enough, do it now:
2185 // Calculate extra DST changes if needed and recurse. The recursion always
2186 // moves toward the stopping condition, so will always end.
2188 if($from == 0) {
2189 // We need a year smaller than $SESSION->dst_range[0]
2190 if($SESSION->dst_range[0] == 1971) {
2191 return 0;
2193 calculate_user_dst_table($SESSION->dst_range[0] - 5, NULL, $strtimezone);
2194 return dst_offset_on($time, $strtimezone);
2196 else {
2197 // We need a year larger than $SESSION->dst_range[1]
2198 if($SESSION->dst_range[1] == 2035) {
2199 return 0;
2201 calculate_user_dst_table(NULL, $SESSION->dst_range[1] + 5, $strtimezone);
2202 return dst_offset_on($time, $strtimezone);
2209 * @todo Document what this function does
2210 * @param int $startday
2211 * @param int $weekday
2212 * @param int $month
2213 * @param int $year
2214 * @return int
2216 function find_day_in_month($startday, $weekday, $month, $year) {
2218 $daysinmonth = days_in_month($month, $year);
2220 if($weekday == -1) {
2221 // Don't care about weekday, so return:
2222 // abs($startday) if $startday != -1
2223 // $daysinmonth otherwise
2224 return ($startday == -1) ? $daysinmonth : abs($startday);
2227 // From now on we 're looking for a specific weekday
2229 // Give "end of month" its actual value, since we know it
2230 if($startday == -1) {
2231 $startday = -1 * $daysinmonth;
2234 // Starting from day $startday, the sign is the direction
2236 if($startday < 1) {
2238 $startday = abs($startday);
2239 $lastmonthweekday = strftime('%w', mktime(12, 0, 0, $month, $daysinmonth, $year));
2241 // This is the last such weekday of the month
2242 $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday;
2243 if($lastinmonth > $daysinmonth) {
2244 $lastinmonth -= 7;
2247 // Find the first such weekday <= $startday
2248 while($lastinmonth > $startday) {
2249 $lastinmonth -= 7;
2252 return $lastinmonth;
2255 else {
2257 $indexweekday = strftime('%w', mktime(12, 0, 0, $month, $startday, $year));
2259 $diff = $weekday - $indexweekday;
2260 if($diff < 0) {
2261 $diff += 7;
2264 // This is the first such weekday of the month equal to or after $startday
2265 $firstfromindex = $startday + $diff;
2267 return $firstfromindex;
2273 * Calculate the number of days in a given month
2275 * @param int $month The month whose day count is sought
2276 * @param int $year The year of the month whose day count is sought
2277 * @return int
2279 function days_in_month($month, $year) {
2280 return intval(date('t', mktime(12, 0, 0, $month, 1, $year)));
2284 * Calculate the position in the week of a specific calendar day
2286 * @param int $day The day of the date whose position in the week is sought
2287 * @param int $month The month of the date whose position in the week is sought
2288 * @param int $year The year of the date whose position in the week is sought
2289 * @return int
2291 function dayofweek($day, $month, $year) {
2292 // I wonder if this is any different from
2293 // strftime('%w', mktime(12, 0, 0, $month, $daysinmonth, $year, 0));
2294 return intval(date('w', mktime(12, 0, 0, $month, $day, $year)));
2297 /// USER AUTHENTICATION AND LOGIN ////////////////////////////////////////
2300 * Returns full login url.
2302 * @return string login url
2304 function get_login_url() {
2305 global $CFG;
2307 $url = "$CFG->wwwroot/login/index.php";
2309 if (!empty($CFG->loginhttps)) {
2310 $url = str_replace('http:', 'https:', $url);
2313 return $url;
2317 * This function checks that the current user is logged in and has the
2318 * required privileges
2320 * This function checks that the current user is logged in, and optionally
2321 * whether they are allowed to be in a particular course and view a particular
2322 * course module.
2323 * If they are not logged in, then it redirects them to the site login unless
2324 * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which
2325 * case they are automatically logged in as guests.
2326 * If $courseid is given and the user is not enrolled in that course then the
2327 * user is redirected to the course enrolment page.
2328 * If $cm is given and the course module is hidden and the user is not a teacher
2329 * in the course then the user is redirected to the course home page.
2331 * When $cm parameter specified, this function sets page layout to 'module'.
2332 * You need to change it manually later if some other layout needed.
2334 * @param mixed $courseorid id of the course or course object
2335 * @param bool $autologinguest default true
2336 * @param object $cm course module object
2337 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2338 * true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2339 * in order to keep redirects working properly. MDL-14495
2340 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2341 * @return mixed Void, exit, and die depending on path
2343 function require_login($courseorid = NULL, $autologinguest = true, $cm = NULL, $setwantsurltome = true, $preventredirect = false) {
2344 global $CFG, $SESSION, $USER, $FULLME, $PAGE, $SITE, $DB, $OUTPUT;
2346 // setup global $COURSE, themes, language and locale
2347 if (!empty($courseorid)) {
2348 if (is_object($courseorid)) {
2349 $course = $courseorid;
2350 } else if ($courseorid == SITEID) {
2351 $course = clone($SITE);
2352 } else {
2353 $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST);
2355 if ($cm) {
2356 if ($cm->course != $course->id) {
2357 throw new coding_exception('course and cm parameters in require_login() call do not match!!');
2359 // make sure we have a $cm from get_fast_modinfo as this contains activity access details
2360 if (!($cm instanceof cm_info)) {
2361 // note: nearly all pages call get_fast_modinfo anyway and it does not make any
2362 // db queries so this is not really a performance concern, however it is obviously
2363 // better if you use get_fast_modinfo to get the cm before calling this.
2364 $modinfo = get_fast_modinfo($course);
2365 $cm = $modinfo->get_cm($cm->id);
2367 $PAGE->set_cm($cm, $course); // set's up global $COURSE
2368 $PAGE->set_pagelayout('incourse');
2369 } else {
2370 $PAGE->set_course($course); // set's up global $COURSE
2372 } else {
2373 // do not touch global $COURSE via $PAGE->set_course(),
2374 // the reasons is we need to be able to call require_login() at any time!!
2375 $course = $SITE;
2376 if ($cm) {
2377 throw new coding_exception('cm parameter in require_login() requires valid course parameter!');
2381 // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false.
2382 // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future
2383 // risk leading the user back to the AJAX request URL.
2384 if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) {
2385 $setwantsurltome = false;
2388 // If the user is not even logged in yet then make sure they are
2389 if (!isloggedin()) {
2390 if ($autologinguest and !empty($CFG->guestloginbutton) and !empty($CFG->autologinguests)) {
2391 if (!$guest = get_complete_user_data('id', $CFG->siteguest)) {
2392 // misconfigured site guest, just redirect to login page
2393 redirect(get_login_url());
2394 exit; // never reached
2396 $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang;
2397 complete_user_login($guest, false);
2398 $USER->autologinguest = true;
2399 $SESSION->lang = $lang;
2400 } else {
2401 //NOTE: $USER->site check was obsoleted by session test cookie,
2402 // $USER->confirmed test is in login/index.php
2403 if ($preventredirect) {
2404 throw new require_login_exception('You are not logged in');
2407 if ($setwantsurltome) {
2408 // TODO: switch to PAGE->url
2409 $SESSION->wantsurl = $FULLME;
2411 if (!empty($_SERVER['HTTP_REFERER'])) {
2412 $SESSION->fromurl = $_SERVER['HTTP_REFERER'];
2414 redirect(get_login_url());
2415 exit; // never reached
2419 // loginas as redirection if needed
2420 if ($course->id != SITEID and session_is_loggedinas()) {
2421 if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) {
2422 if ($USER->loginascontext->instanceid != $course->id) {
2423 print_error('loginasonecourse', '', $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid);
2428 // check whether the user should be changing password (but only if it is REALLY them)
2429 if (get_user_preferences('auth_forcepasswordchange') && !session_is_loggedinas()) {
2430 $userauth = get_auth_plugin($USER->auth);
2431 if ($userauth->can_change_password() and !$preventredirect) {
2432 if ($setwantsurltome) {
2433 $SESSION->wantsurl = $FULLME;
2435 if ($changeurl = $userauth->change_password_url()) {
2436 //use plugin custom url
2437 redirect($changeurl);
2438 } else {
2439 //use moodle internal method
2440 if (empty($CFG->loginhttps)) {
2441 redirect($CFG->wwwroot .'/login/change_password.php');
2442 } else {
2443 $wwwroot = str_replace('http:','https:', $CFG->wwwroot);
2444 redirect($wwwroot .'/login/change_password.php');
2447 } else {
2448 print_error('nopasswordchangeforced', 'auth');
2452 // Check that the user account is properly set up
2453 if (user_not_fully_set_up($USER)) {
2454 if ($preventredirect) {
2455 throw new require_login_exception('User not fully set-up');
2457 if ($setwantsurltome) {
2458 $SESSION->wantsurl = $FULLME;
2460 redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&amp;course='. SITEID);
2463 // Make sure the USER has a sesskey set up. Used for CSRF protection.
2464 sesskey();
2466 // Do not bother admins with any formalities
2467 if (is_siteadmin()) {
2468 //set accesstime or the user will appear offline which messes up messaging
2469 user_accesstime_log($course->id);
2470 return;
2473 // Check that the user has agreed to a site policy if there is one - do not test in case of admins
2474 if (!$USER->policyagreed and !is_siteadmin()) {
2475 if (!empty($CFG->sitepolicy) and !isguestuser()) {
2476 if ($preventredirect) {
2477 throw new require_login_exception('Policy not agreed');
2479 if ($setwantsurltome) {
2480 $SESSION->wantsurl = $FULLME;
2482 redirect($CFG->wwwroot .'/user/policy.php');
2483 } else if (!empty($CFG->sitepolicyguest) and isguestuser()) {
2484 if ($preventredirect) {
2485 throw new require_login_exception('Policy not agreed');
2487 if ($setwantsurltome) {
2488 $SESSION->wantsurl = $FULLME;
2490 redirect($CFG->wwwroot .'/user/policy.php');
2494 // Fetch the system context, the course context, and prefetch its child contexts
2495 $sysctx = get_context_instance(CONTEXT_SYSTEM);
2496 $coursecontext = get_context_instance(CONTEXT_COURSE, $course->id, MUST_EXIST);
2497 if ($cm) {
2498 $cmcontext = get_context_instance(CONTEXT_MODULE, $cm->id, MUST_EXIST);
2499 } else {
2500 $cmcontext = null;
2503 // If the site is currently under maintenance, then print a message
2504 if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:config', $sysctx)) {
2505 if ($preventredirect) {
2506 throw new require_login_exception('Maintenance in progress');
2509 print_maintenance_message();
2512 // make sure the course itself is not hidden
2513 if ($course->id == SITEID) {
2514 // frontpage can not be hidden
2515 } else {
2516 if (is_role_switched($course->id)) {
2517 // when switching roles ignore the hidden flag - user had to be in course to do the switch
2518 } else {
2519 if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
2520 // originally there was also test of parent category visibility,
2521 // BUT is was very slow in complex queries involving "my courses"
2522 // now it is also possible to simply hide all courses user is not enrolled in :-)
2523 if ($preventredirect) {
2524 throw new require_login_exception('Course is hidden');
2526 // We need to override the navigation URL as the course won't have
2527 // been added to the navigation and thus the navigation will mess up
2528 // when trying to find it.
2529 navigation_node::override_active_url(new moodle_url('/'));
2530 notice(get_string('coursehidden'), $CFG->wwwroot .'/');
2535 // is the user enrolled?
2536 if ($course->id == SITEID) {
2537 // everybody is enrolled on the frontpage
2539 } else {
2540 if (session_is_loggedinas()) {
2541 // Make sure the REAL person can access this course first
2542 $realuser = session_get_realuser();
2543 if (!is_enrolled($coursecontext, $realuser->id, '', true) and !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) {
2544 if ($preventredirect) {
2545 throw new require_login_exception('Invalid course login-as access');
2547 echo $OUTPUT->header();
2548 notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/');
2552 // very simple enrolment caching - changes in course setting are not reflected immediately
2553 if (!isset($USER->enrol)) {
2554 $USER->enrol = array();
2555 $USER->enrol['enrolled'] = array();
2556 $USER->enrol['tempguest'] = array();
2559 $access = false;
2561 if (is_role_switched($course->id)) {
2562 // ok, user had to be inside this course before the switch
2563 $access = true;
2565 } else if (is_viewing($coursecontext, $USER)) {
2566 // ok, no need to mess with enrol
2567 $access = true;
2569 } else {
2570 if (isset($USER->enrol['enrolled'][$course->id])) {
2571 if ($USER->enrol['enrolled'][$course->id] == 0) {
2572 $access = true;
2573 } else if ($USER->enrol['enrolled'][$course->id] > time()) {
2574 $access = true;
2575 } else {
2576 //expired
2577 unset($USER->enrol['enrolled'][$course->id]);
2580 if (isset($USER->enrol['tempguest'][$course->id])) {
2581 if ($USER->enrol['tempguest'][$course->id] == 0) {
2582 $access = true;
2583 } else if ($USER->enrol['tempguest'][$course->id] > time()) {
2584 $access = true;
2585 } else {
2586 //expired
2587 unset($USER->enrol['tempguest'][$course->id]);
2588 $USER->access = remove_temp_roles($coursecontext, $USER->access);
2592 if ($access) {
2593 // cache ok
2594 } else if (is_enrolled($coursecontext, $USER, '', true)) {
2595 // active participants may always access
2596 // TODO: refactor this into some new function
2597 $now = time();
2598 $sql = "SELECT MAX(ue.timeend)
2599 FROM {user_enrolments} ue
2600 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
2601 JOIN {user} u ON u.id = ue.userid
2602 WHERE ue.userid = :userid AND ue.status = :active AND e.status = :enabled AND u.deleted = 0
2603 AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)";
2604 $params = array('enabled'=>ENROL_INSTANCE_ENABLED, 'active'=>ENROL_USER_ACTIVE,
2605 'userid'=>$USER->id, 'courseid'=>$coursecontext->instanceid, 'now1'=>$now, 'now2'=>$now);
2606 $until = $DB->get_field_sql($sql, $params);
2607 if (!$until or $until > time() + ENROL_REQUIRE_LOGIN_CACHE_PERIOD) {
2608 $until = time() + ENROL_REQUIRE_LOGIN_CACHE_PERIOD;
2611 $USER->enrol['enrolled'][$course->id] = $until;
2612 $access = true;
2614 // remove traces of previous temp guest access
2615 $USER->access = remove_temp_roles($coursecontext, $USER->access);
2617 } else {
2618 $instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder, id ASC');
2619 $enrols = enrol_get_plugins(true);
2620 // first ask all enabled enrol instances in course if they want to auto enrol user
2621 foreach($instances as $instance) {
2622 if (!isset($enrols[$instance->enrol])) {
2623 continue;
2625 // Get a duration for the guestaccess, a timestamp in the future or false.
2626 $until = $enrols[$instance->enrol]->try_autoenrol($instance);
2627 if ($until !== false) {
2628 $USER->enrol['enrolled'][$course->id] = $until;
2629 $USER->access = remove_temp_roles($coursecontext, $USER->access);
2630 $access = true;
2631 break;
2634 // if not enrolled yet try to gain temporary guest access
2635 if (!$access) {
2636 foreach($instances as $instance) {
2637 if (!isset($enrols[$instance->enrol])) {
2638 continue;
2640 // Get a duration for the guestaccess, a timestamp in the future or false.
2641 $until = $enrols[$instance->enrol]->try_guestaccess($instance);
2642 if ($until !== false) {
2643 $USER->enrol['tempguest'][$course->id] = $until;
2644 $access = true;
2645 break;
2652 if (!$access) {
2653 if ($preventredirect) {
2654 throw new require_login_exception('Not enrolled');
2656 if ($setwantsurltome) {
2657 $SESSION->wantsurl = $FULLME;
2659 redirect($CFG->wwwroot .'/enrol/index.php?id='. $course->id);
2663 // Check visibility of activity to current user; includes visible flag, groupmembersonly,
2664 // conditional availability, etc
2665 if ($cm && !$cm->uservisible) {
2666 if ($preventredirect) {
2667 throw new require_login_exception('Activity is hidden');
2669 redirect($CFG->wwwroot, get_string('activityiscurrentlyhidden'));
2672 // Finally access granted, update lastaccess times
2673 user_accesstime_log($course->id);
2678 * This function just makes sure a user is logged out.
2680 * @global object
2682 function require_logout() {
2683 global $USER;
2685 $params = $USER;
2687 if (isloggedin()) {
2688 add_to_log(SITEID, "user", "logout", "view.php?id=$USER->id&course=".SITEID, $USER->id, 0, $USER->id);
2690 $authsequence = get_enabled_auth_plugins(); // auths, in sequence
2691 foreach($authsequence as $authname) {
2692 $authplugin = get_auth_plugin($authname);
2693 $authplugin->prelogout_hook();
2697 events_trigger('user_logout', $params);
2698 session_get_instance()->terminate_current();
2699 unset($params);
2703 * Weaker version of require_login()
2705 * This is a weaker version of {@link require_login()} which only requires login
2706 * when called from within a course rather than the site page, unless
2707 * the forcelogin option is turned on.
2708 * @see require_login()
2710 * @global object
2711 * @param mixed $courseorid The course object or id in question
2712 * @param bool $autologinguest Allow autologin guests if that is wanted
2713 * @param object $cm Course activity module if known
2714 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2715 * true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2716 * in order to keep redirects working properly. MDL-14495
2717 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2718 * @return void
2720 function require_course_login($courseorid, $autologinguest = true, $cm = NULL, $setwantsurltome = true, $preventredirect = false) {
2721 global $CFG, $PAGE, $SITE;
2722 $issite = (is_object($courseorid) and $courseorid->id == SITEID)
2723 or (!is_object($courseorid) and $courseorid == SITEID);
2724 if ($issite && !empty($cm) && !($cm instanceof cm_info)) {
2725 // note: nearly all pages call get_fast_modinfo anyway and it does not make any
2726 // db queries so this is not really a performance concern, however it is obviously
2727 // better if you use get_fast_modinfo to get the cm before calling this.
2728 if (is_object($courseorid)) {
2729 $course = $courseorid;
2730 } else {
2731 $course = clone($SITE);
2733 $modinfo = get_fast_modinfo($course);
2734 $cm = $modinfo->get_cm($cm->id);
2736 if (!empty($CFG->forcelogin)) {
2737 // login required for both SITE and courses
2738 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2740 } else if ($issite && !empty($cm) and !$cm->uservisible) {
2741 // always login for hidden activities
2742 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2744 } else if ($issite) {
2745 //login for SITE not required
2746 if ($cm and empty($cm->visible)) {
2747 // hidden activities are not accessible without login
2748 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2749 } else if ($cm and !empty($CFG->enablegroupmembersonly) and $cm->groupmembersonly) {
2750 // not-logged-in users do not have any group membership
2751 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2752 } else {
2753 // We still need to instatiate PAGE vars properly so that things
2754 // that rely on it like navigation function correctly.
2755 if (!empty($courseorid)) {
2756 if (is_object($courseorid)) {
2757 $course = $courseorid;
2758 } else {
2759 $course = clone($SITE);
2761 if ($cm) {
2762 if ($cm->course != $course->id) {
2763 throw new coding_exception('course and cm parameters in require_course_login() call do not match!!');
2765 $PAGE->set_cm($cm, $course);
2766 $PAGE->set_pagelayout('incourse');
2767 } else {
2768 $PAGE->set_course($course);
2770 } else {
2771 // If $PAGE->course, and hence $PAGE->context, have not already been set
2772 // up properly, set them up now.
2773 $PAGE->set_course($PAGE->course);
2775 //TODO: verify conditional activities here
2776 user_accesstime_log(SITEID);
2777 return;
2780 } else {
2781 // course login always required
2782 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2787 * Require key login. Function terminates with error if key not found or incorrect.
2789 * @global object
2790 * @global object
2791 * @global object
2792 * @global object
2793 * @uses NO_MOODLE_COOKIES
2794 * @uses PARAM_ALPHANUM
2795 * @param string $script unique script identifier
2796 * @param int $instance optional instance id
2797 * @return int Instance ID
2799 function require_user_key_login($script, $instance=null) {
2800 global $USER, $SESSION, $CFG, $DB;
2802 if (!NO_MOODLE_COOKIES) {
2803 print_error('sessioncookiesdisable');
2806 /// extra safety
2807 @session_write_close();
2809 $keyvalue = required_param('key', PARAM_ALPHANUM);
2811 if (!$key = $DB->get_record('user_private_key', array('script'=>$script, 'value'=>$keyvalue, 'instance'=>$instance))) {
2812 print_error('invalidkey');
2815 if (!empty($key->validuntil) and $key->validuntil < time()) {
2816 print_error('expiredkey');
2819 if ($key->iprestriction) {
2820 $remoteaddr = getremoteaddr(null);
2821 if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) {
2822 print_error('ipmismatch');
2826 if (!$user = $DB->get_record('user', array('id'=>$key->userid))) {
2827 print_error('invaliduserid');
2830 /// emulate normal session
2831 session_set_user($user);
2833 /// note we are not using normal login
2834 if (!defined('USER_KEY_LOGIN')) {
2835 define('USER_KEY_LOGIN', true);
2838 /// return instance id - it might be empty
2839 return $key->instance;
2843 * Creates a new private user access key.
2845 * @global object
2846 * @param string $script unique target identifier
2847 * @param int $userid
2848 * @param int $instance optional instance id
2849 * @param string $iprestriction optional ip restricted access
2850 * @param timestamp $validuntil key valid only until given data
2851 * @return string access key value
2853 function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
2854 global $DB;
2856 $key = new stdClass();
2857 $key->script = $script;
2858 $key->userid = $userid;
2859 $key->instance = $instance;
2860 $key->iprestriction = $iprestriction;
2861 $key->validuntil = $validuntil;
2862 $key->timecreated = time();
2864 $key->value = md5($userid.'_'.time().random_string(40)); // something long and unique
2865 while ($DB->record_exists('user_private_key', array('value'=>$key->value))) {
2866 // must be unique
2867 $key->value = md5($userid.'_'.time().random_string(40));
2869 $DB->insert_record('user_private_key', $key);
2870 return $key->value;
2874 * Delete the user's new private user access keys for a particular script.
2876 * @global object
2877 * @param string $script unique target identifier
2878 * @param int $userid
2879 * @return void
2881 function delete_user_key($script,$userid) {
2882 global $DB;
2883 $DB->delete_records('user_private_key', array('script'=>$script, 'userid'=>$userid));
2887 * Gets a private user access key (and creates one if one doesn't exist).
2889 * @global object
2890 * @param string $script unique target identifier
2891 * @param int $userid
2892 * @param int $instance optional instance id
2893 * @param string $iprestriction optional ip restricted access
2894 * @param timestamp $validuntil key valid only until given data
2895 * @return string access key value
2897 function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
2898 global $DB;
2900 if ($key = $DB->get_record('user_private_key', array('script'=>$script, 'userid'=>$userid,
2901 'instance'=>$instance, 'iprestriction'=>$iprestriction,
2902 'validuntil'=>$validuntil))) {
2903 return $key->value;
2904 } else {
2905 return create_user_key($script, $userid, $instance, $iprestriction, $validuntil);
2911 * Modify the user table by setting the currently logged in user's
2912 * last login to now.
2914 * @global object
2915 * @global object
2916 * @return bool Always returns true
2918 function update_user_login_times() {
2919 global $USER, $DB;
2921 $user = new stdClass();
2922 $USER->lastlogin = $user->lastlogin = $USER->currentlogin;
2923 $USER->currentlogin = $user->lastaccess = $user->currentlogin = time();
2925 $user->id = $USER->id;
2927 $DB->update_record('user', $user);
2928 return true;
2932 * Determines if a user has completed setting up their account.
2934 * @param user $user A {@link $USER} object to test for the existence of a valid name and email
2935 * @return bool
2937 function user_not_fully_set_up($user) {
2938 if (isguestuser($user)) {
2939 return false;
2941 return (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user));
2945 * Check whether the user has exceeded the bounce threshold
2947 * @global object
2948 * @global object
2949 * @param user $user A {@link $USER} object
2950 * @return bool true=>User has exceeded bounce threshold
2952 function over_bounce_threshold($user) {
2953 global $CFG, $DB;
2955 if (empty($CFG->handlebounces)) {
2956 return false;
2959 if (empty($user->id)) { /// No real (DB) user, nothing to do here.
2960 return false;
2963 // set sensible defaults
2964 if (empty($CFG->minbounces)) {
2965 $CFG->minbounces = 10;
2967 if (empty($CFG->bounceratio)) {
2968 $CFG->bounceratio = .20;
2970 $bouncecount = 0;
2971 $sendcount = 0;
2972 if ($bounce = $DB->get_record('user_preferences', array ('userid'=>$user->id, 'name'=>'email_bounce_count'))) {
2973 $bouncecount = $bounce->value;
2975 if ($send = $DB->get_record('user_preferences', array('userid'=>$user->id, 'name'=>'email_send_count'))) {
2976 $sendcount = $send->value;
2978 return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio);
2982 * Used to increment or reset email sent count
2984 * @global object
2985 * @param user $user object containing an id
2986 * @param bool $reset will reset the count to 0
2987 * @return void
2989 function set_send_count($user,$reset=false) {
2990 global $DB;
2992 if (empty($user->id)) { /// No real (DB) user, nothing to do here.
2993 return;
2996 if ($pref = $DB->get_record('user_preferences', array('userid'=>$user->id, 'name'=>'email_send_count'))) {
2997 $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
2998 $DB->update_record('user_preferences', $pref);
3000 else if (!empty($reset)) { // if it's not there and we're resetting, don't bother.
3001 // make a new one
3002 $pref = new stdClass();
3003 $pref->name = 'email_send_count';
3004 $pref->value = 1;
3005 $pref->userid = $user->id;
3006 $DB->insert_record('user_preferences', $pref, false);
3011 * Increment or reset user's email bounce count
3013 * @global object
3014 * @param user $user object containing an id
3015 * @param bool $reset will reset the count to 0
3017 function set_bounce_count($user,$reset=false) {
3018 global $DB;
3020 if ($pref = $DB->get_record('user_preferences', array('userid'=>$user->id, 'name'=>'email_bounce_count'))) {
3021 $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3022 $DB->update_record('user_preferences', $pref);
3024 else if (!empty($reset)) { // if it's not there and we're resetting, don't bother.
3025 // make a new one
3026 $pref = new stdClass();
3027 $pref->name = 'email_bounce_count';
3028 $pref->value = 1;
3029 $pref->userid = $user->id;
3030 $DB->insert_record('user_preferences', $pref, false);
3035 * Keeps track of login attempts
3037 * @global object
3039 function update_login_count() {
3040 global $SESSION;
3042 $max_logins = 10;
3044 if (empty($SESSION->logincount)) {
3045 $SESSION->logincount = 1;
3046 } else {
3047 $SESSION->logincount++;
3050 if ($SESSION->logincount > $max_logins) {
3051 unset($SESSION->wantsurl);
3052 print_error('errortoomanylogins');
3057 * Resets login attempts
3059 * @global object
3061 function reset_login_count() {
3062 global $SESSION;
3064 $SESSION->logincount = 0;
3068 * Determines if the currently logged in user is in editing mode.
3069 * Note: originally this function had $userid parameter - it was not usable anyway
3071 * @deprecated since Moodle 2.0 - use $PAGE->user_is_editing() instead.
3072 * @todo Deprecated function remove when ready
3074 * @global object
3075 * @uses DEBUG_DEVELOPER
3076 * @return bool
3078 function isediting() {
3079 global $PAGE;
3080 debugging('call to deprecated function isediting(). Please use $PAGE->user_is_editing() instead', DEBUG_DEVELOPER);
3081 return $PAGE->user_is_editing();
3085 * Determines if the logged in user is currently moving an activity
3087 * @global object
3088 * @param int $courseid The id of the course being tested
3089 * @return bool
3091 function ismoving($courseid) {
3092 global $USER;
3094 if (!empty($USER->activitycopy)) {
3095 return ($USER->activitycopycourse == $courseid);
3097 return false;
3101 * Returns a persons full name
3103 * Given an object containing firstname and lastname
3104 * values, this function returns a string with the
3105 * full name of the person.
3106 * The result may depend on system settings
3107 * or language. 'override' will force both names
3108 * to be used even if system settings specify one.
3110 * @global object
3111 * @global object
3112 * @param object $user A {@link $USER} object to get full name of
3113 * @param bool $override If true then the name will be first name followed by last name rather than adhering to fullnamedisplay setting.
3114 * @return string
3116 function fullname($user, $override=false) {
3117 global $CFG, $SESSION;
3119 if (!isset($user->firstname) and !isset($user->lastname)) {
3120 return '';
3123 if (!$override) {
3124 if (!empty($CFG->forcefirstname)) {
3125 $user->firstname = $CFG->forcefirstname;
3127 if (!empty($CFG->forcelastname)) {
3128 $user->lastname = $CFG->forcelastname;
3132 if (!empty($SESSION->fullnamedisplay)) {
3133 $CFG->fullnamedisplay = $SESSION->fullnamedisplay;
3136 if (!isset($CFG->fullnamedisplay) or $CFG->fullnamedisplay === 'firstname lastname') {
3137 return $user->firstname .' '. $user->lastname;
3139 } else if ($CFG->fullnamedisplay == 'lastname firstname') {
3140 return $user->lastname .' '. $user->firstname;
3142 } else if ($CFG->fullnamedisplay == 'firstname') {
3143 if ($override) {
3144 return get_string('fullnamedisplay', '', $user);
3145 } else {
3146 return $user->firstname;
3150 return get_string('fullnamedisplay', '', $user);
3154 * Returns whether a given authentication plugin exists.
3156 * @global object
3157 * @param string $auth Form of authentication to check for. Defaults to the
3158 * global setting in {@link $CFG}.
3159 * @return boolean Whether the plugin is available.
3161 function exists_auth_plugin($auth) {
3162 global $CFG;
3164 if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) {
3165 return is_readable("{$CFG->dirroot}/auth/$auth/auth.php");
3167 return false;
3171 * Checks if a given plugin is in the list of enabled authentication plugins.
3173 * @param string $auth Authentication plugin.
3174 * @return boolean Whether the plugin is enabled.
3176 function is_enabled_auth($auth) {
3177 if (empty($auth)) {
3178 return false;
3181 $enabled = get_enabled_auth_plugins();
3183 return in_array($auth, $enabled);
3187 * Returns an authentication plugin instance.
3189 * @global object
3190 * @param string $auth name of authentication plugin
3191 * @return auth_plugin_base An instance of the required authentication plugin.
3193 function get_auth_plugin($auth) {
3194 global $CFG;
3196 // check the plugin exists first
3197 if (! exists_auth_plugin($auth)) {
3198 print_error('authpluginnotfound', 'debug', '', $auth);
3201 // return auth plugin instance
3202 require_once "{$CFG->dirroot}/auth/$auth/auth.php";
3203 $class = "auth_plugin_$auth";
3204 return new $class;
3208 * Returns array of active auth plugins.
3210 * @param bool $fix fix $CFG->auth if needed
3211 * @return array
3213 function get_enabled_auth_plugins($fix=false) {
3214 global $CFG;
3216 $default = array('manual', 'nologin');
3218 if (empty($CFG->auth)) {
3219 $auths = array();
3220 } else {
3221 $auths = explode(',', $CFG->auth);
3224 if ($fix) {
3225 $auths = array_unique($auths);
3226 foreach($auths as $k=>$authname) {
3227 if (!exists_auth_plugin($authname) or in_array($authname, $default)) {
3228 unset($auths[$k]);
3231 $newconfig = implode(',', $auths);
3232 if (!isset($CFG->auth) or $newconfig != $CFG->auth) {
3233 set_config('auth', $newconfig);
3237 return (array_merge($default, $auths));
3241 * Returns true if an internal authentication method is being used.
3242 * if method not specified then, global default is assumed
3244 * @param string $auth Form of authentication required
3245 * @return bool
3247 function is_internal_auth($auth) {
3248 $authplugin = get_auth_plugin($auth); // throws error if bad $auth
3249 return $authplugin->is_internal();
3253 * Returns true if the user is a 'restored' one
3255 * Used in the login process to inform the user
3256 * and allow him/her to reset the password
3258 * @uses $CFG
3259 * @uses $DB
3260 * @param string $username username to be checked
3261 * @return bool
3263 function is_restored_user($username) {
3264 global $CFG, $DB;
3266 return $DB->record_exists('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id, 'password'=>'restored'));
3270 * Returns an array of user fields
3272 * @return array User field/column names
3274 function get_user_fieldnames() {
3275 global $DB;
3277 $fieldarray = $DB->get_columns('user');
3278 unset($fieldarray['id']);
3279 $fieldarray = array_keys($fieldarray);
3281 return $fieldarray;
3285 * Creates a bare-bones user record
3287 * @todo Outline auth types and provide code example
3289 * @param string $username New user's username to add to record
3290 * @param string $password New user's password to add to record
3291 * @param string $auth Form of authentication required
3292 * @return stdClass A complete user object
3294 function create_user_record($username, $password, $auth = 'manual') {
3295 global $CFG, $DB;
3297 //just in case check text case
3298 $username = trim(moodle_strtolower($username));
3300 $authplugin = get_auth_plugin($auth);
3302 $newuser = new stdClass();
3304 if ($newinfo = $authplugin->get_userinfo($username)) {
3305 $newinfo = truncate_userinfo($newinfo);
3306 foreach ($newinfo as $key => $value){
3307 $newuser->$key = $value;
3311 if (!empty($newuser->email)) {
3312 if (email_is_not_allowed($newuser->email)) {
3313 unset($newuser->email);
3317 if (!isset($newuser->city)) {
3318 $newuser->city = '';
3321 $newuser->auth = $auth;
3322 $newuser->username = $username;
3324 // fix for MDL-8480
3325 // user CFG lang for user if $newuser->lang is empty
3326 // or $user->lang is not an installed language
3327 if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) {
3328 $newuser->lang = $CFG->lang;
3330 $newuser->confirmed = 1;
3331 $newuser->lastip = getremoteaddr();
3332 $newuser->timecreated = time();
3333 $newuser->timemodified = $newuser->timecreated;
3334 $newuser->mnethostid = $CFG->mnet_localhost_id;
3336 $newuser->id = $DB->insert_record('user', $newuser);
3337 $user = get_complete_user_data('id', $newuser->id);
3338 if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})){
3339 set_user_preference('auth_forcepasswordchange', 1, $user);
3341 update_internal_user_password($user, $password);
3343 // fetch full user record for the event, the complete user data contains too much info
3344 // and we want to be consistent with other places that trigger this event
3345 events_trigger('user_created', $DB->get_record('user', array('id'=>$user->id)));
3347 return $user;
3351 * Will update a local user record from an external source.
3352 * (MNET users can not be updated using this method!)
3354 * @param string $username user's username to update the record
3355 * @return stdClass A complete user object
3357 function update_user_record($username) {
3358 global $DB, $CFG;
3360 $username = trim(moodle_strtolower($username)); /// just in case check text case
3362 $oldinfo = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id), '*', MUST_EXIST);
3363 $newuser = array();
3364 $userauth = get_auth_plugin($oldinfo->auth);
3366 if ($newinfo = $userauth->get_userinfo($username)) {
3367 $newinfo = truncate_userinfo($newinfo);
3368 foreach ($newinfo as $key => $value){
3369 $key = strtolower($key);
3370 if (!property_exists($oldinfo, $key) or $key === 'username' or $key === 'id'
3371 or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') {
3372 // unknown or must not be changed
3373 continue;
3375 $confval = $userauth->config->{'field_updatelocal_' . $key};
3376 $lockval = $userauth->config->{'field_lock_' . $key};
3377 if (empty($confval) || empty($lockval)) {
3378 continue;
3380 if ($confval === 'onlogin') {
3381 // MDL-4207 Don't overwrite modified user profile values with
3382 // empty LDAP values when 'unlocked if empty' is set. The purpose
3383 // of the setting 'unlocked if empty' is to allow the user to fill
3384 // in a value for the selected field _if LDAP is giving
3385 // nothing_ for this field. Thus it makes sense to let this value
3386 // stand in until LDAP is giving a value for this field.
3387 if (!(empty($value) && $lockval === 'unlockedifempty')) {
3388 if ((string)$oldinfo->$key !== (string)$value) {
3389 $newuser[$key] = (string)$value;
3394 if ($newuser) {
3395 $newuser['id'] = $oldinfo->id;
3396 $newuser['timemodified'] = time();
3397 $DB->update_record('user', $newuser);
3398 // fetch full user record for the event, the complete user data contains too much info
3399 // and we want to be consistent with other places that trigger this event
3400 events_trigger('user_updated', $DB->get_record('user', array('id'=>$oldinfo->id)));
3404 return get_complete_user_data('id', $oldinfo->id);
3408 * Will truncate userinfo as it comes from auth_get_userinfo (from external auth)
3409 * which may have large fields
3411 * @todo Add vartype handling to ensure $info is an array
3413 * @param array $info Array of user properties to truncate if needed
3414 * @return array The now truncated information that was passed in
3416 function truncate_userinfo($info) {
3417 // define the limits
3418 $limit = array(
3419 'username' => 100,
3420 'idnumber' => 255,
3421 'firstname' => 100,
3422 'lastname' => 100,
3423 'email' => 100,
3424 'icq' => 15,
3425 'phone1' => 20,
3426 'phone2' => 20,
3427 'institution' => 40,
3428 'department' => 30,
3429 'address' => 70,
3430 'city' => 120,
3431 'country' => 2,
3432 'url' => 255,
3435 $textlib = textlib_get_instance();
3436 // apply where needed
3437 foreach (array_keys($info) as $key) {
3438 if (!empty($limit[$key])) {
3439 $info[$key] = trim($textlib->substr($info[$key],0, $limit[$key]));
3443 return $info;
3447 * Marks user deleted in internal user database and notifies the auth plugin.
3448 * Also unenrols user from all roles and does other cleanup.
3450 * Any plugin that needs to purge user data should register the 'user_deleted' event.
3452 * @param stdClass $user full user object before delete
3453 * @return boolean always true
3455 function delete_user($user) {
3456 global $CFG, $DB;
3457 require_once($CFG->libdir.'/grouplib.php');
3458 require_once($CFG->libdir.'/gradelib.php');
3459 require_once($CFG->dirroot.'/message/lib.php');
3460 require_once($CFG->dirroot.'/tag/lib.php');
3462 // delete all grades - backup is kept in grade_grades_history table
3463 grade_user_delete($user->id);
3465 //move unread messages from this user to read
3466 message_move_userfrom_unread2read($user->id);
3468 // TODO: remove from cohorts using standard API here
3470 // remove user tags
3471 tag_set('user', $user->id, array());
3473 // unconditionally unenrol from all courses
3474 enrol_user_delete($user);
3476 // unenrol from all roles in all contexts
3477 role_unassign_all(array('userid'=>$user->id)); // this might be slow but it is really needed - modules might do some extra cleanup!
3479 //now do a brute force cleanup
3481 // remove from all cohorts
3482 $DB->delete_records('cohort_members', array('userid'=>$user->id));
3484 // remove from all groups
3485 $DB->delete_records('groups_members', array('userid'=>$user->id));
3487 // brute force unenrol from all courses
3488 $DB->delete_records('user_enrolments', array('userid'=>$user->id));
3490 // purge user preferences
3491 $DB->delete_records('user_preferences', array('userid'=>$user->id));
3493 // purge user extra profile info
3494 $DB->delete_records('user_info_data', array('userid'=>$user->id));
3496 // last course access not necessary either
3497 $DB->delete_records('user_lastaccess', array('userid'=>$user->id));
3499 // remove all user tokens
3500 $DB->delete_records('external_tokens', array('userid'=>$user->id));
3502 // unauthorise the user for all services
3503 $DB->delete_records('external_services_users', array('userid'=>$user->id));
3505 // now do a final accesslib cleanup - removes all role assignments in user context and context itself
3506 delete_context(CONTEXT_USER, $user->id);
3508 // workaround for bulk deletes of users with the same email address
3509 $delname = "$user->email.".time();
3510 while ($DB->record_exists('user', array('username'=>$delname))) { // no need to use mnethostid here
3511 $delname++;
3514 // mark internal user record as "deleted"
3515 $updateuser = new stdClass();
3516 $updateuser->id = $user->id;
3517 $updateuser->deleted = 1;
3518 $updateuser->username = $delname; // Remember it just in case
3519 $updateuser->email = md5($user->username);// Store hash of username, useful importing/restoring users
3520 $updateuser->idnumber = ''; // Clear this field to free it up
3521 $updateuser->timemodified = time();
3523 $DB->update_record('user', $updateuser);
3524 // Add this action to log
3525 add_to_log(SITEID, 'user', 'delete', "view.php?id=$user->id", $user->firstname.' '.$user->lastname);
3528 // notify auth plugin - do not block the delete even when plugin fails
3529 $authplugin = get_auth_plugin($user->auth);
3530 $authplugin->user_delete($user);
3532 // any plugin that needs to cleanup should register this event
3533 events_trigger('user_deleted', $user);
3535 return true;
3539 * Retrieve the guest user object
3541 * @global object
3542 * @global object
3543 * @return user A {@link $USER} object
3545 function guest_user() {
3546 global $CFG, $DB;
3548 if ($newuser = $DB->get_record('user', array('id'=>$CFG->siteguest))) {
3549 $newuser->confirmed = 1;
3550 $newuser->lang = $CFG->lang;
3551 $newuser->lastip = getremoteaddr();
3554 return $newuser;
3558 * Authenticates a user against the chosen authentication mechanism
3560 * Given a username and password, this function looks them
3561 * up using the currently selected authentication mechanism,
3562 * and if the authentication is successful, it returns a
3563 * valid $user object from the 'user' table.
3565 * Uses auth_ functions from the currently active auth module
3567 * After authenticate_user_login() returns success, you will need to
3568 * log that the user has logged in, and call complete_user_login() to set
3569 * the session up.
3571 * Note: this function works only with non-mnet accounts!
3573 * @param string $username User's username
3574 * @param string $password User's password
3575 * @return user|flase A {@link $USER} object or false if error
3577 function authenticate_user_login($username, $password) {
3578 global $CFG, $DB;
3580 $authsenabled = get_enabled_auth_plugins();
3582 if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) {
3583 $auth = empty($user->auth) ? 'manual' : $user->auth; // use manual if auth not set
3584 if (!empty($user->suspended)) {
3585 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
3586 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']);
3587 return false;
3589 if ($auth=='nologin' or !is_enabled_auth($auth)) {
3590 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
3591 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Disabled Login: $username ".$_SERVER['HTTP_USER_AGENT']);
3592 return false;
3594 $auths = array($auth);
3596 } else {
3597 // check if there's a deleted record (cheaply)
3598 if ($DB->get_field('user', 'id', array('username'=>$username, 'deleted'=>1))) {
3599 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Deleted Login: $username ".$_SERVER['HTTP_USER_AGENT']);
3600 return false;
3603 // User does not exist
3604 $auths = $authsenabled;
3605 $user = new stdClass();
3606 $user->id = 0;
3609 foreach ($auths as $auth) {
3610 $authplugin = get_auth_plugin($auth);
3612 // on auth fail fall through to the next plugin
3613 if (!$authplugin->user_login($username, $password)) {
3614 continue;
3617 // successful authentication
3618 if ($user->id) { // User already exists in database
3619 if (empty($user->auth)) { // For some reason auth isn't set yet
3620 $DB->set_field('user', 'auth', $auth, array('username'=>$username));
3621 $user->auth = $auth;
3623 if (empty($user->firstaccess)) { //prevent firstaccess from remaining 0 for manual account that never required confirmation
3624 $DB->set_field('user','firstaccess', $user->timemodified, array('id' => $user->id));
3625 $user->firstaccess = $user->timemodified;
3628 update_internal_user_password($user, $password); // just in case salt or encoding were changed (magic quotes too one day)
3630 if ($authplugin->is_synchronised_with_external()) { // update user record from external DB
3631 $user = update_user_record($username);
3633 } else {
3634 // if user not found and user creation is not disabled, create it
3635 if (empty($CFG->authpreventaccountcreation)) {
3636 $user = create_user_record($username, $password, $auth);
3637 } else {
3638 continue;
3642 $authplugin->sync_roles($user);
3644 foreach ($authsenabled as $hau) {
3645 $hauth = get_auth_plugin($hau);
3646 $hauth->user_authenticated_hook($user, $username, $password);
3649 if (empty($user->id)) {
3650 return false;
3653 if (!empty($user->suspended)) {
3654 // just in case some auth plugin suspended account
3655 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
3656 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']);
3657 return false;
3660 return $user;
3663 // failed if all the plugins have failed
3664 add_to_log(SITEID, 'login', 'error', 'index.php', $username);
3665 if (debugging('', DEBUG_ALL)) {
3666 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Failed Login: $username ".$_SERVER['HTTP_USER_AGENT']);
3668 return false;
3672 * Call to complete the user login process after authenticate_user_login()
3673 * has succeeded. It will setup the $USER variable and other required bits
3674 * and pieces.
3676 * NOTE:
3677 * - It will NOT log anything -- up to the caller to decide what to log.
3679 * @param object $user
3680 * @param bool $setcookie
3681 * @return object A {@link $USER} object - BC only, do not use
3683 function complete_user_login($user, $setcookie=true) {
3684 global $CFG, $USER;
3686 // regenerate session id and delete old session,
3687 // this helps prevent session fixation attacks from the same domain
3688 session_regenerate_id(true);
3690 // check enrolments, load caps and setup $USER object
3691 session_set_user($user);
3693 // reload preferences from DB
3694 unset($user->preference);
3695 check_user_preferences_loaded($user);
3697 // update login times
3698 update_user_login_times();
3700 // extra session prefs init
3701 set_login_session_preferences();
3703 if (isguestuser()) {
3704 // no need to continue when user is THE guest
3705 return $USER;
3708 if ($setcookie) {
3709 if (empty($CFG->nolastloggedin)) {
3710 set_moodle_cookie($USER->username);
3711 } else {
3712 // do not store last logged in user in cookie
3713 // auth plugins can temporarily override this from loginpage_hook()
3714 // do not save $CFG->nolastloggedin in database!
3715 set_moodle_cookie('');
3719 /// Select password change url
3720 $userauth = get_auth_plugin($USER->auth);
3722 /// check whether the user should be changing password
3723 if (get_user_preferences('auth_forcepasswordchange', false)){
3724 if ($userauth->can_change_password()) {
3725 if ($changeurl = $userauth->change_password_url()) {
3726 redirect($changeurl);
3727 } else {
3728 redirect($CFG->httpswwwroot.'/login/change_password.php');
3730 } else {
3731 print_error('nopasswordchangeforced', 'auth');
3734 return $USER;
3738 * Compare password against hash stored in internal user table.
3739 * If necessary it also updates the stored hash to new format.
3741 * @param stdClass $user (password property may be updated)
3742 * @param string $password plain text password
3743 * @return bool is password valid?
3745 function validate_internal_user_password($user, $password) {
3746 global $CFG;
3748 if (!isset($CFG->passwordsaltmain)) {
3749 $CFG->passwordsaltmain = '';
3752 $validated = false;
3754 if ($user->password === 'not cached') {
3755 // internal password is not used at all, it can not validate
3757 } else if ($user->password === md5($password.$CFG->passwordsaltmain)
3758 or $user->password === md5($password)
3759 or $user->password === md5(addslashes($password).$CFG->passwordsaltmain)
3760 or $user->password === md5(addslashes($password))) {
3761 // note: we are intentionally using the addslashes() here because we
3762 // need to accept old password hashes of passwords with magic quotes
3763 $validated = true;
3765 } else {
3766 for ($i=1; $i<=20; $i++) { //20 alternative salts should be enough, right?
3767 $alt = 'passwordsaltalt'.$i;
3768 if (!empty($CFG->$alt)) {
3769 if ($user->password === md5($password.$CFG->$alt) or $user->password === md5(addslashes($password).$CFG->$alt)) {
3770 $validated = true;
3771 break;
3777 if ($validated) {
3778 // force update of password hash using latest main password salt and encoding if needed
3779 update_internal_user_password($user, $password);
3782 return $validated;
3786 * Calculate hashed value from password using current hash mechanism.
3788 * @param string $password
3789 * @return string password hash
3791 function hash_internal_user_password($password) {
3792 global $CFG;
3794 if (isset($CFG->passwordsaltmain)) {
3795 return md5($password.$CFG->passwordsaltmain);
3796 } else {
3797 return md5($password);
3802 * Update password hash in user object.
3804 * @param stdClass $user (password property may be updated)
3805 * @param string $password plain text password
3806 * @return bool always returns true
3808 function update_internal_user_password($user, $password) {
3809 global $DB;
3811 $authplugin = get_auth_plugin($user->auth);
3812 if ($authplugin->prevent_local_passwords()) {
3813 $hashedpassword = 'not cached';
3814 } else {
3815 $hashedpassword = hash_internal_user_password($password);
3818 if ($user->password !== $hashedpassword) {
3819 $DB->set_field('user', 'password', $hashedpassword, array('id'=>$user->id));
3820 $user->password = $hashedpassword;
3823 return true;
3827 * Get a complete user record, which includes all the info
3828 * in the user record.
3830 * Intended for setting as $USER session variable
3832 * @param string $field The user field to be checked for a given value.
3833 * @param string $value The value to match for $field.
3834 * @param int $mnethostid
3835 * @return mixed False, or A {@link $USER} object.
3837 function get_complete_user_data($field, $value, $mnethostid = null) {
3838 global $CFG, $DB;
3840 if (!$field || !$value) {
3841 return false;
3844 /// Build the WHERE clause for an SQL query
3845 $params = array('fieldval'=>$value);
3846 $constraints = "$field = :fieldval AND deleted <> 1";
3848 // If we are loading user data based on anything other than id,
3849 // we must also restrict our search based on mnet host.
3850 if ($field != 'id') {
3851 if (empty($mnethostid)) {
3852 // if empty, we restrict to local users
3853 $mnethostid = $CFG->mnet_localhost_id;
3856 if (!empty($mnethostid)) {
3857 $params['mnethostid'] = $mnethostid;
3858 $constraints .= " AND mnethostid = :mnethostid";
3861 /// Get all the basic user data
3863 if (! $user = $DB->get_record_select('user', $constraints, $params)) {
3864 return false;
3867 /// Get various settings and preferences
3869 // preload preference cache
3870 check_user_preferences_loaded($user);
3872 // load course enrolment related stuff
3873 $user->lastcourseaccess = array(); // during last session
3874 $user->currentcourseaccess = array(); // during current session
3875 if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid'=>$user->id))) {
3876 foreach ($lastaccesses as $lastaccess) {
3877 $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess;
3881 $sql = "SELECT g.id, g.courseid
3882 FROM {groups} g, {groups_members} gm
3883 WHERE gm.groupid=g.id AND gm.userid=?";
3885 // this is a special hack to speedup calendar display
3886 $user->groupmember = array();
3887 if (!isguestuser($user)) {
3888 if ($groups = $DB->get_records_sql($sql, array($user->id))) {
3889 foreach ($groups as $group) {
3890 if (!array_key_exists($group->courseid, $user->groupmember)) {
3891 $user->groupmember[$group->courseid] = array();
3893 $user->groupmember[$group->courseid][$group->id] = $group->id;
3898 /// Add the custom profile fields to the user record
3899 $user->profile = array();
3900 if (!isguestuser($user)) {
3901 require_once($CFG->dirroot.'/user/profile/lib.php');
3902 profile_load_custom_fields($user);
3905 /// Rewrite some variables if necessary
3906 if (!empty($user->description)) {
3907 $user->description = true; // No need to cart all of it around
3909 if (isguestuser($user)) {
3910 $user->lang = $CFG->lang; // Guest language always same as site
3911 $user->firstname = get_string('guestuser'); // Name always in current language
3912 $user->lastname = ' ';
3915 return $user;
3919 * Validate a password against the configured password policy
3921 * @global object
3922 * @param string $password the password to be checked against the password policy
3923 * @param string $errmsg the error message to display when the password doesn't comply with the policy.
3924 * @return bool true if the password is valid according to the policy. false otherwise.
3926 function check_password_policy($password, &$errmsg) {
3927 global $CFG;
3929 if (empty($CFG->passwordpolicy)) {
3930 return true;
3933 $textlib = textlib_get_instance();
3934 $errmsg = '';
3935 if ($textlib->strlen($password) < $CFG->minpasswordlength) {
3936 $errmsg .= '<div>'. get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength) .'</div>';
3939 if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) {
3940 $errmsg .= '<div>'. get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits) .'</div>';
3943 if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) {
3944 $errmsg .= '<div>'. get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower) .'</div>';
3947 if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) {
3948 $errmsg .= '<div>'. get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper) .'</div>';
3951 if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) {
3952 $errmsg .= '<div>'. get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum) .'</div>';
3954 if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) {
3955 $errmsg .= '<div>'. get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars) .'</div>';
3958 if ($errmsg == '') {
3959 return true;
3960 } else {
3961 return false;
3967 * When logging in, this function is run to set certain preferences
3968 * for the current SESSION
3970 * @global object
3971 * @global object
3973 function set_login_session_preferences() {
3974 global $SESSION, $CFG;
3976 $SESSION->justloggedin = true;
3978 unset($SESSION->lang);
3983 * Delete a course, including all related data from the database,
3984 * and any associated files.
3986 * @global object
3987 * @global object
3988 * @param mixed $courseorid The id of the course or course object to delete.
3989 * @param bool $showfeedback Whether to display notifications of each action the function performs.
3990 * @return bool true if all the removals succeeded. false if there were any failures. If this
3991 * method returns false, some of the removals will probably have succeeded, and others
3992 * failed, but you have no way of knowing which.
3994 function delete_course($courseorid, $showfeedback = true) {
3995 global $DB;
3997 if (is_object($courseorid)) {
3998 $courseid = $courseorid->id;
3999 $course = $courseorid;
4000 } else {
4001 $courseid = $courseorid;
4002 if (!$course = $DB->get_record('course', array('id'=>$courseid))) {
4003 return false;
4006 $context = get_context_instance(CONTEXT_COURSE, $courseid);
4008 // frontpage course can not be deleted!!
4009 if ($courseid == SITEID) {
4010 return false;
4013 // make the course completely empty
4014 remove_course_contents($courseid, $showfeedback);
4016 // delete the course and related context instance
4017 delete_context(CONTEXT_COURSE, $courseid);
4018 $DB->delete_records("course", array("id"=>$courseid));
4020 //trigger events
4021 $course->context = $context; // you can not fetch context in the event because it was already deleted
4022 events_trigger('course_deleted', $course);
4024 return true;
4028 * Clear a course out completely, deleting all content
4029 * but don't delete the course itself.
4030 * This function does not verify any permissions.
4032 * Please note this function also deletes all user enrolments,
4033 * enrolment instances and role assignments.
4035 * @param int $courseid The id of the course that is being deleted
4036 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4037 * @return bool true if all the removals succeeded. false if there were any failures. If this
4038 * method returns false, some of the removals will probably have succeeded, and others
4039 * failed, but you have no way of knowing which.
4041 function remove_course_contents($courseid, $showfeedback = true) {
4042 global $CFG, $DB, $OUTPUT;
4043 require_once($CFG->libdir.'/completionlib.php');
4044 require_once($CFG->libdir.'/questionlib.php');
4045 require_once($CFG->libdir.'/gradelib.php');
4046 require_once($CFG->dirroot.'/group/lib.php');
4047 require_once($CFG->dirroot.'/tag/coursetagslib.php');
4049 $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST);
4050 $context = get_context_instance(CONTEXT_COURSE, $courseid, MUST_EXIST);
4052 $strdeleted = get_string('deleted');
4054 // Delete course completion information,
4055 // this has to be done before grades and enrols
4056 $cc = new completion_info($course);
4057 $cc->clear_criteria();
4059 // remove roles and enrolments
4060 role_unassign_all(array('contextid'=>$context->id), true);
4061 enrol_course_delete($course);
4063 // Clean up course formats (iterate through all formats in the even the course format was ever changed)
4064 $formats = get_plugin_list('format');
4065 foreach ($formats as $format=>$formatdir) {
4066 $formatdelete = 'format_'.$format.'_delete_course';
4067 $formatlib = "$formatdir/lib.php";
4068 if (file_exists($formatlib)) {
4069 include_once($formatlib);
4070 if (function_exists($formatdelete)) {
4071 if ($showfeedback) {
4072 echo $OUTPUT->notification($strdeleted.' '.$format);
4074 $formatdelete($course->id);
4079 // Remove all data from gradebook - this needs to be done before course modules
4080 // because while deleting this information, the system may need to reference
4081 // the course modules that own the grades.
4082 remove_course_grades($courseid, $showfeedback);
4083 remove_grade_letters($context, $showfeedback);
4085 // Remove all data from availability and completion tables that is associated
4086 // with course-modules belonging to this course. Note this is done even if the
4087 // features are not enabled now, in case they were enabled previously
4088 $DB->delete_records_select('course_modules_completion',
4089 'coursemoduleid IN (SELECT id from {course_modules} WHERE course=?)',
4090 array($courseid));
4091 $DB->delete_records_select('course_modules_availability',
4092 'coursemoduleid IN (SELECT id from {course_modules} WHERE course=?)',
4093 array($courseid));
4095 // Delete course blocks - they may depend on modules so delete them first
4096 blocks_delete_all_for_context($context->id);
4098 // Delete every instance of every module
4099 if ($allmods = $DB->get_records('modules') ) {
4100 foreach ($allmods as $mod) {
4101 $modname = $mod->name;
4102 $modfile = $CFG->dirroot .'/mod/'. $modname .'/lib.php';
4103 $moddelete = $modname .'_delete_instance'; // Delete everything connected to an instance
4104 $moddeletecourse = $modname .'_delete_course'; // Delete other stray stuff (uncommon)
4105 $count=0;
4106 if (file_exists($modfile)) {
4107 include_once($modfile);
4108 if (function_exists($moddelete)) {
4109 if ($instances = $DB->get_records($modname, array('course'=>$course->id))) {
4110 foreach ($instances as $instance) {
4111 if ($cm = get_coursemodule_from_instance($modname, $instance->id, $course->id)) {
4112 /// Delete activity context questions and question categories
4113 question_delete_activity($cm, $showfeedback);
4115 if ($moddelete($instance->id)) {
4116 $count++;
4118 } else {
4119 echo $OUTPUT->notification('Could not delete '. $modname .' instance '. $instance->id .' ('. format_string($instance->name) .')');
4121 if ($cm) {
4122 // delete cm and its context in correct order
4123 delete_context(CONTEXT_MODULE, $cm->id); // some callbacks may try to fetch context, better delete first
4124 $DB->delete_records('course_modules', array('id'=>$cm->id));
4128 } else {
4129 //note: we should probably delete these anyway
4130 echo $OUTPUT->notification('Function '.$moddelete.'() doesn\'t exist!');
4133 if (function_exists($moddeletecourse)) {
4134 $moddeletecourse($course, $showfeedback);
4137 if ($showfeedback) {
4138 echo $OUTPUT->notification($strdeleted .' '. $count .' x '. $modname);
4143 // Delete any groups, removing members and grouping/course links first.
4144 groups_delete_groupings($course->id, $showfeedback);
4145 groups_delete_groups($course->id, $showfeedback);
4147 // Delete questions and question categories
4148 question_delete_course($course, $showfeedback);
4150 // Delete course tags
4151 coursetag_delete_course_tags($course->id, $showfeedback);
4153 // Delete legacy files (just in case some files are still left there after conversion to new file api)
4154 fulldelete($CFG->dataroot.'/'.$course->id);
4156 // cleanup course record - remove links to delted stuff
4157 $oldcourse = new stdClass();
4158 $oldcourse->id = $course->id;
4159 $oldcourse->summary = '';
4160 $oldcourse->modinfo = NULL;
4161 $oldcourse->legacyfiles = 0;
4162 $oldcourse->defaultgroupingid = 0;
4163 $oldcourse->enablecompletion = 0;
4164 $DB->update_record('course', $oldcourse);
4166 // Delete all related records in other tables that may have a courseid
4167 // This array stores the tables that need to be cleared, as
4168 // table_name => column_name that contains the course id.
4169 $tablestoclear = array(
4170 'event' => 'courseid', // Delete events
4171 'log' => 'course', // Delete logs
4172 'course_sections' => 'course', // Delete any course stuff
4173 'course_modules' => 'course',
4174 'course_display' => 'course',
4175 'backup_courses' => 'courseid', // Delete scheduled backup stuff
4176 'user_lastaccess' => 'courseid',
4177 'backup_log' => 'courseid'
4179 foreach ($tablestoclear as $table => $col) {
4180 $DB->delete_records($table, array($col=>$course->id));
4183 // Delete all remaining stuff linked to context,
4184 // such as remaining roles, files, comments, etc.
4185 // Keep the context record for now.
4186 delete_context(CONTEXT_COURSE, $course->id, false);
4188 //trigger events
4189 $course->context = $context; // you can not access context in cron event later after course is deleted
4190 events_trigger('course_content_removed', $course);
4192 return true;
4196 * Change dates in module - used from course reset.
4198 * @global object
4199 * @global object
4200 * @param string $modname forum, assignment, etc
4201 * @param array $fields array of date fields from mod table
4202 * @param int $timeshift time difference
4203 * @param int $courseid
4204 * @return bool success
4206 function shift_course_mod_dates($modname, $fields, $timeshift, $courseid) {
4207 global $CFG, $DB;
4208 include_once($CFG->dirroot.'/mod/'.$modname.'/lib.php');
4210 $return = true;
4211 foreach ($fields as $field) {
4212 $updatesql = "UPDATE {".$modname."}
4213 SET $field = $field + ?
4214 WHERE course=? AND $field<>0 AND $field<>0";
4215 $return = $DB->execute($updatesql, array($timeshift, $courseid)) && $return;
4218 $refreshfunction = $modname.'_refresh_events';
4219 if (function_exists($refreshfunction)) {
4220 $refreshfunction($courseid);
4223 return $return;
4227 * This function will empty a course of user data.
4228 * It will retain the activities and the structure of the course.
4230 * @param object $data an object containing all the settings including courseid (without magic quotes)
4231 * @return array status array of array component, item, error
4233 function reset_course_userdata($data) {
4234 global $CFG, $USER, $DB;
4235 require_once($CFG->libdir.'/gradelib.php');
4236 require_once($CFG->libdir.'/completionlib.php');
4237 require_once($CFG->dirroot.'/group/lib.php');
4239 $data->courseid = $data->id;
4240 $context = get_context_instance(CONTEXT_COURSE, $data->courseid);
4242 // calculate the time shift of dates
4243 if (!empty($data->reset_start_date)) {
4244 // time part of course startdate should be zero
4245 $data->timeshift = $data->reset_start_date - usergetmidnight($data->reset_start_date_old);
4246 } else {
4247 $data->timeshift = 0;
4250 // result array: component, item, error
4251 $status = array();
4253 // start the resetting
4254 $componentstr = get_string('general');
4256 // move the course start time
4257 if (!empty($data->reset_start_date) and $data->timeshift) {
4258 // change course start data
4259 $DB->set_field('course', 'startdate', $data->reset_start_date, array('id'=>$data->courseid));
4260 // update all course and group events - do not move activity events
4261 $updatesql = "UPDATE {event}
4262 SET timestart = timestart + ?
4263 WHERE courseid=? AND instance=0";
4264 $DB->execute($updatesql, array($data->timeshift, $data->courseid));
4266 $status[] = array('component'=>$componentstr, 'item'=>get_string('datechanged'), 'error'=>false);
4269 if (!empty($data->reset_logs)) {
4270 $DB->delete_records('log', array('course'=>$data->courseid));
4271 $status[] = array('component'=>$componentstr, 'item'=>get_string('deletelogs'), 'error'=>false);
4274 if (!empty($data->reset_events)) {
4275 $DB->delete_records('event', array('courseid'=>$data->courseid));
4276 $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteevents', 'calendar'), 'error'=>false);
4279 if (!empty($data->reset_notes)) {
4280 require_once($CFG->dirroot.'/notes/lib.php');
4281 note_delete_all($data->courseid);
4282 $status[] = array('component'=>$componentstr, 'item'=>get_string('deletenotes', 'notes'), 'error'=>false);
4285 if (!empty($data->delete_blog_associations)) {
4286 require_once($CFG->dirroot.'/blog/lib.php');
4287 blog_remove_associations_for_course($data->courseid);
4288 $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteblogassociations', 'blog'), 'error'=>false);
4291 if (!empty($data->reset_course_completion)) {
4292 // Delete course completion information
4293 $course = $DB->get_record('course', array('id'=>$data->courseid));
4294 $cc = new completion_info($course);
4295 $cc->delete_course_completion_data();
4296 $status[] = array('component'=>$componentstr, 'item'=>get_string('deletecoursecompletiondata', 'completion'), 'error'=>false);
4299 $componentstr = get_string('roles');
4301 if (!empty($data->reset_roles_overrides)) {
4302 $children = get_child_contexts($context);
4303 foreach ($children as $child) {
4304 $DB->delete_records('role_capabilities', array('contextid'=>$child->id));
4306 $DB->delete_records('role_capabilities', array('contextid'=>$context->id));
4307 //force refresh for logged in users
4308 mark_context_dirty($context->path);
4309 $status[] = array('component'=>$componentstr, 'item'=>get_string('deletecourseoverrides', 'role'), 'error'=>false);
4312 if (!empty($data->reset_roles_local)) {
4313 $children = get_child_contexts($context);
4314 foreach ($children as $child) {
4315 role_unassign_all(array('contextid'=>$child->id));
4317 //force refresh for logged in users
4318 mark_context_dirty($context->path);
4319 $status[] = array('component'=>$componentstr, 'item'=>get_string('deletelocalroles', 'role'), 'error'=>false);
4322 // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc.
4323 $data->unenrolled = array();
4324 if (!empty($data->unenrol_users)) {
4325 $plugins = enrol_get_plugins(true);
4326 $instances = enrol_get_instances($data->courseid, true);
4327 foreach ($instances as $key=>$instance) {
4328 if (!isset($plugins[$instance->enrol])) {
4329 unset($instances[$key]);
4330 continue;
4332 if (!$plugins[$instance->enrol]->allow_unenrol($instance)) {
4333 unset($instances[$key]);
4337 $sqlempty = $DB->sql_empty();
4338 foreach($data->unenrol_users as $withroleid) {
4339 $sql = "SELECT DISTINCT ue.userid, ue.enrolid
4340 FROM {user_enrolments} ue
4341 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
4342 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
4343 JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)";
4344 $params = array('courseid'=>$data->courseid, 'roleid'=>$withroleid, 'courselevel'=>CONTEXT_COURSE);
4346 $rs = $DB->get_recordset_sql($sql, $params);
4347 foreach ($rs as $ue) {
4348 if (!isset($instances[$ue->enrolid])) {
4349 continue;
4351 $plugins[$instances[$ue->enrolid]->enrol]->unenrol_user($instances[$ue->enrolid], $ue->userid);
4352 $data->unenrolled[$ue->userid] = $ue->userid;
4356 if (!empty($data->unenrolled)) {
4357 $status[] = array('component'=>$componentstr, 'item'=>get_string('unenrol', 'enrol').' ('.count($data->unenrolled).')', 'error'=>false);
4361 $componentstr = get_string('groups');
4363 // remove all group members
4364 if (!empty($data->reset_groups_members)) {
4365 groups_delete_group_members($data->courseid);
4366 $status[] = array('component'=>$componentstr, 'item'=>get_string('removegroupsmembers', 'group'), 'error'=>false);
4369 // remove all groups
4370 if (!empty($data->reset_groups_remove)) {
4371 groups_delete_groups($data->courseid, false);
4372 $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteallgroups', 'group'), 'error'=>false);
4375 // remove all grouping members
4376 if (!empty($data->reset_groupings_members)) {
4377 groups_delete_groupings_groups($data->courseid, false);
4378 $status[] = array('component'=>$componentstr, 'item'=>get_string('removegroupingsmembers', 'group'), 'error'=>false);
4381 // remove all groupings
4382 if (!empty($data->reset_groupings_remove)) {
4383 groups_delete_groupings($data->courseid, false);
4384 $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteallgroupings', 'group'), 'error'=>false);
4387 // Look in every instance of every module for data to delete
4388 $unsupported_mods = array();
4389 if ($allmods = $DB->get_records('modules') ) {
4390 foreach ($allmods as $mod) {
4391 $modname = $mod->name;
4392 if (!$DB->count_records($modname, array('course'=>$data->courseid))) {
4393 continue; // skip mods with no instances
4395 $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php';
4396 $moddeleteuserdata = $modname.'_reset_userdata'; // Function to delete user data
4397 if (file_exists($modfile)) {
4398 include_once($modfile);
4399 if (function_exists($moddeleteuserdata)) {
4400 $modstatus = $moddeleteuserdata($data);
4401 if (is_array($modstatus)) {
4402 $status = array_merge($status, $modstatus);
4403 } else {
4404 debugging('Module '.$modname.' returned incorrect staus - must be an array!');
4406 } else {
4407 $unsupported_mods[] = $mod;
4409 } else {
4410 debugging('Missing lib.php in '.$modname.' module!');
4415 // mention unsupported mods
4416 if (!empty($unsupported_mods)) {
4417 foreach($unsupported_mods as $mod) {
4418 $status[] = array('component'=>get_string('modulenameplural', $mod->name), 'item'=>'', 'error'=>get_string('resetnotimplemented'));
4423 $componentstr = get_string('gradebook', 'grades');
4424 // reset gradebook
4425 if (!empty($data->reset_gradebook_items)) {
4426 remove_course_grades($data->courseid, false);
4427 grade_grab_course_grades($data->courseid);
4428 grade_regrade_final_grades($data->courseid);
4429 $status[] = array('component'=>$componentstr, 'item'=>get_string('removeallcourseitems', 'grades'), 'error'=>false);
4431 } else if (!empty($data->reset_gradebook_grades)) {
4432 grade_course_reset($data->courseid);
4433 $status[] = array('component'=>$componentstr, 'item'=>get_string('removeallcoursegrades', 'grades'), 'error'=>false);
4435 // reset comments
4436 if (!empty($data->reset_comments)) {
4437 require_once($CFG->dirroot.'/comment/lib.php');
4438 comment::reset_course_page_comments($context);
4441 return $status;
4445 * Generate an email processing address
4447 * @param int $modid
4448 * @param string $modargs
4449 * @return string Returns email processing address
4451 function generate_email_processing_address($modid,$modargs) {
4452 global $CFG;
4454 $header = $CFG->mailprefix . substr(base64_encode(pack('C',$modid)),0,2).$modargs;
4455 return $header . substr(md5($header.get_site_identifier()),0,16).'@'.$CFG->maildomain;
4461 * @todo Finish documenting this function
4463 * @global object
4464 * @param string $modargs
4465 * @param string $body Currently unused
4467 function moodle_process_email($modargs,$body) {
4468 global $DB;
4470 // the first char should be an unencoded letter. We'll take this as an action
4471 switch ($modargs{0}) {
4472 case 'B': { // bounce
4473 list(,$userid) = unpack('V',base64_decode(substr($modargs,1,8)));
4474 if ($user = $DB->get_record("user", array('id'=>$userid), "id,email")) {
4475 // check the half md5 of their email
4476 $md5check = substr(md5($user->email),0,16);
4477 if ($md5check == substr($modargs, -16)) {
4478 set_bounce_count($user);
4480 // else maybe they've already changed it?
4483 break;
4484 // maybe more later?
4488 /// CORRESPONDENCE ////////////////////////////////////////////////
4491 * Get mailer instance, enable buffering, flush buffer or disable buffering.
4493 * @global object
4494 * @param string $action 'get', 'buffer', 'close' or 'flush'
4495 * @return object|null mailer instance if 'get' used or nothing
4497 function get_mailer($action='get') {
4498 global $CFG;
4500 static $mailer = null;
4501 static $counter = 0;
4503 if (!isset($CFG->smtpmaxbulk)) {
4504 $CFG->smtpmaxbulk = 1;
4507 if ($action == 'get') {
4508 $prevkeepalive = false;
4510 if (isset($mailer) and $mailer->Mailer == 'smtp') {
4511 if ($counter < $CFG->smtpmaxbulk and !$mailer->IsError()) {
4512 $counter++;
4513 // reset the mailer
4514 $mailer->Priority = 3;
4515 $mailer->CharSet = 'UTF-8'; // our default
4516 $mailer->ContentType = "text/plain";
4517 $mailer->Encoding = "8bit";
4518 $mailer->From = "root@localhost";
4519 $mailer->FromName = "Root User";
4520 $mailer->Sender = "";
4521 $mailer->Subject = "";
4522 $mailer->Body = "";
4523 $mailer->AltBody = "";
4524 $mailer->ConfirmReadingTo = "";
4526 $mailer->ClearAllRecipients();
4527 $mailer->ClearReplyTos();
4528 $mailer->ClearAttachments();
4529 $mailer->ClearCustomHeaders();
4530 return $mailer;
4533 $prevkeepalive = $mailer->SMTPKeepAlive;
4534 get_mailer('flush');
4537 include_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php');
4538 $mailer = new moodle_phpmailer();
4540 $counter = 1;
4542 $mailer->Version = 'Moodle '.$CFG->version; // mailer version
4543 $mailer->PluginDir = $CFG->libdir.'/phpmailer/'; // plugin directory (eg smtp plugin)
4544 $mailer->CharSet = 'UTF-8';
4546 // some MTAs may do double conversion of LF if CRLF used, CRLF is required line ending in RFC 822bis
4547 if (isset($CFG->mailnewline) and $CFG->mailnewline == 'CRLF') {
4548 $mailer->LE = "\r\n";
4549 } else {
4550 $mailer->LE = "\n";
4553 if ($CFG->smtphosts == 'qmail') {
4554 $mailer->IsQmail(); // use Qmail system
4556 } else if (empty($CFG->smtphosts)) {
4557 $mailer->IsMail(); // use PHP mail() = sendmail
4559 } else {
4560 $mailer->IsSMTP(); // use SMTP directly
4561 if (!empty($CFG->debugsmtp)) {
4562 $mailer->SMTPDebug = true;
4564 $mailer->Host = $CFG->smtphosts; // specify main and backup servers
4565 $mailer->SMTPKeepAlive = $prevkeepalive; // use previous keepalive
4567 if ($CFG->smtpuser) { // Use SMTP authentication
4568 $mailer->SMTPAuth = true;
4569 $mailer->Username = $CFG->smtpuser;
4570 $mailer->Password = $CFG->smtppass;
4574 return $mailer;
4577 $nothing = null;
4579 // keep smtp session open after sending
4580 if ($action == 'buffer') {
4581 if (!empty($CFG->smtpmaxbulk)) {
4582 get_mailer('flush');
4583 $m = get_mailer();
4584 if ($m->Mailer == 'smtp') {
4585 $m->SMTPKeepAlive = true;
4588 return $nothing;
4591 // close smtp session, but continue buffering
4592 if ($action == 'flush') {
4593 if (isset($mailer) and $mailer->Mailer == 'smtp') {
4594 if (!empty($mailer->SMTPDebug)) {
4595 echo '<pre>'."\n";
4597 $mailer->SmtpClose();
4598 if (!empty($mailer->SMTPDebug)) {
4599 echo '</pre>';
4602 return $nothing;
4605 // close smtp session, do not buffer anymore
4606 if ($action == 'close') {
4607 if (isset($mailer) and $mailer->Mailer == 'smtp') {
4608 get_mailer('flush');
4609 $mailer->SMTPKeepAlive = false;
4611 $mailer = null; // better force new instance
4612 return $nothing;
4617 * Send an email to a specified user
4619 * @global object
4620 * @global string
4621 * @global string IdentityProvider(IDP) URL user hits to jump to mnet peer.
4622 * @uses SITEID
4623 * @param stdClass $user A {@link $USER} object
4624 * @param stdClass $from A {@link $USER} object
4625 * @param string $subject plain text subject line of the email
4626 * @param string $messagetext plain text version of the message
4627 * @param string $messagehtml complete html version of the message (optional)
4628 * @param string $attachment a file on the filesystem, relative to $CFG->dataroot
4629 * @param string $attachname the name of the file (extension indicates MIME)
4630 * @param bool $usetrueaddress determines whether $from email address should
4631 * be sent out. Will be overruled by user profile setting for maildisplay
4632 * @param string $replyto Email address to reply to
4633 * @param string $replytoname Name of reply to recipient
4634 * @param int $wordwrapwidth custom word wrap width, default 79
4635 * @return bool Returns true if mail was sent OK and false if there was an error.
4637 function email_to_user($user, $from, $subject, $messagetext, $messagehtml='', $attachment='', $attachname='', $usetrueaddress=true, $replyto='', $replytoname='', $wordwrapwidth=79) {
4639 global $CFG, $FULLME;
4641 if (empty($user) || empty($user->email)) {
4642 $nulluser = 'User is null or has no email';
4643 error_log($nulluser);
4644 if (CLI_SCRIPT) {
4645 mtrace('Error: lib/moodlelib.php email_to_user(): '.$nulluser);
4647 return false;
4650 if (!empty($user->deleted)) {
4651 // do not mail deleted users
4652 $userdeleted = 'User is deleted';
4653 error_log($userdeleted);
4654 if (CLI_SCRIPT) {
4655 mtrace('Error: lib/moodlelib.php email_to_user(): '.$userdeleted);
4657 return false;
4660 if (!empty($CFG->noemailever)) {
4661 // hidden setting for development sites, set in config.php if needed
4662 $noemail = 'Not sending email due to noemailever config setting';
4663 error_log($noemail);
4664 if (CLI_SCRIPT) {
4665 mtrace('Error: lib/moodlelib.php email_to_user(): '.$noemail);
4667 return true;
4670 if (!empty($CFG->divertallemailsto)) {
4671 $subject = "[DIVERTED {$user->email}] $subject";
4672 $user = clone($user);
4673 $user->email = $CFG->divertallemailsto;
4676 // skip mail to suspended users
4677 if ((isset($user->auth) && $user->auth=='nologin') or (isset($user->suspended) && $user->suspended)) {
4678 return true;
4681 if (!validate_email($user->email)) {
4682 // we can not send emails to invalid addresses - it might create security issue or confuse the mailer
4683 $invalidemail = "User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending.";
4684 error_log($invalidemail);
4685 if (CLI_SCRIPT) {
4686 mtrace('Error: lib/moodlelib.php email_to_user(): '.$invalidemail);
4688 return false;
4691 if (over_bounce_threshold($user)) {
4692 $bouncemsg = "User $user->id (".fullname($user).") is over bounce threshold! Not sending.";
4693 error_log($bouncemsg);
4694 if (CLI_SCRIPT) {
4695 mtrace('Error: lib/moodlelib.php email_to_user(): '.$bouncemsg);
4697 return false;
4700 // If the user is a remote mnet user, parse the email text for URL to the
4701 // wwwroot and modify the url to direct the user's browser to login at their
4702 // home site (identity provider - idp) before hitting the link itself
4703 if (is_mnet_remote_user($user)) {
4704 require_once($CFG->dirroot.'/mnet/lib.php');
4706 $jumpurl = mnet_get_idp_jump_url($user);
4707 $callback = partial('mnet_sso_apply_indirection', $jumpurl);
4709 $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%",
4710 $callback,
4711 $messagetext);
4712 $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%",
4713 $callback,
4714 $messagehtml);
4716 $mail = get_mailer();
4718 if (!empty($mail->SMTPDebug)) {
4719 echo '<pre>' . "\n";
4722 $temprecipients = array();
4723 $tempreplyto = array();
4725 $supportuser = generate_email_supportuser();
4727 // make up an email address for handling bounces
4728 if (!empty($CFG->handlebounces)) {
4729 $modargs = 'B'.base64_encode(pack('V',$user->id)).substr(md5($user->email),0,16);
4730 $mail->Sender = generate_email_processing_address(0,$modargs);
4731 } else {
4732 $mail->Sender = $supportuser->email;
4735 if (is_string($from)) { // So we can pass whatever we want if there is need
4736 $mail->From = $CFG->noreplyaddress;
4737 $mail->FromName = $from;
4738 } else if ($usetrueaddress and $from->maildisplay) {
4739 $mail->From = $from->email;
4740 $mail->FromName = fullname($from);
4741 } else {
4742 $mail->From = $CFG->noreplyaddress;
4743 $mail->FromName = fullname($from);
4744 if (empty($replyto)) {
4745 $tempreplyto[] = array($CFG->noreplyaddress, get_string('noreplyname'));
4749 if (!empty($replyto)) {
4750 $tempreplyto[] = array($replyto, $replytoname);
4753 $mail->Subject = substr($subject, 0, 900);
4755 $temprecipients[] = array($user->email, fullname($user));
4757 $mail->WordWrap = $wordwrapwidth; // set word wrap
4759 if (!empty($from->customheaders)) { // Add custom headers
4760 if (is_array($from->customheaders)) {
4761 foreach ($from->customheaders as $customheader) {
4762 $mail->AddCustomHeader($customheader);
4764 } else {
4765 $mail->AddCustomHeader($from->customheaders);
4769 if (!empty($from->priority)) {
4770 $mail->Priority = $from->priority;
4773 if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) { // Don't ever send HTML to users who don't want it
4774 $mail->IsHTML(true);
4775 $mail->Encoding = 'quoted-printable'; // Encoding to use
4776 $mail->Body = $messagehtml;
4777 $mail->AltBody = "\n$messagetext\n";
4778 } else {
4779 $mail->IsHTML(false);
4780 $mail->Body = "\n$messagetext\n";
4783 if ($attachment && $attachname) {
4784 if (preg_match( "~\\.\\.~" ,$attachment )) { // Security check for ".." in dir path
4785 $temprecipients[] = array($supportuser->email, fullname($supportuser, true));
4786 $mail->AddStringAttachment('Error in attachment. User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain');
4787 } else {
4788 require_once($CFG->libdir.'/filelib.php');
4789 $mimetype = mimeinfo('type', $attachname);
4790 $mail->AddAttachment($CFG->dataroot .'/'. $attachment, $attachname, 'base64', $mimetype);
4794 // Check if the email should be sent in an other charset then the default UTF-8
4795 if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) {
4797 // use the defined site mail charset or eventually the one preferred by the recipient
4798 $charset = $CFG->sitemailcharset;
4799 if (!empty($CFG->allowusermailcharset)) {
4800 if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) {
4801 $charset = $useremailcharset;
4805 // convert all the necessary strings if the charset is supported
4806 $charsets = get_list_of_charsets();
4807 unset($charsets['UTF-8']);
4808 if (in_array($charset, $charsets)) {
4809 $textlib = textlib_get_instance();
4810 $mail->CharSet = $charset;
4811 $mail->FromName = $textlib->convert($mail->FromName, 'utf-8', strtolower($charset));
4812 $mail->Subject = $textlib->convert($mail->Subject, 'utf-8', strtolower($charset));
4813 $mail->Body = $textlib->convert($mail->Body, 'utf-8', strtolower($charset));
4814 $mail->AltBody = $textlib->convert($mail->AltBody, 'utf-8', strtolower($charset));
4816 foreach ($temprecipients as $key => $values) {
4817 $temprecipients[$key][1] = $textlib->convert($values[1], 'utf-8', strtolower($charset));
4819 foreach ($tempreplyto as $key => $values) {
4820 $tempreplyto[$key][1] = $textlib->convert($values[1], 'utf-8', strtolower($charset));
4825 foreach ($temprecipients as $values) {
4826 $mail->AddAddress($values[0], $values[1]);
4828 foreach ($tempreplyto as $values) {
4829 $mail->AddReplyTo($values[0], $values[1]);
4832 if ($mail->Send()) {
4833 set_send_count($user);
4834 $mail->IsSMTP(); // use SMTP directly
4835 if (!empty($mail->SMTPDebug)) {
4836 echo '</pre>';
4838 return true;
4839 } else {
4840 add_to_log(SITEID, 'library', 'mailer', $FULLME, 'ERROR: '. $mail->ErrorInfo);
4841 if (CLI_SCRIPT) {
4842 mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo);
4844 if (!empty($mail->SMTPDebug)) {
4845 echo '</pre>';
4847 return false;
4852 * Generate a signoff for emails based on support settings
4854 * @global object
4855 * @return string
4857 function generate_email_signoff() {
4858 global $CFG;
4860 $signoff = "\n";
4861 if (!empty($CFG->supportname)) {
4862 $signoff .= $CFG->supportname."\n";
4864 if (!empty($CFG->supportemail)) {
4865 $signoff .= $CFG->supportemail."\n";
4867 if (!empty($CFG->supportpage)) {
4868 $signoff .= $CFG->supportpage."\n";
4870 return $signoff;
4874 * Generate a fake user for emails based on support settings
4875 * @global object
4876 * @return object user info
4878 function generate_email_supportuser() {
4879 global $CFG;
4881 static $supportuser;
4883 if (!empty($supportuser)) {
4884 return $supportuser;
4887 $supportuser = new stdClass();
4888 $supportuser->email = $CFG->supportemail ? $CFG->supportemail : $CFG->noreplyaddress;
4889 $supportuser->firstname = $CFG->supportname ? $CFG->supportname : get_string('noreplyname');
4890 $supportuser->lastname = '';
4891 $supportuser->maildisplay = true;
4893 return $supportuser;
4898 * Sets specified user's password and send the new password to the user via email.
4900 * @global object
4901 * @global object
4902 * @param user $user A {@link $USER} object
4903 * @return boolean|string Returns "true" if mail was sent OK and "false" if there was an error
4905 function setnew_password_and_mail($user) {
4906 global $CFG, $DB;
4908 $site = get_site();
4910 $supportuser = generate_email_supportuser();
4912 $newpassword = generate_password();
4914 $DB->set_field('user', 'password', hash_internal_user_password($newpassword), array('id'=>$user->id));
4916 $a = new stdClass();
4917 $a->firstname = fullname($user, true);
4918 $a->sitename = format_string($site->fullname);
4919 $a->username = $user->username;
4920 $a->newpassword = $newpassword;
4921 $a->link = $CFG->wwwroot .'/login/';
4922 $a->signoff = generate_email_signoff();
4924 $message = get_string('newusernewpasswordtext', '', $a);
4926 $subject = format_string($site->fullname) .': '. get_string('newusernewpasswordsubj');
4928 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
4929 return email_to_user($user, $supportuser, $subject, $message);
4934 * Resets specified user's password and send the new password to the user via email.
4936 * @param stdClass $user A {@link $USER} object
4937 * @return bool Returns true if mail was sent OK and false if there was an error.
4939 function reset_password_and_mail($user) {
4940 global $CFG;
4942 $site = get_site();
4943 $supportuser = generate_email_supportuser();
4945 $userauth = get_auth_plugin($user->auth);
4946 if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) {
4947 trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth.");
4948 return false;
4951 $newpassword = generate_password();
4953 if (!$userauth->user_update_password($user, $newpassword)) {
4954 print_error("cannotsetpassword");
4957 $a = new stdClass();
4958 $a->firstname = $user->firstname;
4959 $a->lastname = $user->lastname;
4960 $a->sitename = format_string($site->fullname);
4961 $a->username = $user->username;
4962 $a->newpassword = $newpassword;
4963 $a->link = $CFG->httpswwwroot .'/login/change_password.php';
4964 $a->signoff = generate_email_signoff();
4966 $message = get_string('newpasswordtext', '', $a);
4968 $subject = format_string($site->fullname) .': '. get_string('changedpassword');
4970 unset_user_preference('create_password', $user); // prevent cron from generating the password
4972 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
4973 return email_to_user($user, $supportuser, $subject, $message);
4978 * Send email to specified user with confirmation text and activation link.
4980 * @global object
4981 * @param user $user A {@link $USER} object
4982 * @return bool Returns true if mail was sent OK and false if there was an error.
4984 function send_confirmation_email($user) {
4985 global $CFG;
4987 $site = get_site();
4988 $supportuser = generate_email_supportuser();
4990 $data = new stdClass();
4991 $data->firstname = fullname($user);
4992 $data->sitename = format_string($site->fullname);
4993 $data->admin = generate_email_signoff();
4995 $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname));
4997 $username = urlencode($user->username);
4998 $username = str_replace('.', '%2E', $username); // prevent problems with trailing dots
4999 $data->link = $CFG->wwwroot .'/login/confirm.php?data='. $user->secret .'/'. $username;
5000 $message = get_string('emailconfirmation', '', $data);
5001 $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true);
5003 $user->mailformat = 1; // Always send HTML version as well
5005 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5006 return email_to_user($user, $supportuser, $subject, $message, $messagehtml);
5011 * send_password_change_confirmation_email.
5013 * @global object
5014 * @param user $user A {@link $USER} object
5015 * @return bool Returns true if mail was sent OK and false if there was an error.
5017 function send_password_change_confirmation_email($user) {
5018 global $CFG;
5020 $site = get_site();
5021 $supportuser = generate_email_supportuser();
5023 $data = new stdClass();
5024 $data->firstname = $user->firstname;
5025 $data->lastname = $user->lastname;
5026 $data->sitename = format_string($site->fullname);
5027 $data->link = $CFG->httpswwwroot .'/login/forgot_password.php?p='. $user->secret .'&s='. urlencode($user->username);
5028 $data->admin = generate_email_signoff();
5030 $message = get_string('emailpasswordconfirmation', '', $data);
5031 $subject = get_string('emailpasswordconfirmationsubject', '', format_string($site->fullname));
5033 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5034 return email_to_user($user, $supportuser, $subject, $message);
5039 * send_password_change_info.
5041 * @global object
5042 * @param user $user A {@link $USER} object
5043 * @return bool Returns true if mail was sent OK and false if there was an error.
5045 function send_password_change_info($user) {
5046 global $CFG;
5048 $site = get_site();
5049 $supportuser = generate_email_supportuser();
5050 $systemcontext = get_context_instance(CONTEXT_SYSTEM);
5052 $data = new stdClass();
5053 $data->firstname = $user->firstname;
5054 $data->lastname = $user->lastname;
5055 $data->sitename = format_string($site->fullname);
5056 $data->admin = generate_email_signoff();
5058 $userauth = get_auth_plugin($user->auth);
5060 if (!is_enabled_auth($user->auth) or $user->auth == 'nologin') {
5061 $message = get_string('emailpasswordchangeinfodisabled', '', $data);
5062 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
5063 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5064 return email_to_user($user, $supportuser, $subject, $message);
5067 if ($userauth->can_change_password() and $userauth->change_password_url()) {
5068 // we have some external url for password changing
5069 $data->link .= $userauth->change_password_url();
5071 } else {
5072 //no way to change password, sorry
5073 $data->link = '';
5076 if (!empty($data->link) and has_capability('moodle/user:changeownpassword', $systemcontext, $user->id)) {
5077 $message = get_string('emailpasswordchangeinfo', '', $data);
5078 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
5079 } else {
5080 $message = get_string('emailpasswordchangeinfofail', '', $data);
5081 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
5084 //directly email rather than using the messaging system to ensure its not routed to a popup or jabber
5085 return email_to_user($user, $supportuser, $subject, $message);
5090 * Check that an email is allowed. It returns an error message if there
5091 * was a problem.
5093 * @global object
5094 * @param string $email Content of email
5095 * @return string|false
5097 function email_is_not_allowed($email) {
5098 global $CFG;
5100 if (!empty($CFG->allowemailaddresses)) {
5101 $allowed = explode(' ', $CFG->allowemailaddresses);
5102 foreach ($allowed as $allowedpattern) {
5103 $allowedpattern = trim($allowedpattern);
5104 if (!$allowedpattern) {
5105 continue;
5107 if (strpos($allowedpattern, '.') === 0) {
5108 if (strpos(strrev($email), strrev($allowedpattern)) === 0) {
5109 // subdomains are in a form ".example.com" - matches "xxx@anything.example.com"
5110 return false;
5113 } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) { // Match! (bug 5250)
5114 return false;
5117 return get_string('emailonlyallowed', '', $CFG->allowemailaddresses);
5119 } else if (!empty($CFG->denyemailaddresses)) {
5120 $denied = explode(' ', $CFG->denyemailaddresses);
5121 foreach ($denied as $deniedpattern) {
5122 $deniedpattern = trim($deniedpattern);
5123 if (!$deniedpattern) {
5124 continue;
5126 if (strpos($deniedpattern, '.') === 0) {
5127 if (strpos(strrev($email), strrev($deniedpattern)) === 0) {
5128 // subdomains are in a form ".example.com" - matches "xxx@anything.example.com"
5129 return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
5132 } else if (strpos(strrev($email), strrev('@'.$deniedpattern)) === 0) { // Match! (bug 5250)
5133 return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
5138 return false;
5141 /// FILE HANDLING /////////////////////////////////////////////
5144 * Returns local file storage instance
5146 * @return file_storage
5148 function get_file_storage() {
5149 global $CFG;
5151 static $fs = null;
5153 if ($fs) {
5154 return $fs;
5157 require_once("$CFG->libdir/filelib.php");
5159 if (isset($CFG->filedir)) {
5160 $filedir = $CFG->filedir;
5161 } else {
5162 $filedir = $CFG->dataroot.'/filedir';
5165 if (isset($CFG->trashdir)) {
5166 $trashdirdir = $CFG->trashdir;
5167 } else {
5168 $trashdirdir = $CFG->dataroot.'/trashdir';
5171 $fs = new file_storage($filedir, $trashdirdir, "$CFG->dataroot/temp/filestorage", $CFG->directorypermissions, $CFG->filepermissions);
5173 return $fs;
5177 * Returns local file storage instance
5179 * @return file_browser
5181 function get_file_browser() {
5182 global $CFG;
5184 static $fb = null;
5186 if ($fb) {
5187 return $fb;
5190 require_once("$CFG->libdir/filelib.php");
5192 $fb = new file_browser();
5194 return $fb;
5198 * Returns file packer
5200 * @param string $mimetype default application/zip
5201 * @return file_packer
5203 function get_file_packer($mimetype='application/zip') {
5204 global $CFG;
5206 static $fp = array();;
5208 if (isset($fp[$mimetype])) {
5209 return $fp[$mimetype];
5212 switch ($mimetype) {
5213 case 'application/zip':
5214 case 'application/vnd.moodle.backup':
5215 $classname = 'zip_packer';
5216 break;
5217 case 'application/x-tar':
5218 // $classname = 'tar_packer';
5219 // break;
5220 default:
5221 return false;
5224 require_once("$CFG->libdir/filestorage/$classname.php");
5225 $fp[$mimetype] = new $classname();
5227 return $fp[$mimetype];
5231 * Returns current name of file on disk if it exists.
5233 * @param string $newfile File to be verified
5234 * @return string Current name of file on disk if true
5236 function valid_uploaded_file($newfile) {
5237 if (empty($newfile)) {
5238 return '';
5240 if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) {
5241 return $newfile['tmp_name'];
5242 } else {
5243 return '';
5248 * Returns the maximum size for uploading files.
5250 * There are seven possible upload limits:
5251 * 1. in Apache using LimitRequestBody (no way of checking or changing this)
5252 * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
5253 * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
5254 * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
5255 * 5. by the Moodle admin in $CFG->maxbytes
5256 * 6. by the teacher in the current course $course->maxbytes
5257 * 7. by the teacher for the current module, eg $assignment->maxbytes
5259 * These last two are passed to this function as arguments (in bytes).
5260 * Anything defined as 0 is ignored.
5261 * The smallest of all the non-zero numbers is returned.
5263 * @todo Finish documenting this function
5265 * @param int $sizebytes Set maximum size
5266 * @param int $coursebytes Current course $course->maxbytes (in bytes)
5267 * @param int $modulebytes Current module ->maxbytes (in bytes)
5268 * @return int The maximum size for uploading files.
5270 function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0) {
5272 if (! $filesize = ini_get('upload_max_filesize')) {
5273 $filesize = '5M';
5275 $minimumsize = get_real_size($filesize);
5277 if ($postsize = ini_get('post_max_size')) {
5278 $postsize = get_real_size($postsize);
5279 if ($postsize < $minimumsize) {
5280 $minimumsize = $postsize;
5284 if ($sitebytes and $sitebytes < $minimumsize) {
5285 $minimumsize = $sitebytes;
5288 if ($coursebytes and $coursebytes < $minimumsize) {
5289 $minimumsize = $coursebytes;
5292 if ($modulebytes and $modulebytes < $minimumsize) {
5293 $minimumsize = $modulebytes;
5296 return $minimumsize;
5300 * Returns an array of possible sizes in local language
5302 * Related to {@link get_max_upload_file_size()} - this function returns an
5303 * array of possible sizes in an array, translated to the
5304 * local language.
5306 * @todo Finish documenting this function
5308 * @global object
5309 * @uses SORT_NUMERIC
5310 * @param int $sizebytes Set maximum size
5311 * @param int $coursebytes Current course $course->maxbytes (in bytes)
5312 * @param int $modulebytes Current module ->maxbytes (in bytes)
5313 * @return array
5315 function get_max_upload_sizes($sitebytes=0, $coursebytes=0, $modulebytes=0) {
5316 global $CFG;
5318 if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) {
5319 return array();
5322 $filesize[intval($maxsize)] = display_size($maxsize);
5324 $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152,
5325 5242880, 10485760, 20971520, 52428800, 104857600);
5327 // Allow maxbytes to be selected if it falls outside the above boundaries
5328 if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) {
5329 // note: get_real_size() is used in order to prevent problems with invalid values
5330 $sizelist[] = get_real_size($CFG->maxbytes);
5333 foreach ($sizelist as $sizebytes) {
5334 if ($sizebytes < $maxsize) {
5335 $filesize[intval($sizebytes)] = display_size($sizebytes);
5339 krsort($filesize, SORT_NUMERIC);
5341 return $filesize;
5345 * Returns an array with all the filenames in all subdirectories, relative to the given rootdir.
5347 * If excludefiles is defined, then that file/directory is ignored
5348 * If getdirs is true, then (sub)directories are included in the output
5349 * If getfiles is true, then files are included in the output
5350 * (at least one of these must be true!)
5352 * @todo Finish documenting this function. Add examples of $excludefile usage.
5354 * @param string $rootdir A given root directory to start from
5355 * @param string|array $excludefile If defined then the specified file/directory is ignored
5356 * @param bool $descend If true then subdirectories are recursed as well
5357 * @param bool $getdirs If true then (sub)directories are included in the output
5358 * @param bool $getfiles If true then files are included in the output
5359 * @return array An array with all the filenames in
5360 * all subdirectories, relative to the given rootdir
5362 function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) {
5364 $dirs = array();
5366 if (!$getdirs and !$getfiles) { // Nothing to show
5367 return $dirs;
5370 if (!is_dir($rootdir)) { // Must be a directory
5371 return $dirs;
5374 if (!$dir = opendir($rootdir)) { // Can't open it for some reason
5375 return $dirs;
5378 if (!is_array($excludefiles)) {
5379 $excludefiles = array($excludefiles);
5382 while (false !== ($file = readdir($dir))) {
5383 $firstchar = substr($file, 0, 1);
5384 if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) {
5385 continue;
5387 $fullfile = $rootdir .'/'. $file;
5388 if (filetype($fullfile) == 'dir') {
5389 if ($getdirs) {
5390 $dirs[] = $file;
5392 if ($descend) {
5393 $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles);
5394 foreach ($subdirs as $subdir) {
5395 $dirs[] = $file .'/'. $subdir;
5398 } else if ($getfiles) {
5399 $dirs[] = $file;
5402 closedir($dir);
5404 asort($dirs);
5406 return $dirs;
5411 * Adds up all the files in a directory and works out the size.
5413 * @todo Finish documenting this function
5415 * @param string $rootdir The directory to start from
5416 * @param string $excludefile A file to exclude when summing directory size
5417 * @return int The summed size of all files and subfiles within the root directory
5419 function get_directory_size($rootdir, $excludefile='') {
5420 global $CFG;
5422 // do it this way if we can, it's much faster
5423 if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) {
5424 $command = trim($CFG->pathtodu).' -sk '.escapeshellarg($rootdir);
5425 $output = null;
5426 $return = null;
5427 exec($command,$output,$return);
5428 if (is_array($output)) {
5429 return get_real_size(intval($output[0]).'k'); // we told it to return k.
5433 if (!is_dir($rootdir)) { // Must be a directory
5434 return 0;
5437 if (!$dir = @opendir($rootdir)) { // Can't open it for some reason
5438 return 0;
5441 $size = 0;
5443 while (false !== ($file = readdir($dir))) {
5444 $firstchar = substr($file, 0, 1);
5445 if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) {
5446 continue;
5448 $fullfile = $rootdir .'/'. $file;
5449 if (filetype($fullfile) == 'dir') {
5450 $size += get_directory_size($fullfile, $excludefile);
5451 } else {
5452 $size += filesize($fullfile);
5455 closedir($dir);
5457 return $size;
5461 * Converts bytes into display form
5463 * @todo Finish documenting this function. Verify return type.
5465 * @staticvar string $gb Localized string for size in gigabytes
5466 * @staticvar string $mb Localized string for size in megabytes
5467 * @staticvar string $kb Localized string for size in kilobytes
5468 * @staticvar string $b Localized string for size in bytes
5469 * @param int $size The size to convert to human readable form
5470 * @return string
5472 function display_size($size) {
5474 static $gb, $mb, $kb, $b;
5476 if (empty($gb)) {
5477 $gb = get_string('sizegb');
5478 $mb = get_string('sizemb');
5479 $kb = get_string('sizekb');
5480 $b = get_string('sizeb');
5483 if ($size >= 1073741824) {
5484 $size = round($size / 1073741824 * 10) / 10 . $gb;
5485 } else if ($size >= 1048576) {
5486 $size = round($size / 1048576 * 10) / 10 . $mb;
5487 } else if ($size >= 1024) {
5488 $size = round($size / 1024 * 10) / 10 . $kb;
5489 } else {
5490 $size = intval($size) .' '. $b; // file sizes over 2GB can not work in 32bit PHP anyway
5492 return $size;
5496 * Cleans a given filename by removing suspicious or troublesome characters
5497 * @see clean_param()
5499 * @uses PARAM_FILE
5500 * @param string $string file name
5501 * @return string cleaned file name
5503 function clean_filename($string) {
5504 return clean_param($string, PARAM_FILE);
5508 /// STRING TRANSLATION ////////////////////////////////////////
5511 * Returns the code for the current language
5513 * @return string
5515 function current_language() {
5516 global $CFG, $USER, $SESSION, $COURSE;
5518 if (!empty($COURSE->id) and $COURSE->id != SITEID and !empty($COURSE->lang)) { // Course language can override all other settings for this page
5519 $return = $COURSE->lang;
5521 } else if (!empty($SESSION->lang)) { // Session language can override other settings
5522 $return = $SESSION->lang;
5524 } else if (!empty($USER->lang)) {
5525 $return = $USER->lang;
5527 } else if (isset($CFG->lang)) {
5528 $return = $CFG->lang;
5530 } else {
5531 $return = 'en';
5534 $return = str_replace('_utf8', '', $return); // Just in case this slipped in from somewhere by accident
5536 return $return;
5540 * Returns parent language of current active language if defined
5542 * @uses COURSE
5543 * @uses SESSION
5544 * @param string $lang null means current language
5545 * @return string
5547 function get_parent_language($lang=null) {
5548 global $COURSE, $SESSION;
5550 //let's hack around the current language
5551 if (!empty($lang)) {
5552 $old_course_lang = empty($COURSE->lang) ? '' : $COURSE->lang;
5553 $old_session_lang = empty($SESSION->lang) ? '' : $SESSION->lang;
5554 $COURSE->lang = '';
5555 $SESSION->lang = $lang;
5558 $parentlang = get_string('parentlanguage', 'langconfig');
5559 if ($parentlang === 'en') {
5560 $parentlang = '';
5563 //let's hack around the current language
5564 if (!empty($lang)) {
5565 $COURSE->lang = $old_course_lang;
5566 $SESSION->lang = $old_session_lang;
5569 return $parentlang;
5573 * Returns current string_manager instance.
5575 * The param $forcereload is needed for CLI installer only where the string_manager instance
5576 * must be replaced during the install.php script life time.
5578 * @param bool $forcereload shall the singleton be released and new instance created instead?
5579 * @return string_manager
5581 function get_string_manager($forcereload=false) {
5582 global $CFG;
5584 static $singleton = null;
5586 if ($forcereload) {
5587 $singleton = null;
5589 if ($singleton === null) {
5590 if (empty($CFG->early_install_lang)) {
5592 if (empty($CFG->langcacheroot)) {
5593 $langcacheroot = $CFG->dataroot . '/cache/lang';
5594 } else {
5595 $langcacheroot = $CFG->langcacheroot;
5598 if (empty($CFG->langlist)) {
5599 $translist = array();
5600 } else {
5601 $translist = explode(',', $CFG->langlist);
5604 if (empty($CFG->langmenucachefile)) {
5605 $langmenucache = $CFG->dataroot . '/cache/languages';
5606 } else {
5607 $langmenucache = $CFG->langmenucachefile;
5610 $singleton = new core_string_manager($CFG->langotherroot, $CFG->langlocalroot, $langcacheroot,
5611 !empty($CFG->langstringcache), $translist, $langmenucache);
5613 } else {
5614 $singleton = new install_string_manager();
5618 return $singleton;
5623 * Interface describing class which is responsible for getting
5624 * of localised strings from language packs.
5626 * @package moodlecore
5627 * @copyright 2010 Petr Skoda (http://skodak.org)
5628 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5630 interface string_manager {
5632 * Get String returns a requested string
5634 * @param string $identifier The identifier of the string to search for
5635 * @param string $component The module the string is associated with
5636 * @param string|object|array $a An object, string or number that can be used
5637 * within translation strings
5638 * @param string $lang moodle translation language, NULL means use current
5639 * @return string The String !
5641 public function get_string($identifier, $component = '', $a = NULL, $lang = NULL);
5644 * Does the string actually exist?
5646 * get_string() is throwing debug warnings, sometimes we do not want them
5647 * or we want to display better explanation of the problem.
5649 * Use with care!
5651 * @param string $identifier The identifier of the string to search for
5652 * @param string $component The module the string is associated with
5653 * @return boot true if exists
5655 public function string_exists($identifier, $component);
5658 * Returns a localised list of all country names, sorted by country keys.
5659 * @param bool $returnall return all or just enabled
5660 * @param string $lang moodle translation language, NULL means use current
5661 * @return array two-letter country code => translated name.
5663 public function get_list_of_countries($returnall = false, $lang = NULL);
5666 * Returns a localised list of languages, sorted by code keys.
5668 * @param string $lang moodle translation language, NULL means use current
5669 * @param string $standard language list standard
5670 * iso6392: three-letter language code (ISO 639-2/T) => translated name.
5671 * @return array language code => translated name
5673 public function get_list_of_languages($lang = NULL, $standard = 'iso6392');
5676 * Does the translation exist?
5678 * @param string $lang moodle translation language code
5679 * @param bool include also disabled translations?
5680 * @return boot true if exists
5682 public function translation_exists($lang, $includeall = true);
5685 * Returns localised list of installed translations
5686 * @param bool $returnall return all or just enabled
5687 * @return array moodle translation code => localised translation name
5689 public function get_list_of_translations($returnall = false);
5692 * Returns localised list of currencies.
5694 * @param string $lang moodle translation language, NULL means use current
5695 * @return array currency code => localised currency name
5697 public function get_list_of_currencies($lang = NULL);
5700 * Load all strings for one component
5701 * @param string $component The module the string is associated with
5702 * @param string $lang
5703 * @param bool $disablecache Do not use caches, force fetching the strings from sources
5704 * @param bool $disablelocal Do not use customized strings in xx_local language packs
5705 * @return array of all string for given component and lang
5707 public function load_component_strings($component, $lang, $disablecache=false, $disablelocal=false);
5710 * Invalidates all caches, should the implementation use any
5712 public function reset_caches();
5717 * Standard string_manager implementation
5719 * @package moodlecore
5720 * @copyright 2010 Petr Skoda (http://skodak.org)
5721 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5723 class core_string_manager implements string_manager {
5724 /** @var string location of all packs except 'en' */
5725 protected $otherroot;
5726 /** @var string location of all lang pack local modifications */
5727 protected $localroot;
5728 /** @var string location of on-disk cache of merged strings */
5729 protected $cacheroot;
5730 /** @var array lang string cache - it will be optimised more later */
5731 protected $cache = array();
5732 /** @var int get_string() counter */
5733 protected $countgetstring = 0;
5734 /** @var int in-memory cache hits counter */
5735 protected $countmemcache = 0;
5736 /** @var int on-disk cache hits counter */
5737 protected $countdiskcache = 0;
5738 /** @var bool use disk cache */
5739 protected $usediskcache;
5740 /* @var array limit list of translations */
5741 protected $translist;
5742 /** @var string location of a file that caches the list of available translations */
5743 protected $menucache;
5746 * Create new instance of string manager
5748 * @param string $otherroot location of downlaoded lang packs - usually $CFG->dataroot/lang
5749 * @param string $localroot usually the same as $otherroot
5750 * @param string $cacheroot usually lang dir in cache folder
5751 * @param bool $usediskcache use disk cache
5752 * @param array $translist limit list of visible translations
5753 * @param string $menucache the location of a file that caches the list of available translations
5755 public function __construct($otherroot, $localroot, $cacheroot, $usediskcache, $translist, $menucache) {
5756 $this->otherroot = $otherroot;
5757 $this->localroot = $localroot;
5758 $this->cacheroot = $cacheroot;
5759 $this->usediskcache = $usediskcache;
5760 $this->translist = $translist;
5761 $this->menucache = $menucache;
5765 * Returns dependencies of current language, en is not included.
5766 * @param string $lang
5767 * @return array all parents, the lang itself is last
5769 public function get_language_dependencies($lang) {
5770 if ($lang === 'en') {
5771 return array();
5773 if (!file_exists("$this->otherroot/$lang/langconfig.php")) {
5774 return array();
5776 $string = array();
5777 include("$this->otherroot/$lang/langconfig.php");
5779 if (empty($string['parentlanguage'])) {
5780 return array($lang);
5781 } else {
5782 $parentlang = $string['parentlanguage'];
5783 unset($string);
5784 return array_merge($this->get_language_dependencies($parentlang), array($lang));
5789 * Load all strings for one component
5790 * @param string $component The module the string is associated with
5791 * @param string $lang
5792 * @param bool $disablecache Do not use caches, force fetching the strings from sources
5793 * @param bool $disablelocal Do not use customized strings in xx_local language packs
5794 * @return array of all string for given component and lang
5796 public function load_component_strings($component, $lang, $disablecache=false, $disablelocal=false) {
5797 global $CFG;
5799 list($plugintype, $pluginname) = normalize_component($component);
5800 if ($plugintype == 'core' and is_null($pluginname)) {
5801 $component = 'core';
5802 } else {
5803 $component = $plugintype . '_' . $pluginname;
5806 if (!$disablecache) {
5807 // try in-memory cache first
5808 if (isset($this->cache[$lang][$component])) {
5809 $this->countmemcache++;
5810 return $this->cache[$lang][$component];
5813 // try on-disk cache then
5814 if ($this->usediskcache and file_exists($this->cacheroot . "/$lang/$component.php")) {
5815 $this->countdiskcache++;
5816 include($this->cacheroot . "/$lang/$component.php");
5817 return $this->cache[$lang][$component];
5821 // no cache found - let us merge all possible sources of the strings
5822 if ($plugintype === 'core') {
5823 $file = $pluginname;
5824 if ($file === null) {
5825 $file = 'moodle';
5827 $string = array();
5828 // first load english pack
5829 if (!file_exists("$CFG->dirroot/lang/en/$file.php")) {
5830 return array();
5832 include("$CFG->dirroot/lang/en/$file.php");
5833 $originalkeys = array_keys($string);
5834 $originalkeys = array_flip($originalkeys);
5836 // and then corresponding local if present and allowed
5837 if (!$disablelocal and file_exists("$this->localroot/en_local/$file.php")) {
5838 include("$this->localroot/en_local/$file.php");
5840 // now loop through all langs in correct order
5841 $deps = $this->get_language_dependencies($lang);
5842 foreach ($deps as $dep) {
5843 // the main lang string location
5844 if (file_exists("$this->otherroot/$dep/$file.php")) {
5845 include("$this->otherroot/$dep/$file.php");
5847 if (!$disablelocal and file_exists("$this->localroot/{$dep}_local/$file.php")) {
5848 include("$this->localroot/{$dep}_local/$file.php");
5852 } else {
5853 if (!$location = get_plugin_directory($plugintype, $pluginname) or !is_dir($location)) {
5854 return array();
5856 if ($plugintype === 'mod') {
5857 // bloody mod hack
5858 $file = $pluginname;
5859 } else {
5860 $file = $plugintype . '_' . $pluginname;
5862 $string = array();
5863 // first load English pack
5864 if (!file_exists("$location/lang/en/$file.php")) {
5865 //English pack does not exist, so do not try to load anything else
5866 return array();
5868 include("$location/lang/en/$file.php");
5869 $originalkeys = array_keys($string);
5870 $originalkeys = array_flip($originalkeys);
5871 // and then corresponding local english if present
5872 if (!$disablelocal and file_exists("$this->localroot/en_local/$file.php")) {
5873 include("$this->localroot/en_local/$file.php");
5876 // now loop through all langs in correct order
5877 $deps = $this->get_language_dependencies($lang);
5878 foreach ($deps as $dep) {
5879 // legacy location - used by contrib only
5880 if (file_exists("$location/lang/$dep/$file.php")) {
5881 include("$location/lang/$dep/$file.php");
5883 // the main lang string location
5884 if (file_exists("$this->otherroot/$dep/$file.php")) {
5885 include("$this->otherroot/$dep/$file.php");
5887 // local customisations
5888 if (!$disablelocal and file_exists("$this->localroot/{$dep}_local/$file.php")) {
5889 include("$this->localroot/{$dep}_local/$file.php");
5894 // we do not want any extra strings from other languages - everything must be in en lang pack
5895 $string = array_intersect_key($string, $originalkeys);
5897 // now we have a list of strings from all possible sources. put it into both in-memory and on-disk
5898 // caches so we do not need to do all this merging and dependencies resolving again
5899 $this->cache[$lang][$component] = $string;
5900 if ($this->usediskcache) {
5901 check_dir_exists("$this->cacheroot/$lang");
5902 file_put_contents("$this->cacheroot/$lang/$component.php", "<?php \$this->cache['$lang']['$component'] = ".var_export($string, true).";");
5904 return $string;
5908 * Does the string actually exist?
5910 * get_string() is throwing debug warnings, sometimes we do not want them
5911 * or we want to display better explanation of the problem.
5913 * Use with care!
5915 * @param string $identifier The identifier of the string to search for
5916 * @param string $component The module the string is associated with
5917 * @return boot true if exists
5919 public function string_exists($identifier, $component) {
5920 $identifier = clean_param($identifier, PARAM_STRINGID);
5921 if (empty($identifier)) {
5922 return false;
5924 $lang = current_language();
5925 $string = $this->load_component_strings($component, $lang);
5926 return isset($string[$identifier]);
5930 * Get String returns a requested string
5932 * @param string $identifier The identifier of the string to search for
5933 * @param string $component The module the string is associated with
5934 * @param string|object|array $a An object, string or number that can be used
5935 * within translation strings
5936 * @param string $lang moodle translation language, NULL means use current
5937 * @return string The String !
5939 public function get_string($identifier, $component = '', $a = NULL, $lang = NULL) {
5940 $this->countgetstring++;
5941 // there are very many uses of these time formating strings without the 'langconfig' component,
5942 // it would not be reasonable to expect that all of them would be converted during 2.0 migration
5943 static $langconfigstrs = array(
5944 'strftimedate' => 1,
5945 'strftimedatefullshort' => 1,
5946 'strftimedateshort' => 1,
5947 'strftimedatetime' => 1,
5948 'strftimedatetimeshort' => 1,
5949 'strftimedaydate' => 1,
5950 'strftimedaydatetime' => 1,
5951 'strftimedayshort' => 1,
5952 'strftimedaytime' => 1,
5953 'strftimemonthyear' => 1,
5954 'strftimerecent' => 1,
5955 'strftimerecentfull' => 1,
5956 'strftimetime' => 1);
5958 if (empty($component)) {
5959 if (isset($langconfigstrs[$identifier])) {
5960 $component = 'langconfig';
5961 } else {
5962 $component = 'moodle';
5966 if ($lang === NULL) {
5967 $lang = current_language();
5970 $string = $this->load_component_strings($component, $lang);
5972 if (!isset($string[$identifier])) {
5973 if ($component === 'pix' or $component === 'core_pix') {
5974 // this component contains only alt tags for emoticons,
5975 // not all of them are supposed to be defined
5976 return '';
5978 if ($identifier === 'parentlanguage' and ($component === 'langconfig' or $component === 'core_langconfig')) {
5979 // parentlanguage is a special string, undefined means use English if not defined
5980 return 'en';
5982 if ($this->usediskcache) {
5983 // maybe the on-disk cache is dirty - let the last attempt be to find the string in original sources
5984 $string = $this->load_component_strings($component, $lang, true);
5986 if (!isset($string[$identifier])) {
5987 // the string is still missing - should be fixed by developer
5988 debugging("Invalid get_string() identifier: '$identifier' or component '$component'", DEBUG_DEVELOPER);
5989 return "[[$identifier]]";
5993 $string = $string[$identifier];
5995 if ($a !== NULL) {
5996 if (is_object($a) or is_array($a)) {
5997 $a = (array)$a;
5998 $search = array();
5999 $replace = array();
6000 foreach ($a as $key=>$value) {
6001 if (is_int($key)) {
6002 // we do not support numeric keys - sorry!
6003 continue;
6005 if (is_object($value) or is_array($value)) {
6006 // we support just string as value
6007 continue;
6009 $search[] = '{$a->'.$key.'}';
6010 $replace[] = (string)$value;
6012 if ($search) {
6013 $string = str_replace($search, $replace, $string);
6015 } else {
6016 $string = str_replace('{$a}', (string)$a, $string);
6020 return $string;
6024 * Returns information about the string_manager performance
6025 * @return array
6027 public function get_performance_summary() {
6028 return array(array(
6029 'langcountgetstring' => $this->countgetstring,
6030 'langcountmemcache' => $this->countmemcache,
6031 'langcountdiskcache' => $this->countdiskcache,
6032 ), array(
6033 'langcountgetstring' => 'get_string calls',
6034 'langcountmemcache' => 'strings mem cache hits',
6035 'langcountdiskcache' => 'strings disk cache hits',
6040 * Returns a localised list of all country names, sorted by localised name.
6042 * @param bool $returnall return all or just enabled
6043 * @param string $lang moodle translation language, NULL means use current
6044 * @return array two-letter country code => translated name.
6046 public function get_list_of_countries($returnall = false, $lang = NULL) {
6047 global $CFG;
6049 if ($lang === NULL) {
6050 $lang = current_language();
6053 $countries = $this->load_component_strings('core_countries', $lang);
6054 textlib_get_instance()->asort($countries);
6055 if (!$returnall and !empty($CFG->allcountrycodes)) {
6056 $enabled = explode(',', $CFG->allcountrycodes);
6057 $return = array();
6058 foreach ($enabled as $c) {
6059 if (isset($countries[$c])) {
6060 $return[$c] = $countries[$c];
6063 return $return;
6066 return $countries;
6070 * Returns a localised list of languages, sorted by code keys.
6072 * @param string $lang moodle translation language, NULL means use current
6073 * @param string $standard language list standard
6074 * - iso6392: three-letter language code (ISO 639-2/T) => translated name
6075 * - iso6391: two-letter langauge code (ISO 639-1) => translated name
6076 * @return array language code => translated name
6078 public function get_list_of_languages($lang = NULL, $standard = 'iso6391') {
6079 if ($lang === NULL) {
6080 $lang = current_language();
6083 if ($standard === 'iso6392') {
6084 $langs = $this->load_component_strings('core_iso6392', $lang);
6085 ksort($langs);
6086 return $langs;
6088 } else if ($standard === 'iso6391') {
6089 $langs2 = $this->load_component_strings('core_iso6392', $lang);
6090 static $mapping = array('aar' => 'aa', 'abk' => 'ab', 'afr' => 'af', 'aka' => 'ak', 'sqi' => 'sq', 'amh' => 'am', 'ara' => 'ar', 'arg' => 'an', 'hye' => 'hy',
6091 'asm' => 'as', 'ava' => 'av', 'ave' => 'ae', 'aym' => 'ay', 'aze' => 'az', 'bak' => 'ba', 'bam' => 'bm', 'eus' => 'eu', 'bel' => 'be', 'ben' => 'bn', 'bih' => 'bh',
6092 'bis' => 'bi', 'bos' => 'bs', 'bre' => 'br', 'bul' => 'bg', 'mya' => 'my', 'cat' => 'ca', 'cha' => 'ch', 'che' => 'ce', 'zho' => 'zh', 'chu' => 'cu', 'chv' => 'cv',
6093 'cor' => 'kw', 'cos' => 'co', 'cre' => 'cr', 'ces' => 'cs', 'dan' => 'da', 'div' => 'dv', 'nld' => 'nl', 'dzo' => 'dz', 'eng' => 'en', 'epo' => 'eo', 'est' => 'et',
6094 'ewe' => 'ee', 'fao' => 'fo', 'fij' => 'fj', 'fin' => 'fi', 'fra' => 'fr', 'fry' => 'fy', 'ful' => 'ff', 'kat' => 'ka', 'deu' => 'de', 'gla' => 'gd', 'gle' => 'ga',
6095 'glg' => 'gl', 'glv' => 'gv', 'ell' => 'el', 'grn' => 'gn', 'guj' => 'gu', 'hat' => 'ht', 'hau' => 'ha', 'heb' => 'he', 'her' => 'hz', 'hin' => 'hi', 'hmo' => 'ho',
6096 'hrv' => 'hr', 'hun' => 'hu', 'ibo' => 'ig', 'isl' => 'is', 'ido' => 'io', 'iii' => 'ii', 'iku' => 'iu', 'ile' => 'ie', 'ina' => 'ia', 'ind' => 'id', 'ipk' => 'ik',
6097 'ita' => 'it', 'jav' => 'jv', 'jpn' => 'ja', 'kal' => 'kl', 'kan' => 'kn', 'kas' => 'ks', 'kau' => 'kr', 'kaz' => 'kk', 'khm' => 'km', 'kik' => 'ki', 'kin' => 'rw',
6098 'kir' => 'ky', 'kom' => 'kv', 'kon' => 'kg', 'kor' => 'ko', 'kua' => 'kj', 'kur' => 'ku', 'lao' => 'lo', 'lat' => 'la', 'lav' => 'lv', 'lim' => 'li', 'lin' => 'ln',
6099 'lit' => 'lt', 'ltz' => 'lb', 'lub' => 'lu', 'lug' => 'lg', 'mkd' => 'mk', 'mah' => 'mh', 'mal' => 'ml', 'mri' => 'mi', 'mar' => 'mr', 'msa' => 'ms', 'mlg' => 'mg',
6100 'mlt' => 'mt', 'mon' => 'mn', 'nau' => 'na', 'nav' => 'nv', 'nbl' => 'nr', 'nde' => 'nd', 'ndo' => 'ng', 'nep' => 'ne', 'nno' => 'nn', 'nob' => 'nb', 'nor' => 'no',
6101 'nya' => 'ny', 'oci' => 'oc', 'oji' => 'oj', 'ori' => 'or', 'orm' => 'om', 'oss' => 'os', 'pan' => 'pa', 'fas' => 'fa', 'pli' => 'pi', 'pol' => 'pl', 'por' => 'pt',
6102 'pus' => 'ps', 'que' => 'qu', 'roh' => 'rm', 'ron' => 'ro', 'run' => 'rn', 'rus' => 'ru', 'sag' => 'sg', 'san' => 'sa', 'sin' => 'si', 'slk' => 'sk', 'slv' => 'sl',
6103 'sme' => 'se', 'smo' => 'sm', 'sna' => 'sn', 'snd' => 'sd', 'som' => 'so', 'sot' => 'st', 'spa' => 'es', 'srd' => 'sc', 'srp' => 'sr', 'ssw' => 'ss', 'sun' => 'su',
6104 'swa' => 'sw', 'swe' => 'sv', 'tah' => 'ty', 'tam' => 'ta', 'tat' => 'tt', 'tel' => 'te', 'tgk' => 'tg', 'tgl' => 'tl', 'tha' => 'th', 'bod' => 'bo', 'tir' => 'ti',
6105 'ton' => 'to', 'tsn' => 'tn', 'tso' => 'ts', 'tuk' => 'tk', 'tur' => 'tr', 'twi' => 'tw', 'uig' => 'ug', 'ukr' => 'uk', 'urd' => 'ur', 'uzb' => 'uz', 'ven' => 've',
6106 'vie' => 'vi', 'vol' => 'vo', 'cym' => 'cy', 'wln' => 'wa', 'wol' => 'wo', 'xho' => 'xh', 'yid' => 'yi', 'yor' => 'yo', 'zha' => 'za', 'zul' => 'zu');
6107 $langs1 = array();
6108 foreach ($mapping as $c2=>$c1) {
6109 $langs1[$c1] = $langs2[$c2];
6111 ksort($langs1);
6112 return $langs1;
6114 } else {
6115 debugging('Unsupported $standard parameter in get_list_of_languages() method: '.$standard);
6118 return array();
6122 * Does the translation exist?
6124 * @param string $lang moodle translation language code
6125 * @param bool include also disabled translations?
6126 * @return boot true if exists
6128 public function translation_exists($lang, $includeall = true) {
6130 if (strpos($lang, '_local') !== false) {
6131 // _local packs are not real translations
6132 return false;
6134 if (!$includeall and !empty($this->translist)) {
6135 if (!in_array($lang, $this->translist)) {
6136 return false;
6139 if ($lang === 'en') {
6140 // part of distribution
6141 return true;
6143 return file_exists("$this->otherroot/$lang/langconfig.php");
6147 * Returns localised list of installed translations
6148 * @param bool $returnall return all or just enabled
6149 * @return array moodle translation code => localised translation name
6151 public function get_list_of_translations($returnall = false) {
6152 global $CFG;
6154 $languages = array();
6156 if (!empty($CFG->langcache) and is_readable($this->menucache)) {
6157 // try to re-use the cached list of all available languages
6158 $cachedlist = json_decode(file_get_contents($this->menucache), true);
6160 if (is_array($cachedlist) and !empty($cachedlist)) {
6161 // the cache file is restored correctly
6163 if (!$returnall and !empty($this->translist)) {
6164 // return just enabled translations
6165 foreach ($cachedlist as $langcode => $langname) {
6166 if (in_array($langcode, $this->translist)) {
6167 $languages[$langcode] = $langname;
6170 return $languages;
6172 } else {
6173 // return all translations
6174 return $cachedlist;
6179 // the cached list of languages is not available, let us populate the list
6181 if (!$returnall and !empty($this->translist)) {
6182 // return only some translations
6183 foreach ($this->translist as $lang) {
6184 $lang = trim($lang); //Just trim spaces to be a bit more permissive
6185 if (strstr($lang, '_local') !== false) {
6186 continue;
6188 if (strstr($lang, '_utf8') !== false) {
6189 continue;
6191 if ($lang !== 'en' and !file_exists("$this->otherroot/$lang/langconfig.php")) {
6192 // some broken or missing lang - can not switch to it anyway
6193 continue;
6195 $string = $this->load_component_strings('langconfig', $lang);
6196 if (!empty($string['thislanguage'])) {
6197 $languages[$lang] = $string['thislanguage'].' ('. $lang .')';
6199 unset($string);
6202 } else {
6203 // return all languages available in system
6204 $langdirs = get_list_of_plugins('', '', $this->otherroot);
6206 $langdirs = array_merge($langdirs, array("$CFG->dirroot/lang/en"=>'en'));
6207 // Sort all
6209 // Loop through all langs and get info
6210 foreach ($langdirs as $lang) {
6211 if (strstr($lang, '_local') !== false) {
6212 continue;
6214 if (strstr($lang, '_utf8') !== false) {
6215 continue;
6217 $string = $this->load_component_strings('langconfig', $lang);
6218 if (!empty($string['thislanguage'])) {
6219 $languages[$lang] = $string['thislanguage'].' ('. $lang .')';
6221 unset($string);
6224 if (!empty($CFG->langcache) and !empty($this->menucache)) {
6225 // cache the list so that it can be used next time
6226 textlib_get_instance()->asort($languages);
6227 file_put_contents($this->menucache, json_encode($languages));
6231 textlib_get_instance()->asort($languages);
6233 return $languages;
6237 * Returns localised list of currencies.
6239 * @param string $lang moodle translation language, NULL means use current
6240 * @return array currency code => localised currency name
6242 public function get_list_of_currencies($lang = NULL) {
6243 if ($lang === NULL) {
6244 $lang = current_language();
6247 $currencies = $this->load_component_strings('core_currencies', $lang);
6248 asort($currencies);
6250 return $currencies;
6254 * Clears both in-memory and on-disk caches
6256 public function reset_caches() {
6257 global $CFG;
6258 require_once("$CFG->libdir/filelib.php");
6260 // clear the on-disk disk with aggregated string files
6261 fulldelete($this->cacheroot);
6263 // clear the in-memory cache of loaded strings
6264 $this->cache = array();
6266 // clear the cache containing the list of available translations
6267 // and re-populate it again
6268 fulldelete($this->menucache);
6269 $this->get_list_of_translations(true);
6275 * Minimalistic string fetching implementation
6276 * that is used in installer before we fetch the wanted
6277 * language pack from moodle.org lang download site.
6279 * @package moodlecore
6280 * @copyright 2010 Petr Skoda (http://skodak.org)
6281 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6283 class install_string_manager implements string_manager {
6284 /** @var string location of pre-install packs for all langs */
6285 protected $installroot;
6288 * Crate new instance of install string manager
6290 public function __construct() {
6291 global $CFG;
6292 $this->installroot = "$CFG->dirroot/install/lang";
6296 * Load all strings for one component
6297 * @param string $component The module the string is associated with
6298 * @param string $lang
6299 * @param bool $disablecache Do not use caches, force fetching the strings from sources
6300 * @param bool $disablelocal Do not use customized strings in xx_local language packs
6301 * @return array of all string for given component and lang
6303 public function load_component_strings($component, $lang, $disablecache=false, $disablelocal=false) {
6304 // not needed in installer
6305 return array();
6309 * Does the string actually exist?
6311 * get_string() is throwing debug warnings, sometimes we do not want them
6312 * or we want to display better explanation of the problem.
6314 * Use with care!
6316 * @param string $identifier The identifier of the string to search for
6317 * @param string $component The module the string is associated with
6318 * @return boot true if exists
6320 public function string_exists($identifier, $component) {
6321 $identifier = clean_param($identifier, PARAM_STRINGID);
6322 if (empty($identifier)) {
6323 return false;
6325 // simple old style hack ;)
6326 $str = get_string($identifier, $component);
6327 return (strpos($str, '[[') === false);
6331 * Get String returns a requested string
6333 * @param string $identifier The identifier of the string to search for
6334 * @param string $component The module the string is associated with
6335 * @param string|object|array $a An object, string or number that can be used
6336 * within translation strings
6337 * @param string $lang moodle translation language, NULL means use current
6338 * @return string The String !
6340 public function get_string($identifier, $component = '', $a = NULL, $lang = NULL) {
6341 if (!$component) {
6342 $component = 'moodle';
6345 if ($lang === NULL) {
6346 $lang = current_language();
6349 //get parent lang
6350 $parent = '';
6351 if ($lang !== 'en' and $identifier !== 'parentlanguage' and $component !== 'langconfig') {
6352 if (file_exists("$this->installroot/$lang/langconfig.php")) {
6353 $string = array();
6354 include("$this->installroot/$lang/langconfig.php");
6355 if (isset($string['parentlanguage'])) {
6356 $parent = $string['parentlanguage'];
6358 unset($string);
6362 // include en string first
6363 if (!file_exists("$this->installroot/en/$component.php")) {
6364 return "[[$identifier]]";
6366 $string = array();
6367 include("$this->installroot/en/$component.php");
6369 // now override en with parent if defined
6370 if ($parent and $parent !== 'en' and file_exists("$this->installroot/$parent/$component.php")) {
6371 include("$this->installroot/$parent/$component.php");
6374 // finally override with requested language
6375 if ($lang !== 'en' and file_exists("$this->installroot/$lang/$component.php")) {
6376 include("$this->installroot/$lang/$component.php");
6379 if (!isset($string[$identifier])) {
6380 return "[[$identifier]]";
6383 $string = $string[$identifier];
6385 if ($a !== NULL) {
6386 if (is_object($a) or is_array($a)) {
6387 $a = (array)$a;
6388 $search = array();
6389 $replace = array();
6390 foreach ($a as $key=>$value) {
6391 if (is_int($key)) {
6392 // we do not support numeric keys - sorry!
6393 continue;
6395 $search[] = '{$a->'.$key.'}';
6396 $replace[] = (string)$value;
6398 if ($search) {
6399 $string = str_replace($search, $replace, $string);
6401 } else {
6402 $string = str_replace('{$a}', (string)$a, $string);
6406 return $string;
6410 * Returns a localised list of all country names, sorted by country keys.
6412 * @param bool $returnall return all or just enabled
6413 * @param string $lang moodle translation language, NULL means use current
6414 * @return array two-letter country code => translated name.
6416 public function get_list_of_countries($returnall = false, $lang = NULL) {
6417 //not used in installer
6418 return array();
6422 * Returns a localised list of languages, sorted by code keys.
6424 * @param string $lang moodle translation language, NULL means use current
6425 * @param string $standard language list standard
6426 * iso6392: three-letter language code (ISO 639-2/T) => translated name.
6427 * @return array language code => translated name
6429 public function get_list_of_languages($lang = NULL, $standard = 'iso6392') {
6430 //not used in installer
6431 return array();
6435 * Does the translation exist?
6437 * @param string $lang moodle translation language code
6438 * @param bool include also disabled translations?
6439 * @return boot true if exists
6441 public function translation_exists($lang, $includeall = true) {
6442 return file_exists($this->installroot.'/'.$lang.'/langconfig.php');
6446 * Returns localised list of installed translations
6447 * @param bool $returnall return all or just enabled
6448 * @return array moodle translation code => localised translation name
6450 public function get_list_of_translations($returnall = false) {
6451 // return all is ignored here - we need to know all langs in installer
6452 $languages = array();
6453 // Get raw list of lang directories
6454 $langdirs = get_list_of_plugins('install/lang');
6455 asort($langdirs);
6456 // Get some info from each lang
6457 foreach ($langdirs as $lang) {
6458 if (file_exists($this->installroot.'/'.$lang.'/langconfig.php')) {
6459 $string = array();
6460 include($this->installroot.'/'.$lang.'/langconfig.php');
6461 if (!empty($string['thislanguage'])) {
6462 $languages[$lang] = $string['thislanguage'].' ('.$lang.')';
6466 // Return array
6467 return $languages;
6471 * Returns localised list of currencies.
6473 * @param string $lang moodle translation language, NULL means use current
6474 * @return array currency code => localised currency name
6476 public function get_list_of_currencies($lang = NULL) {
6477 // not used in installer
6478 return array();
6482 * This implementation does not use any caches
6484 public function reset_caches() {}
6489 * Returns a localized string.
6491 * Returns the translated string specified by $identifier as
6492 * for $module. Uses the same format files as STphp.
6493 * $a is an object, string or number that can be used
6494 * within translation strings
6496 * eg 'hello {$a->firstname} {$a->lastname}'
6497 * or 'hello {$a}'
6499 * If you would like to directly echo the localized string use
6500 * the function {@link print_string()}
6502 * Example usage of this function involves finding the string you would
6503 * like a local equivalent of and using its identifier and module information
6504 * to retrieve it.<br/>
6505 * If you open moodle/lang/en/moodle.php and look near line 278
6506 * you will find a string to prompt a user for their word for 'course'
6507 * <code>
6508 * $string['course'] = 'Course';
6509 * </code>
6510 * So if you want to display the string 'Course'
6511 * in any language that supports it on your site
6512 * you just need to use the identifier 'course'
6513 * <code>
6514 * $mystring = '<strong>'. get_string('course') .'</strong>';
6515 * or
6516 * </code>
6517 * If the string you want is in another file you'd take a slightly
6518 * different approach. Looking in moodle/lang/en/calendar.php you find
6519 * around line 75:
6520 * <code>
6521 * $string['typecourse'] = 'Course event';
6522 * </code>
6523 * If you want to display the string "Course event" in any language
6524 * supported you would use the identifier 'typecourse' and the module 'calendar'
6525 * (because it is in the file calendar.php):
6526 * <code>
6527 * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>';
6528 * </code>
6530 * As a last resort, should the identifier fail to map to a string
6531 * the returned string will be [[ $identifier ]]
6533 * @param string $identifier The key identifier for the localized string
6534 * @param string $component The module where the key identifier is stored,
6535 * usually expressed as the filename in the language pack without the
6536 * .php on the end but can also be written as mod/forum or grade/export/xls.
6537 * If none is specified then moodle.php is used.
6538 * @param string|object|array $a An object, string or number that can be used
6539 * within translation strings
6540 * @return string The localized string.
6542 function get_string($identifier, $component = '', $a = NULL) {
6544 $identifier = clean_param($identifier, PARAM_STRINGID);
6545 if (empty($identifier)) {
6546 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');
6549 if (func_num_args() > 3) {
6550 debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.');
6553 if (strpos($component, '/') !== false) {
6554 debugging('The module name you passed to get_string is the deprecated format ' .
6555 'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER);
6556 $componentpath = explode('/', $component);
6558 switch ($componentpath[0]) {
6559 case 'mod':
6560 $component = $componentpath[1];
6561 break;
6562 case 'blocks':
6563 case 'block':
6564 $component = 'block_'.$componentpath[1];
6565 break;
6566 case 'enrol':
6567 $component = 'enrol_'.$componentpath[1];
6568 break;
6569 case 'format':
6570 $component = 'format_'.$componentpath[1];
6571 break;
6572 case 'grade':
6573 $component = 'grade'.$componentpath[1].'_'.$componentpath[2];
6574 break;
6578 return get_string_manager()->get_string($identifier, $component, $a);
6582 * Converts an array of strings to their localized value.
6584 * @param array $array An array of strings
6585 * @param string $module The language module that these strings can be found in.
6586 * @return array and array of translated strings.
6588 function get_strings($array, $component = '') {
6589 $string = new stdClass;
6590 foreach ($array as $item) {
6591 $string->$item = get_string($item, $component);
6593 return $string;
6597 * Prints out a translated string.
6599 * Prints out a translated string using the return value from the {@link get_string()} function.
6601 * Example usage of this function when the string is in the moodle.php file:<br/>
6602 * <code>
6603 * echo '<strong>';
6604 * print_string('course');
6605 * echo '</strong>';
6606 * </code>
6608 * Example usage of this function when the string is not in the moodle.php file:<br/>
6609 * <code>
6610 * echo '<h1>';
6611 * print_string('typecourse', 'calendar');
6612 * echo '</h1>';
6613 * </code>
6615 * @param string $identifier The key identifier for the localized string
6616 * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used.
6617 * @param mixed $a An object, string or number that can be used within translation strings
6619 function print_string($identifier, $component = '', $a = NULL) {
6620 echo get_string($identifier, $component, $a);
6624 * Returns a list of charset codes
6626 * Returns a list of charset codes. It's hardcoded, so they should be added manually
6627 * (checking that such charset is supported by the texlib library!)
6629 * @return array And associative array with contents in the form of charset => charset
6631 function get_list_of_charsets() {
6633 $charsets = array(
6634 'EUC-JP' => 'EUC-JP',
6635 'ISO-2022-JP'=> 'ISO-2022-JP',
6636 'ISO-8859-1' => 'ISO-8859-1',
6637 'SHIFT-JIS' => 'SHIFT-JIS',
6638 'GB2312' => 'GB2312',
6639 'GB18030' => 'GB18030', // gb18030 not supported by typo and mbstring
6640 'UTF-8' => 'UTF-8');
6642 asort($charsets);
6644 return $charsets;
6648 * Returns a list of valid and compatible themes
6650 * @global object
6651 * @return array
6653 function get_list_of_themes() {
6654 global $CFG;
6656 $themes = array();
6658 if (!empty($CFG->themelist)) { // use admin's list of themes
6659 $themelist = explode(',', $CFG->themelist);
6660 } else {
6661 $themelist = array_keys(get_plugin_list("theme"));
6664 foreach ($themelist as $key => $themename) {
6665 $theme = theme_config::load($themename);
6666 $themes[$themename] = $theme;
6668 asort($themes);
6670 return $themes;
6674 * Returns a list of timezones in the current language
6676 * @global object
6677 * @global object
6678 * @return array
6680 function get_list_of_timezones() {
6681 global $CFG, $DB;
6683 static $timezones;
6685 if (!empty($timezones)) { // This function has been called recently
6686 return $timezones;
6689 $timezones = array();
6691 if ($rawtimezones = $DB->get_records_sql("SELECT MAX(id), name FROM {timezone} GROUP BY name")) {
6692 foreach($rawtimezones as $timezone) {
6693 if (!empty($timezone->name)) {
6694 if (get_string_manager()->string_exists(strtolower($timezone->name), 'timezones')) {
6695 $timezones[$timezone->name] = get_string(strtolower($timezone->name), 'timezones');
6696 } else {
6697 $timezones[$timezone->name] = $timezone->name;
6699 if (substr($timezones[$timezone->name], 0, 1) == '[') { // No translation found
6700 $timezones[$timezone->name] = $timezone->name;
6706 asort($timezones);
6708 for ($i = -13; $i <= 13; $i += .5) {
6709 $tzstring = 'UTC';
6710 if ($i < 0) {
6711 $timezones[sprintf("%.1f", $i)] = $tzstring . $i;
6712 } else if ($i > 0) {
6713 $timezones[sprintf("%.1f", $i)] = $tzstring . '+' . $i;
6714 } else {
6715 $timezones[sprintf("%.1f", $i)] = $tzstring;
6719 return $timezones;
6723 * Factory function for emoticon_manager
6725 * @return emoticon_manager singleton
6727 function get_emoticon_manager() {
6728 static $singleton = null;
6730 if (is_null($singleton)) {
6731 $singleton = new emoticon_manager();
6734 return $singleton;
6738 * Provides core support for plugins that have to deal with
6739 * emoticons (like HTML editor or emoticon filter).
6741 * Whenever this manager mentiones 'emoticon object', the following data
6742 * structure is expected: stdClass with properties text, imagename, imagecomponent,
6743 * altidentifier and altcomponent
6745 * @see admin_setting_emoticons
6747 class emoticon_manager {
6750 * Returns the currently enabled emoticons
6752 * @return array of emoticon objects
6754 public function get_emoticons() {
6755 global $CFG;
6757 if (empty($CFG->emoticons)) {
6758 return array();
6761 $emoticons = $this->decode_stored_config($CFG->emoticons);
6763 if (!is_array($emoticons)) {
6764 // something is wrong with the format of stored setting
6765 debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL);
6766 return array();
6769 return $emoticons;
6773 * Converts emoticon object into renderable pix_emoticon object
6775 * @param stdClass $emoticon emoticon object
6776 * @param array $attributes explicit HTML attributes to set
6777 * @return pix_emoticon
6779 public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array()) {
6780 $stringmanager = get_string_manager();
6781 if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) {
6782 $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent);
6783 } else {
6784 $alt = s($emoticon->text);
6786 return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes);
6790 * Encodes the array of emoticon objects into a string storable in config table
6792 * @see self::decode_stored_config()
6793 * @param array $emoticons array of emtocion objects
6794 * @return string
6796 public function encode_stored_config(array $emoticons) {
6797 return json_encode($emoticons);
6801 * Decodes the string into an array of emoticon objects
6803 * @see self::encode_stored_config()
6804 * @param string $encoded
6805 * @return string|null
6807 public function decode_stored_config($encoded) {
6808 $decoded = json_decode($encoded);
6809 if (!is_array($decoded)) {
6810 return null;
6812 return $decoded;
6816 * Returns default set of emoticons supported by Moodle
6818 * @return array of sdtClasses
6820 public function default_emoticons() {
6821 return array(
6822 $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'),
6823 $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'),
6824 $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'),
6825 $this->prepare_emoticon_object(";-)", 's/wink', 'wink'),
6826 $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'),
6827 $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'),
6828 $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'),
6829 $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'),
6830 $this->prepare_emoticon_object("B-)", 's/cool', 'cool'),
6831 $this->prepare_emoticon_object("^-)", 's/approve', 'approve'),
6832 $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'),
6833 $this->prepare_emoticon_object(":o)", 's/clown', 'clown'),
6834 $this->prepare_emoticon_object(":-(", 's/sad', 'sad'),
6835 $this->prepare_emoticon_object(":(", 's/sad', 'sad'),
6836 $this->prepare_emoticon_object("8-.", 's/shy', 'shy'),
6837 $this->prepare_emoticon_object(":-I", 's/blush', 'blush'),
6838 $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'),
6839 $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'),
6840 $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'),
6841 $this->prepare_emoticon_object("8-[", 's/angry', 'angry'),
6842 $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'),
6843 $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'),
6844 $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'),
6845 $this->prepare_emoticon_object("}-]", 's/evil', 'evil'),
6846 $this->prepare_emoticon_object("(h)", 's/heart', 'heart'),
6847 $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'),
6848 $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'),
6849 $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'),
6850 $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'),
6851 $this->prepare_emoticon_object("( )", 's/egg', 'egg'),
6856 * Helper method preparing the stdClass with the emoticon properties
6858 * @param string|array $text or array of strings
6859 * @param string $imagename to be used by {@see pix_emoticon}
6860 * @param string $altidentifier alternative string identifier, null for no alt
6861 * @param array $altcomponent where the alternative string is defined
6862 * @param string $imagecomponent to be used by {@see pix_emoticon}
6863 * @return stdClass
6865 protected function prepare_emoticon_object($text, $imagename, $altidentifier = null, $altcomponent = 'core_pix', $imagecomponent = 'core') {
6866 return (object)array(
6867 'text' => $text,
6868 'imagename' => $imagename,
6869 'imagecomponent' => $imagecomponent,
6870 'altidentifier' => $altidentifier,
6871 'altcomponent' => $altcomponent,
6876 /// ENCRYPTION ////////////////////////////////////////////////
6879 * rc4encrypt
6881 * @todo Finish documenting this function
6883 * @param string $data Data to encrypt.
6884 * @param bool $usesecurekey Lets us know if we are using the old or new password.
6885 * @return string The now encrypted data.
6887 function rc4encrypt($data, $usesecurekey = false) {
6888 if (!$usesecurekey) {
6889 $passwordkey = 'nfgjeingjk';
6890 } else {
6891 $passwordkey = get_site_identifier();
6893 return endecrypt($passwordkey, $data, '');
6897 * rc4decrypt
6899 * @todo Finish documenting this function
6901 * @param string $data Data to decrypt.
6902 * @param bool $usesecurekey Lets us know if we are using the old or new password.
6903 * @return string The now decrypted data.
6905 function rc4decrypt($data, $usesecurekey = false) {
6906 if (!$usesecurekey) {
6907 $passwordkey = 'nfgjeingjk';
6908 } else {
6909 $passwordkey = get_site_identifier();
6911 return endecrypt($passwordkey, $data, 'de');
6915 * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com]
6917 * @todo Finish documenting this function
6919 * @param string $pwd The password to use when encrypting or decrypting
6920 * @param string $data The data to be decrypted/encrypted
6921 * @param string $case Either 'de' for decrypt or '' for encrypt
6922 * @return string
6924 function endecrypt ($pwd, $data, $case) {
6926 if ($case == 'de') {
6927 $data = urldecode($data);
6930 $key[] = '';
6931 $box[] = '';
6932 $temp_swap = '';
6933 $pwd_length = 0;
6935 $pwd_length = strlen($pwd);
6937 for ($i = 0; $i <= 255; $i++) {
6938 $key[$i] = ord(substr($pwd, ($i % $pwd_length), 1));
6939 $box[$i] = $i;
6942 $x = 0;
6944 for ($i = 0; $i <= 255; $i++) {
6945 $x = ($x + $box[$i] + $key[$i]) % 256;
6946 $temp_swap = $box[$i];
6947 $box[$i] = $box[$x];
6948 $box[$x] = $temp_swap;
6951 $temp = '';
6952 $k = '';
6954 $cipherby = '';
6955 $cipher = '';
6957 $a = 0;
6958 $j = 0;
6960 for ($i = 0; $i < strlen($data); $i++) {
6961 $a = ($a + 1) % 256;
6962 $j = ($j + $box[$a]) % 256;
6963 $temp = $box[$a];
6964 $box[$a] = $box[$j];
6965 $box[$j] = $temp;
6966 $k = $box[(($box[$a] + $box[$j]) % 256)];
6967 $cipherby = ord(substr($data, $i, 1)) ^ $k;
6968 $cipher .= chr($cipherby);
6971 if ($case == 'de') {
6972 $cipher = urldecode(urlencode($cipher));
6973 } else {
6974 $cipher = urlencode($cipher);
6977 return $cipher;
6980 /// ENVIRONMENT CHECKING ////////////////////////////////////////////////////////////
6983 * Returns the exact absolute path to plugin directory.
6985 * @param string $plugintype type of plugin
6986 * @param string $name name of the plugin
6987 * @return string full path to plugin directory; NULL if not found
6989 function get_plugin_directory($plugintype, $name) {
6990 global $CFG;
6992 if ($plugintype === '') {
6993 $plugintype = 'mod';
6996 $types = get_plugin_types(true);
6997 if (!array_key_exists($plugintype, $types)) {
6998 return NULL;
7000 $name = clean_param($name, PARAM_SAFEDIR); // just in case ;-)
7002 if (!empty($CFG->themedir) and $plugintype === 'theme') {
7003 if (!is_dir($types['theme'] . '/' . $name)) {
7004 // ok, so the theme is supposed to be in the $CFG->themedir
7005 return $CFG->themedir . '/' . $name;
7009 return $types[$plugintype].'/'.$name;
7013 * Return exact absolute path to a plugin directory,
7014 * this method support "simpletest_" prefix designed for unit testing.
7016 * @param string $component name such as 'moodle', 'mod_forum' or special simpletest value
7017 * @return string full path to component directory; NULL if not found
7019 function get_component_directory($component) {
7020 global $CFG;
7022 $simpletest = false;
7023 if (strpos($component, 'simpletest_') === 0) {
7024 $subdir = substr($component, strlen('simpletest_'));
7025 //TODO: this looks borked, where is it used actually?
7026 return $subdir;
7029 list($type, $plugin) = normalize_component($component);
7031 if ($type === 'core') {
7032 if ($plugin === NULL ) {
7033 $path = $CFG->libdir;
7034 } else {
7035 $subsystems = get_core_subsystems();
7036 if (isset($subsystems[$plugin])) {
7037 $path = $CFG->dirroot.'/'.$subsystems[$plugin];
7038 } else {
7039 $path = NULL;
7043 } else {
7044 $path = get_plugin_directory($type, $plugin);
7047 return $path;
7051 * Normalize the component name using the "frankenstyle" names.
7052 * @param string $component
7053 * @return array $type+$plugin elements
7055 function normalize_component($component) {
7056 if ($component === 'moodle' or $component === 'core') {
7057 $type = 'core';
7058 $plugin = NULL;
7060 } else if (strpos($component, '_') === false) {
7061 $subsystems = get_core_subsystems();
7062 if (array_key_exists($component, $subsystems)) {
7063 $type = 'core';
7064 $plugin = $component;
7065 } else {
7066 // everything else is a module
7067 $type = 'mod';
7068 $plugin = $component;
7071 } else {
7072 list($type, $plugin) = explode('_', $component, 2);
7073 $plugintypes = get_plugin_types(false);
7074 if ($type !== 'core' and !array_key_exists($type, $plugintypes)) {
7075 $type = 'mod';
7076 $plugin = $component;
7080 return array($type, $plugin);
7084 * List all core subsystems and their location
7086 * This is a whitelist of components that are part of the core and their
7087 * language strings are defined in /lang/en/<<subsystem>>.php. If a given
7088 * plugin is not listed here and it does not have proper plugintype prefix,
7089 * then it is considered as course activity module.
7091 * The location is dirroot relative path. NULL means there is no special
7092 * directory for this subsystem. If the location is set, the subsystem's
7093 * renderer.php is expected to be there.
7095 * @return array of (string)name => (string|null)location
7097 function get_core_subsystems() {
7098 global $CFG;
7100 static $info = null;
7102 if (!$info) {
7103 $info = array(
7104 'access' => NULL,
7105 'admin' => $CFG->admin,
7106 'auth' => 'auth',
7107 'backup' => 'backup/util/ui',
7108 'block' => 'blocks',
7109 'blog' => 'blog',
7110 'bulkusers' => NULL,
7111 'calendar' => 'calendar',
7112 'cohort' => 'cohort',
7113 'condition' => NULL,
7114 'completion' => NULL,
7115 'countries' => NULL,
7116 'course' => 'course',
7117 'currencies' => NULL,
7118 'dbtransfer' => NULL,
7119 'debug' => NULL,
7120 'dock' => NULL,
7121 'editor' => 'lib/editor',
7122 'edufields' => NULL,
7123 'enrol' => 'enrol',
7124 'error' => NULL,
7125 'filepicker' => NULL,
7126 'files' => 'files',
7127 'filters' => NULL,
7128 'flashdetect' => NULL,
7129 'fonts' => NULL,
7130 'form' => 'lib/form',
7131 'grades' => 'grade',
7132 'group' => 'group',
7133 'help' => NULL,
7134 'hub' => NULL,
7135 'imscc' => NULL,
7136 'install' => NULL,
7137 'iso6392' => NULL,
7138 'langconfig' => NULL,
7139 'license' => NULL,
7140 'mathslib' => NULL,
7141 'message' => 'message',
7142 'mimetypes' => NULL,
7143 'mnet' => 'mnet',
7144 'moodle.org' => NULL, // the dot is nasty, watch out! should be renamed to moodleorg
7145 'my' => 'my',
7146 'notes' => 'notes',
7147 'pagetype' => NULL,
7148 'pix' => NULL,
7149 'plagiarism' => 'plagiarism',
7150 'plugin' => NULL,
7151 'portfolio' => 'portfolio',
7152 'publish' => 'course/publish',
7153 'question' => 'question',
7154 'rating' => 'rating',
7155 'register' => 'admin/registration',
7156 'repository' => 'repository',
7157 'rss' => 'rss',
7158 'role' => $CFG->admin.'/role',
7159 'simpletest' => NULL,
7160 'search' => 'search',
7161 'table' => NULL,
7162 'tag' => 'tag',
7163 'timezones' => NULL,
7164 'user' => 'user',
7165 'userkey' => NULL,
7166 'webservice' => 'webservice',
7167 'xmldb' => NULL,
7171 return $info;
7175 * Lists all plugin types
7176 * @param bool $fullpaths false means relative paths from dirroot
7177 * @return array Array of strings - name=>location
7179 function get_plugin_types($fullpaths=true) {
7180 global $CFG;
7182 static $info = null;
7183 static $fullinfo = null;
7185 if (!$info) {
7186 $info = array('qtype' => 'question/type',
7187 'mod' => 'mod',
7188 'auth' => 'auth',
7189 'enrol' => 'enrol',
7190 'message' => 'message/output',
7191 'block' => 'blocks',
7192 'filter' => 'filter',
7193 'editor' => 'lib/editor',
7194 'format' => 'course/format',
7195 'profilefield' => 'user/profile/field',
7196 'report' => $CFG->admin.'/report',
7197 'coursereport' => 'course/report', // must be after system reports
7198 'gradeexport' => 'grade/export',
7199 'gradeimport' => 'grade/import',
7200 'gradereport' => 'grade/report',
7201 'mnetservice' => 'mnet/service',
7202 'webservice' => 'webservice',
7203 'repository' => 'repository',
7204 'portfolio' => 'portfolio',
7205 'qbehaviour' => 'question/behaviour',
7206 'qformat' => 'question/format',
7207 'plagiarism' => 'plagiarism',
7208 'theme' => 'theme'); // this is a bit hacky, themes may be in $CFG->themedir too
7210 $mods = get_plugin_list('mod');
7211 foreach ($mods as $mod => $moddir) {
7212 if (file_exists("$moddir/db/subplugins.php")) {
7213 $subplugins = array();
7214 include("$moddir/db/subplugins.php");
7215 foreach ($subplugins as $subtype=>$dir) {
7216 $info[$subtype] = $dir;
7221 // local is always last!
7222 $info['local'] = 'local';
7224 $fullinfo = array();
7225 foreach ($info as $type => $dir) {
7226 $fullinfo[$type] = $CFG->dirroot.'/'.$dir;
7230 return ($fullpaths ? $fullinfo : $info);
7234 * Simplified version of get_list_of_plugins()
7235 * @param string $plugintype type of plugin
7236 * @return array name=>fulllocation pairs of plugins of given type
7238 function get_plugin_list($plugintype) {
7239 global $CFG;
7241 $ignored = array('CVS', '_vti_cnf', 'simpletest', 'db', 'yui', 'phpunit');
7242 if ($plugintype == 'auth') {
7243 // Historically we have had an auth plugin called 'db', so allow a special case.
7244 $key = array_search('db', $ignored);
7245 if ($key !== false) {
7246 unset($ignored[$key]);
7250 if ($plugintype === '') {
7251 $plugintype = 'mod';
7254 $fulldirs = array();
7256 if ($plugintype === 'mod') {
7257 // mod is an exception because we have to call this function from get_plugin_types()
7258 $fulldirs[] = $CFG->dirroot.'/mod';
7260 } else if ($plugintype === 'theme') {
7261 $fulldirs[] = $CFG->dirroot.'/theme';
7262 // themes are special because they may be stored also in separate directory
7263 if (!empty($CFG->themedir) and file_exists($CFG->themedir) and is_dir($CFG->themedir) ) {
7264 $fulldirs[] = $CFG->themedir;
7267 } else {
7268 $types = get_plugin_types(true);
7269 if (!array_key_exists($plugintype, $types)) {
7270 return array();
7272 $fulldir = $types[$plugintype];
7273 if (!file_exists($fulldir)) {
7274 return array();
7276 $fulldirs[] = $fulldir;
7279 $result = array();
7281 foreach ($fulldirs as $fulldir) {
7282 if (!is_dir($fulldir)) {
7283 continue;
7285 $items = new DirectoryIterator($fulldir);
7286 foreach ($items as $item) {
7287 if ($item->isDot() or !$item->isDir()) {
7288 continue;
7290 $pluginname = $item->getFilename();
7291 if (in_array($pluginname, $ignored)) {
7292 continue;
7294 if ($pluginname !== clean_param($pluginname, PARAM_SAFEDIR)) {
7295 // better ignore plugins with problematic names here
7296 continue;
7298 $result[$pluginname] = $fulldir.'/'.$pluginname;
7299 unset($item);
7301 unset($items);
7304 //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!
7305 ksort($result);
7306 return $result;
7310 * Gets a list of all plugin API functions for given plugin type, function
7311 * name, and filename.
7312 * @param string $plugintype Plugin type, e.g. 'mod' or 'report'
7313 * @param string $function Name of function after the frankenstyle prefix;
7314 * e.g. if the function is called report_courselist_hook then this value
7315 * would be 'hook'
7316 * @param string $file Name of file that includes function within plugin,
7317 * default 'lib.php'
7318 * @return Array of plugin frankenstyle (e.g. 'report_courselist', 'mod_forum')
7319 * to valid, existing plugin function name (e.g. 'report_courselist_hook',
7320 * 'forum_hook')
7322 function get_plugin_list_with_function($plugintype, $function, $file='lib.php') {
7323 global $CFG; // mandatory in case it is referenced by include()d PHP script
7325 $result = array();
7326 // Loop through list of plugins with given type
7327 $list = get_plugin_list($plugintype);
7328 foreach($list as $plugin => $dir) {
7329 $path = $dir . '/' . $file;
7330 // If file exists, require it and look for function
7331 if (file_exists($path)) {
7332 include_once($path);
7333 $fullfunction = $plugintype . '_' . $plugin . '_' . $function;
7334 if (function_exists($fullfunction)) {
7335 // Function exists with standard name. Store, indexed by
7336 // frankenstyle name of plugin
7337 $result[$plugintype . '_' . $plugin] = $fullfunction;
7338 } else if ($plugintype === 'mod') {
7339 // For modules, we also allow plugin without full frankenstyle
7340 // but just starting with the module name
7341 $shortfunction = $plugin . '_' . $function;
7342 if (function_exists($shortfunction)) {
7343 $result[$plugintype . '_' . $plugin] = $shortfunction;
7348 return $result;
7352 * Lists plugin-like directories within specified directory
7354 * This function was originally used for standard Moodle plugins, please use
7355 * new get_plugin_list() now.
7357 * This function is used for general directory listing and backwards compatility.
7359 * @param string $directory relative directory from root
7360 * @param string $exclude dir name to exclude from the list (defaults to none)
7361 * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot)
7362 * @return array Sorted array of directory names found under the requested parameters
7364 function get_list_of_plugins($directory='mod', $exclude='', $basedir='') {
7365 global $CFG;
7367 $plugins = array();
7369 if (empty($basedir)) {
7370 $basedir = $CFG->dirroot .'/'. $directory;
7372 } else {
7373 $basedir = $basedir .'/'. $directory;
7376 if (file_exists($basedir) && filetype($basedir) == 'dir') {
7377 $dirhandle = opendir($basedir);
7378 while (false !== ($dir = readdir($dirhandle))) {
7379 $firstchar = substr($dir, 0, 1);
7380 if ($firstchar === '.' or $dir === 'CVS' or $dir === '_vti_cnf' or $dir === 'simpletest' or $dir === 'yui' or $dir === 'phpunit' or $dir === $exclude) {
7381 continue;
7383 if (filetype($basedir .'/'. $dir) != 'dir') {
7384 continue;
7386 $plugins[] = $dir;
7388 closedir($dirhandle);
7390 if ($plugins) {
7391 asort($plugins);
7393 return $plugins;
7397 * Invoke plugin's callback functions
7399 * @param string $type plugin type e.g. 'mod'
7400 * @param string $name plugin name
7401 * @param string $feature feature name
7402 * @param string $action feature's action
7403 * @param array $params parameters of callback function, should be an array
7404 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
7405 * @return mixed
7407 * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743
7409 function plugin_callback($type, $name, $feature, $action, $params = null, $default = null) {
7410 return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default);
7414 * Invoke component's callback functions
7416 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
7417 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
7418 * @param array $params parameters of callback function
7419 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
7420 * @return mixed
7422 function component_callback($component, $function, array $params = array(), $default = null) {
7423 global $CFG; // this is needed for require_once() below
7425 $cleancomponent = clean_param($component, PARAM_SAFEDIR);
7426 if (empty($cleancomponent)) {
7427 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
7429 $component = $cleancomponent;
7431 list($type, $name) = normalize_component($component);
7432 $component = $type . '_' . $name;
7434 $oldfunction = $name.'_'.$function;
7435 $function = $component.'_'.$function;
7437 $dir = get_component_directory($component);
7438 if (empty($dir)) {
7439 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
7442 // Load library and look for function
7443 if (file_exists($dir.'/lib.php')) {
7444 require_once($dir.'/lib.php');
7447 if (!function_exists($function) and function_exists($oldfunction)) {
7448 // In Moodle 2.2 and greater this will result in a debugging notice however
7449 // for 2.1+ we tolerate it.
7450 $function = $oldfunction;
7453 if (function_exists($function)) {
7454 // Function exists, so just return function result
7455 $ret = call_user_func_array($function, $params);
7456 if (is_null($ret)) {
7457 return $default;
7458 } else {
7459 return $ret;
7462 return $default;
7466 * Checks whether a plugin supports a specified feature.
7468 * @param string $type Plugin type e.g. 'mod'
7469 * @param string $name Plugin name e.g. 'forum'
7470 * @param string $feature Feature code (FEATURE_xx constant)
7471 * @param mixed $default default value if feature support unknown
7472 * @return mixed Feature result (false if not supported, null if feature is unknown,
7473 * otherwise usually true but may have other feature-specific value such as array)
7475 function plugin_supports($type, $name, $feature, $default = NULL) {
7476 global $CFG;
7478 $name = clean_param($name, PARAM_SAFEDIR); //bit of extra security
7480 $function = null;
7482 if ($type === 'mod') {
7483 // we need this special case because we support subplugins in modules,
7484 // otherwise it would end up in infinite loop
7485 if ($name === 'NEWMODULE') {
7486 //somebody forgot to rename the module template
7487 return false;
7489 if (file_exists("$CFG->dirroot/mod/$name/lib.php")) {
7490 include_once("$CFG->dirroot/mod/$name/lib.php");
7491 $function = $type.'_'.$name.'_supports';
7492 if (!function_exists($function)) {
7493 // legacy non-frankenstyle function name
7494 $function = $name.'_supports';
7498 } else {
7499 if (!$path = get_plugin_directory($type, $name)) {
7500 // non existent plugin type
7501 return false;
7503 if (file_exists("$path/lib.php")) {
7504 include_once("$path/lib.php");
7505 $function = $type.'_'.$name.'_supports';
7509 if ($function and function_exists($function)) {
7510 $supports = $function($feature);
7511 if (is_null($supports)) {
7512 // plugin does not know - use default
7513 return $default;
7514 } else {
7515 return $supports;
7519 //plugin does not care, so use default
7520 return $default;
7524 * Returns true if the current version of PHP is greater that the specified one.
7526 * @todo Check PHP version being required here is it too low?
7528 * @param string $version The version of php being tested.
7529 * @return bool
7531 function check_php_version($version='5.2.4') {
7532 return (version_compare(phpversion(), $version) >= 0);
7536 * Checks to see if is the browser operating system matches the specified
7537 * brand.
7539 * Known brand: 'Windows','Linux','Macintosh','SGI','SunOS','HP-UX'
7541 * @uses $_SERVER
7542 * @param string $brand The operating system identifier being tested
7543 * @return bool true if the given brand below to the detected operating system
7545 function check_browser_operating_system($brand) {
7546 if (empty($_SERVER['HTTP_USER_AGENT'])) {
7547 return false;
7550 if (preg_match("/$brand/i", $_SERVER['HTTP_USER_AGENT'])) {
7551 return true;
7554 return false;
7558 * Checks to see if is a browser matches the specified
7559 * brand and is equal or better version.
7561 * @uses $_SERVER
7562 * @param string $brand The browser identifier being tested
7563 * @param int $version The version of the browser, if not specified any version (except 5.5 for IE for BC reasons)
7564 * @return bool true if the given version is below that of the detected browser
7566 function check_browser_version($brand, $version = null) {
7567 if (empty($_SERVER['HTTP_USER_AGENT'])) {
7568 return false;
7571 $agent = $_SERVER['HTTP_USER_AGENT'];
7573 switch ($brand) {
7575 case 'Camino': /// OSX browser using Gecke engine
7576 if (strpos($agent, 'Camino') === false) {
7577 return false;
7579 if (empty($version)) {
7580 return true; // no version specified
7582 if (preg_match("/Camino\/([0-9\.]+)/i", $agent, $match)) {
7583 if (version_compare($match[1], $version) >= 0) {
7584 return true;
7587 break;
7590 case 'Firefox': /// Mozilla Firefox browsers
7591 if (strpos($agent, 'Iceweasel') === false and strpos($agent, 'Firefox') === false) {
7592 return false;
7594 if (empty($version)) {
7595 return true; // no version specified
7597 if (preg_match("/(Iceweasel|Firefox)\/([0-9\.]+)/i", $agent, $match)) {
7598 if (version_compare($match[2], $version) >= 0) {
7599 return true;
7602 break;
7605 case 'Gecko': /// Gecko based browsers
7606 if (empty($version) and substr_count($agent, 'Camino')) {
7607 // MacOS X Camino support
7608 $version = 20041110;
7611 // the proper string - Gecko/CCYYMMDD Vendor/Version
7612 // Faster version and work-a-round No IDN problem.
7613 if (preg_match("/Gecko\/([0-9]+)/i", $agent, $match)) {
7614 if ($match[1] > $version) {
7615 return true;
7618 break;
7621 case 'MSIE': /// Internet Explorer
7622 if (strpos($agent, 'Opera') !== false) { // Reject Opera
7623 return false;
7625 // in case of IE we have to deal with BC of the version parameter
7626 if (is_null($version)) {
7627 $version = 5.5; // anything older is not considered a browser at all!
7630 //see: http://www.useragentstring.com/pages/Internet%20Explorer/
7631 if (preg_match("/MSIE ([0-9\.]+)/", $agent, $match)) {
7632 if (version_compare($match[1], $version) >= 0) {
7633 return true;
7636 break;
7639 case 'Opera': /// Opera
7640 if (strpos($agent, 'Opera') === false) {
7641 return false;
7643 if (empty($version)) {
7644 return true; // no version specified
7646 if (preg_match("/Opera\/([0-9\.]+)/i", $agent, $match)) {
7647 if (version_compare($match[1], $version) >= 0) {
7648 return true;
7651 break;
7654 case 'WebKit': /// WebKit based browser - everything derived from it (Safari, Chrome, iOS, Android and other mobiles)
7655 if (strpos($agent, 'AppleWebKit') === false) {
7656 return false;
7658 if (empty($version)) {
7659 return true; // no version specified
7661 if (preg_match("/AppleWebKit\/([0-9]+)/i", $agent, $match)) {
7662 if (version_compare($match[1], $version) >= 0) {
7663 return true;
7666 break;
7669 case 'Safari': /// Desktop version of Apple Safari browser - no mobile or touch devices
7670 if (strpos($agent, 'AppleWebKit') === false) {
7671 return false;
7673 // Look for AppleWebKit, excluding strings with OmniWeb, Shiira and SymbianOS and any other mobile devices
7674 if (strpos($agent, 'OmniWeb')) { // Reject OmniWeb
7675 return false;
7677 if (strpos($agent, 'Shiira')) { // Reject Shiira
7678 return false;
7680 if (strpos($agent, 'SymbianOS')) { // Reject SymbianOS
7681 return false;
7683 if (strpos($agent, 'Android')) { // Reject Androids too
7684 return false;
7686 if (strpos($agent, 'iPhone') or strpos($agent, 'iPad') or strpos($agent, 'iPod')) {
7687 // No Apple mobile devices here - editor does not work, course ajax is not touch compatible, etc.
7688 return false;
7690 if (strpos($agent, 'Chrome')) { // Reject chrome browsers - it needs to be tested explicitly
7691 return false;
7694 if (empty($version)) {
7695 return true; // no version specified
7697 if (preg_match("/AppleWebKit\/([0-9]+)/i", $agent, $match)) {
7698 if (version_compare($match[1], $version) >= 0) {
7699 return true;
7702 break;
7705 case 'Chrome':
7706 if (strpos($agent, 'Chrome') === false) {
7707 return false;
7709 if (empty($version)) {
7710 return true; // no version specified
7712 if (preg_match("/Chrome\/(.*)[ ]+/i", $agent, $match)) {
7713 if (version_compare($match[1], $version) >= 0) {
7714 return true;
7717 break;
7720 case 'Safari iOS': /// Safari on iPhone, iPad and iPod touch
7721 if (strpos($agent, 'AppleWebKit') === false or strpos($agent, 'Safari') === false) {
7722 return false;
7724 if (!strpos($agent, 'iPhone') and !strpos($agent, 'iPad') and !strpos($agent, 'iPod')) {
7725 return false;
7727 if (empty($version)) {
7728 return true; // no version specified
7730 if (preg_match("/AppleWebKit\/([0-9]+)/i", $agent, $match)) {
7731 if (version_compare($match[1], $version) >= 0) {
7732 return true;
7735 break;
7738 case 'WebKit Android': /// WebKit browser on Android
7739 if (strpos($agent, 'Linux; U; Android') === false) {
7740 return false;
7742 if (empty($version)) {
7743 return true; // no version specified
7745 if (preg_match("/AppleWebKit\/([0-9]+)/i", $agent, $match)) {
7746 if (version_compare($match[1], $version) >= 0) {
7747 return true;
7750 break;
7754 return false;
7758 * Returns whether a device/browser combination is mobile, tablet, legacy, default or the result of
7759 * an optional admin specified regular expression. If enabledevicedetection is set to no or not set
7760 * it returns default
7762 * @return string device type
7764 function get_device_type() {
7765 global $CFG;
7767 if (empty($CFG->enabledevicedetection) || empty($_SERVER['HTTP_USER_AGENT'])) {
7768 return 'default';
7771 $useragent = $_SERVER['HTTP_USER_AGENT'];
7773 if (!empty($CFG->devicedetectregex)) {
7774 $regexes = json_decode($CFG->devicedetectregex);
7776 foreach ($regexes as $value=>$regex) {
7777 if (preg_match($regex, $useragent)) {
7778 return $value;
7783 //mobile detection PHP direct copy from open source detectmobilebrowser.com
7784 $phonesregex = '/android .+ mobile|avantgo|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i';
7785 $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';
7786 if (preg_match($phonesregex,$useragent) || preg_match($modelsregex,substr($useragent, 0, 4))){
7787 return 'mobile';
7790 $tabletregex = '/Tablet browser|android|iPad|iProd|GT-P1000|GT-I9000|SHW-M180S|SGH-T849|SCH-I800|Build\/ERE27|sholest/i';
7791 if (preg_match($tabletregex, $useragent)) {
7792 return 'tablet';
7795 // Safe way to check for IE6 and not get false positives for some IE 7/8 users
7796 if (substr($_SERVER['HTTP_USER_AGENT'], 0, 34) === 'Mozilla/4.0 (compatible; MSIE 6.0;') {
7797 return 'legacy';
7800 return 'default';
7804 * Returns a list of the device types supporting by Moodle
7806 * @param boolean $incusertypes includes types specified using the devicedetectregex admin setting
7807 * @return array $types
7809 function get_device_type_list($incusertypes = true) {
7810 global $CFG;
7812 $types = array('default', 'legacy', 'mobile', 'tablet');
7814 if ($incusertypes && !empty($CFG->devicedetectregex)) {
7815 $regexes = json_decode($CFG->devicedetectregex);
7817 foreach ($regexes as $value => $regex) {
7818 $types[] = $value;
7822 return $types;
7826 * Returns the theme selected for a particular device or false if none selected.
7828 * @param string $devicetype
7829 * @return string|false The name of the theme to use for the device or the false if not set
7831 function get_selected_theme_for_device_type($devicetype = null) {
7832 global $CFG;
7834 if (empty($devicetype)) {
7835 $devicetype = get_user_device_type();
7838 $themevarname = get_device_cfg_var_name($devicetype);
7839 if (empty($CFG->$themevarname)) {
7840 return false;
7843 return $CFG->$themevarname;
7847 * Returns the name of the device type theme var in $CFG (because there is not a standard convention to allow backwards compatability
7849 * @param string $devicetype
7850 * @return string The config variable to use to determine the theme
7852 function get_device_cfg_var_name($devicetype = null) {
7853 if ($devicetype == 'default' || empty($devicetype)) {
7854 return 'theme';
7857 return 'theme' . $devicetype;
7861 * Allows the user to switch the device they are seeing the theme for.
7862 * This allows mobile users to switch back to the default theme, or theme for any other device.
7864 * @param string $newdevice The device the user is currently using.
7865 * @return string The device the user has switched to
7867 function set_user_device_type($newdevice) {
7868 global $USER;
7870 $devicetype = get_device_type();
7871 $devicetypes = get_device_type_list();
7873 if ($newdevice == $devicetype) {
7874 unset_user_preference('switchdevice'.$devicetype);
7875 } else if (in_array($newdevice, $devicetypes)) {
7876 set_user_preference('switchdevice'.$devicetype, $newdevice);
7881 * Returns the device the user is currently using, or if the user has chosen to switch devices
7882 * for the current device type the type they have switched to.
7884 * @return string The device the user is currently using or wishes to use
7886 function get_user_device_type() {
7887 $device = get_device_type();
7888 $switched = get_user_preferences('switchdevice'.$device, false);
7889 if ($switched != false) {
7890 return $switched;
7892 return $device;
7896 * Returns one or several CSS class names that match the user's browser. These can be put
7897 * in the body tag of the page to apply browser-specific rules without relying on CSS hacks
7899 * @return array An array of browser version classes
7901 function get_browser_version_classes() {
7902 $classes = array();
7904 if (check_browser_version("MSIE", "0")) {
7905 $classes[] = 'ie';
7906 if (check_browser_version("MSIE", 9)) {
7907 $classes[] = 'ie9';
7908 } else if (check_browser_version("MSIE", 8)) {
7909 $classes[] = 'ie8';
7910 } elseif (check_browser_version("MSIE", 7)) {
7911 $classes[] = 'ie7';
7912 } elseif (check_browser_version("MSIE", 6)) {
7913 $classes[] = 'ie6';
7916 } else if (check_browser_version("Firefox") || check_browser_version("Gecko") || check_browser_version("Camino")) {
7917 $classes[] = 'gecko';
7918 if (preg_match('/rv\:([1-2])\.([0-9])/', $_SERVER['HTTP_USER_AGENT'], $matches)) {
7919 $classes[] = "gecko{$matches[1]}{$matches[2]}";
7922 } else if (check_browser_version("WebKit")) {
7923 $classes[] = 'safari';
7924 if (check_browser_version("Safari iOS")) {
7925 $classes[] = 'ios';
7927 } else if (check_browser_version("WebKit Android")) {
7928 $classes[] = 'android';
7931 } else if (check_browser_version("Opera")) {
7932 $classes[] = 'opera';
7936 return $classes;
7940 * Can handle rotated text. Whether it is safe to use the trickery in textrotate.js.
7942 * @return bool True for yes, false for no
7944 function can_use_rotated_text() {
7945 global $USER;
7946 return ajaxenabled(array('Firefox' => 2.0)) && !$USER->screenreader;;
7950 * Hack to find out the GD version by parsing phpinfo output
7952 * @return int GD version (1, 2, or 0)
7954 function check_gd_version() {
7955 $gdversion = 0;
7957 if (function_exists('gd_info')){
7958 $gd_info = gd_info();
7959 if (substr_count($gd_info['GD Version'], '2.')) {
7960 $gdversion = 2;
7961 } else if (substr_count($gd_info['GD Version'], '1.')) {
7962 $gdversion = 1;
7965 } else {
7966 ob_start();
7967 phpinfo(INFO_MODULES);
7968 $phpinfo = ob_get_contents();
7969 ob_end_clean();
7971 $phpinfo = explode("\n", $phpinfo);
7974 foreach ($phpinfo as $text) {
7975 $parts = explode('</td>', $text);
7976 foreach ($parts as $key => $val) {
7977 $parts[$key] = trim(strip_tags($val));
7979 if ($parts[0] == 'GD Version') {
7980 if (substr_count($parts[1], '2.0')) {
7981 $parts[1] = '2.0';
7983 $gdversion = intval($parts[1]);
7988 return $gdversion; // 1, 2 or 0
7992 * Determine if moodle installation requires update
7994 * Checks version numbers of main code and all modules to see
7995 * if there are any mismatches
7997 * @global object
7998 * @global object
7999 * @return bool
8001 function moodle_needs_upgrading() {
8002 global $CFG, $DB, $OUTPUT;
8004 if (empty($CFG->version)) {
8005 return true;
8008 // main versio nfirst
8009 $version = null;
8010 include($CFG->dirroot.'/version.php'); // defines $version and upgrades
8011 if ($version > $CFG->version) {
8012 return true;
8015 // modules
8016 $mods = get_plugin_list('mod');
8017 $installed = $DB->get_records('modules', array(), '', 'name, version');
8018 foreach ($mods as $mod => $fullmod) {
8019 if ($mod === 'NEWMODULE') { // Someone has unzipped the template, ignore it
8020 continue;
8022 $module = new stdClass();
8023 if (!is_readable($fullmod.'/version.php')) {
8024 continue;
8026 include($fullmod.'/version.php'); // defines $module with version etc
8027 if (empty($installed[$mod])) {
8028 return true;
8029 } else if ($module->version > $installed[$mod]->version) {
8030 return true;
8033 unset($installed);
8035 // blocks
8036 $blocks = get_plugin_list('block');
8037 $installed = $DB->get_records('block', array(), '', 'name, version');
8038 require_once($CFG->dirroot.'/blocks/moodleblock.class.php');
8039 foreach ($blocks as $blockname=>$fullblock) {
8040 if ($blockname === 'NEWBLOCK') { // Someone has unzipped the template, ignore it
8041 continue;
8043 if (!is_readable($fullblock.'/version.php')) {
8044 continue;
8046 $plugin = new stdClass();
8047 $plugin->version = NULL;
8048 include($fullblock.'/version.php');
8049 if (empty($installed[$blockname])) {
8050 return true;
8051 } else if ($plugin->version > $installed[$blockname]->version) {
8052 return true;
8055 unset($installed);
8057 // now the rest of plugins
8058 $plugintypes = get_plugin_types();
8059 unset($plugintypes['mod']);
8060 unset($plugintypes['block']);
8061 foreach ($plugintypes as $type=>$unused) {
8062 $plugs = get_plugin_list($type);
8063 foreach ($plugs as $plug=>$fullplug) {
8064 $component = $type.'_'.$plug;
8065 if (!is_readable($fullplug.'/version.php')) {
8066 continue;
8068 $plugin = new stdClass();
8069 include($fullplug.'/version.php'); // defines $plugin with version etc
8070 $installedversion = get_config($component, 'version');
8071 if (empty($installedversion)) { // new installation
8072 return true;
8073 } else if ($installedversion < $plugin->version) { // upgrade
8074 return true;
8079 return false;
8083 * Sets maximum expected time needed for upgrade task.
8084 * Please always make sure that upgrade will not run longer!
8086 * The script may be automatically aborted if upgrade times out.
8088 * @global object
8089 * @param int $max_execution_time in seconds (can not be less than 60 s)
8091 function upgrade_set_timeout($max_execution_time=300) {
8092 global $CFG;
8094 if (!isset($CFG->upgraderunning) or $CFG->upgraderunning < time()) {
8095 $upgraderunning = get_config(null, 'upgraderunning');
8096 } else {
8097 $upgraderunning = $CFG->upgraderunning;
8100 if (!$upgraderunning) {
8101 // upgrade not running or aborted
8102 print_error('upgradetimedout', 'admin', "$CFG->wwwroot/$CFG->admin/");
8103 die;
8106 if ($max_execution_time < 60) {
8107 // protection against 0 here
8108 $max_execution_time = 60;
8111 $expected_end = time() + $max_execution_time;
8113 if ($expected_end < $upgraderunning + 10 and $expected_end > $upgraderunning - 10) {
8114 // no need to store new end, it is nearly the same ;-)
8115 return;
8118 if (CLI_SCRIPT) {
8119 // there is no point in timing out of CLI scripts, admins can stop them if necessary
8120 set_time_limit(0);
8121 } else {
8122 set_time_limit($max_execution_time);
8124 set_config('upgraderunning', $expected_end); // keep upgrade locked until this time
8127 /// MISCELLANEOUS ////////////////////////////////////////////////////////////////////
8130 * Notify admin users or admin user of any failed logins (since last notification).
8132 * Note that this function must be only executed from the cron script
8133 * It uses the cache_flags system to store temporary records, deleting them
8134 * by name before finishing
8136 * @global object
8137 * @global object
8138 * @uses HOURSECS
8139 * @return bool True if executed, false if not
8141 function notify_login_failures() {
8142 global $CFG, $DB, $OUTPUT;
8144 if (empty($CFG->notifyloginfailures)) {
8145 return false;
8148 if (empty($CFG->lastnotifyfailure)) {
8149 $CFG->lastnotifyfailure=0;
8152 $recip = get_users_from_config($CFG->notifyloginfailures, 'moodle/site:config');
8154 // If it has been less than an hour, or if there are no recipients, don't execute.
8155 if (((time() - HOURSECS) < $CFG->lastnotifyfailure) || !is_array($recip) || count($recip) <= 0) {
8156 return false;
8159 // we need to deal with the threshold stuff first.
8160 if (empty($CFG->notifyloginthreshold)) {
8161 $CFG->notifyloginthreshold = 10; // default to something sensible.
8164 /// Get all the IPs with more than notifyloginthreshold failures since lastnotifyfailure
8165 /// and insert them into the cache_flags temp table
8166 $sql = "SELECT ip, COUNT(*)
8167 FROM {log}
8168 WHERE module = 'login' AND action = 'error'
8169 AND time > ?
8170 GROUP BY ip
8171 HAVING COUNT(*) >= ?";
8172 $params = array($CFG->lastnotifyfailure, $CFG->notifyloginthreshold);
8173 $rs = $DB->get_recordset_sql($sql, $params);
8174 foreach ($rs as $iprec) {
8175 if (!empty($iprec->ip)) {
8176 set_cache_flag('login_failure_by_ip', $iprec->ip, '1', 0);
8179 $rs->close();
8181 /// Get all the INFOs with more than notifyloginthreshold failures since lastnotifyfailure
8182 /// and insert them into the cache_flags temp table
8183 $sql = "SELECT info, count(*)
8184 FROM {log}
8185 WHERE module = 'login' AND action = 'error'
8186 AND time > ?
8187 GROUP BY info
8188 HAVING count(*) >= ?";
8189 $params = array($CFG->lastnotifyfailure, $CFG->notifyloginthreshold);
8190 $rs = $DB->get_recordset_sql($sql, $params);
8191 foreach ($rs as $inforec) {
8192 if (!empty($inforec->info)) {
8193 set_cache_flag('login_failure_by_info', $inforec->info, '1', 0);
8196 $rs->close();
8198 /// Now, select all the login error logged records belonging to the ips and infos
8199 /// since lastnotifyfailure, that we have stored in the cache_flags table
8200 $sql = "SELECT * FROM (
8201 SELECT l.*, u.firstname, u.lastname
8202 FROM {log} l
8203 JOIN {cache_flags} cf ON l.ip = cf.name
8204 LEFT JOIN {user} u ON l.userid = u.id
8205 WHERE l.module = 'login' AND l.action = 'error'
8206 AND l.time > ?
8207 AND cf.flagtype = 'login_failure_by_ip'
8208 UNION ALL
8209 SELECT l.*, u.firstname, u.lastname
8210 FROM {log} l
8211 JOIN {cache_flags} cf ON l.info = cf.name
8212 LEFT JOIN {user} u ON l.userid = u.id
8213 WHERE l.module = 'login' AND l.action = 'error'
8214 AND l.time > ?
8215 AND cf.flagtype = 'login_failure_by_info') t
8216 ORDER BY t.time DESC";
8217 $params = array($CFG->lastnotifyfailure, $CFG->lastnotifyfailure);
8219 /// Init some variables
8220 $count = 0;
8221 $messages = '';
8222 /// Iterate over the logs recordset
8223 $rs = $DB->get_recordset_sql($sql, $params);
8224 foreach ($rs as $log) {
8225 $log->time = userdate($log->time);
8226 $messages .= get_string('notifyloginfailuresmessage','',$log)."\n";
8227 $count++;
8229 $rs->close();
8231 // If we have something useful to report.
8232 if ($count > 0) {
8233 $site = get_site();
8234 $subject = get_string('notifyloginfailuressubject', '', format_string($site->fullname));
8235 /// Calculate the complete body of notification (start + messages + end)
8236 $body = get_string('notifyloginfailuresmessagestart', '', $CFG->wwwroot) .
8237 (($CFG->lastnotifyfailure != 0) ? '('.userdate($CFG->lastnotifyfailure).')' : '')."\n\n" .
8238 $messages .
8239 "\n\n".get_string('notifyloginfailuresmessageend','',$CFG->wwwroot)."\n\n";
8241 /// For each destination, send mail
8242 mtrace('Emailing admins about '. $count .' failed login attempts');
8243 foreach ($recip as $admin) {
8244 //emailing the admins directly rather than putting these through the messaging system
8245 email_to_user($admin,get_admin(), $subject, $body);
8248 /// Update lastnotifyfailure with current time
8249 set_config('lastnotifyfailure', time());
8252 /// Finally, delete all the temp records we have created in cache_flags
8253 $DB->delete_records_select('cache_flags', "flagtype IN ('login_failure_by_ip', 'login_failure_by_info')");
8255 return true;
8259 * Sets the system locale
8261 * @todo Finish documenting this function
8263 * @global object
8264 * @param string $locale Can be used to force a locale
8266 function moodle_setlocale($locale='') {
8267 global $CFG;
8269 static $currentlocale = ''; // last locale caching
8271 $oldlocale = $currentlocale;
8273 /// Fetch the correct locale based on ostype
8274 if ($CFG->ostype == 'WINDOWS') {
8275 $stringtofetch = 'localewin';
8276 } else {
8277 $stringtofetch = 'locale';
8280 /// the priority is the same as in get_string() - parameter, config, course, session, user, global language
8281 if (!empty($locale)) {
8282 $currentlocale = $locale;
8283 } else if (!empty($CFG->locale)) { // override locale for all language packs
8284 $currentlocale = $CFG->locale;
8285 } else {
8286 $currentlocale = get_string($stringtofetch, 'langconfig');
8289 /// do nothing if locale already set up
8290 if ($oldlocale == $currentlocale) {
8291 return;
8294 /// Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values,
8295 /// set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7
8296 /// Some day, numeric, monetary and other categories should be set too, I think. :-/
8298 /// Get current values
8299 $monetary= setlocale (LC_MONETARY, 0);
8300 $numeric = setlocale (LC_NUMERIC, 0);
8301 $ctype = setlocale (LC_CTYPE, 0);
8302 if ($CFG->ostype != 'WINDOWS') {
8303 $messages= setlocale (LC_MESSAGES, 0);
8305 /// Set locale to all
8306 setlocale (LC_ALL, $currentlocale);
8307 /// Set old values
8308 setlocale (LC_MONETARY, $monetary);
8309 setlocale (LC_NUMERIC, $numeric);
8310 if ($CFG->ostype != 'WINDOWS') {
8311 setlocale (LC_MESSAGES, $messages);
8313 if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') { // To workaround a well-known PHP problem with Turkish letter Ii
8314 setlocale (LC_CTYPE, $ctype);
8319 * Converts string to lowercase using most compatible function available.
8321 * @todo Remove this function when no longer in use
8322 * @deprecated Use textlib->strtolower($text) instead.
8324 * @param string $string The string to convert to all lowercase characters.
8325 * @param string $encoding The encoding on the string.
8326 * @return string
8328 function moodle_strtolower ($string, $encoding='') {
8330 //If not specified use utf8
8331 if (empty($encoding)) {
8332 $encoding = 'UTF-8';
8334 //Use text services
8335 $textlib = textlib_get_instance();
8337 return $textlib->strtolower($string, $encoding);
8341 * Count words in a string.
8343 * Words are defined as things between whitespace.
8345 * @param string $string The text to be searched for words.
8346 * @return int The count of words in the specified string
8348 function count_words($string) {
8349 $string = strip_tags($string);
8350 return count(preg_split("/\w\b/", $string)) - 1;
8353 /** Count letters in a string.
8355 * Letters are defined as chars not in tags and different from whitespace.
8357 * @param string $string The text to be searched for letters.
8358 * @return int The count of letters in the specified text.
8360 function count_letters($string) {
8361 /// Loading the textlib singleton instance. We are going to need it.
8362 $textlib = textlib_get_instance();
8364 $string = strip_tags($string); // Tags are out now
8365 $string = preg_replace('/[[:space:]]*/','',$string); //Whitespace are out now
8367 return $textlib->strlen($string);
8371 * Generate and return a random string of the specified length.
8373 * @param int $length The length of the string to be created.
8374 * @return string
8376 function random_string ($length=15) {
8377 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
8378 $pool .= 'abcdefghijklmnopqrstuvwxyz';
8379 $pool .= '0123456789';
8380 $poollen = strlen($pool);
8381 mt_srand ((double) microtime() * 1000000);
8382 $string = '';
8383 for ($i = 0; $i < $length; $i++) {
8384 $string .= substr($pool, (mt_rand()%($poollen)), 1);
8386 return $string;
8390 * Generate a complex random string (useful for md5 salts)
8392 * This function is based on the above {@link random_string()} however it uses a
8393 * larger pool of characters and generates a string between 24 and 32 characters
8395 * @param int $length Optional if set generates a string to exactly this length
8396 * @return string
8398 function complex_random_string($length=null) {
8399 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
8400 $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
8401 $poollen = strlen($pool);
8402 mt_srand ((double) microtime() * 1000000);
8403 if ($length===null) {
8404 $length = floor(rand(24,32));
8406 $string = '';
8407 for ($i = 0; $i < $length; $i++) {
8408 $string .= $pool[(mt_rand()%$poollen)];
8410 return $string;
8414 * Given some text (which may contain HTML) and an ideal length,
8415 * this function truncates the text neatly on a word boundary if possible
8417 * @global object
8418 * @param string $text - text to be shortened
8419 * @param int $ideal - ideal string length
8420 * @param boolean $exact if false, $text will not be cut mid-word
8421 * @param string $ending The string to append if the passed string is truncated
8422 * @return string $truncate - shortened string
8424 function shorten_text($text, $ideal=30, $exact = false, $ending='...') {
8426 global $CFG;
8428 // if the plain text is shorter than the maximum length, return the whole text
8429 if (strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) {
8430 return $text;
8433 // Splits on HTML tags. Each open/close/empty tag will be the first thing
8434 // and only tag in its 'line'
8435 preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
8437 $total_length = strlen($ending);
8438 $truncate = '';
8440 // This array stores information about open and close tags and their position
8441 // in the truncated string. Each item in the array is an object with fields
8442 // ->open (true if open), ->tag (tag name in lower case), and ->pos
8443 // (byte position in truncated text)
8444 $tagdetails = array();
8446 foreach ($lines as $line_matchings) {
8447 // if there is any html-tag in this line, handle it and add it (uncounted) to the output
8448 if (!empty($line_matchings[1])) {
8449 // if it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>)
8450 if (preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $line_matchings[1])) {
8451 // do nothing
8452 // if tag is a closing tag (f.e. </b>)
8453 } else if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $line_matchings[1], $tag_matchings)) {
8454 // record closing tag
8455 $tagdetails[] = (object)array('open'=>false,
8456 'tag'=>strtolower($tag_matchings[1]), 'pos'=>strlen($truncate));
8457 // if tag is an opening tag (f.e. <b>)
8458 } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $line_matchings[1], $tag_matchings)) {
8459 // record opening tag
8460 $tagdetails[] = (object)array('open'=>true,
8461 'tag'=>strtolower($tag_matchings[1]), 'pos'=>strlen($truncate));
8463 // add html-tag to $truncate'd text
8464 $truncate .= $line_matchings[1];
8467 // calculate the length of the plain text part of the line; handle entities as one character
8468 $content_length = strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $line_matchings[2]));
8469 if ($total_length+$content_length > $ideal) {
8470 // the number of characters which are left
8471 $left = $ideal - $total_length;
8472 $entities_length = 0;
8473 // search for html entities
8474 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)) {
8475 // calculate the real length of all entities in the legal range
8476 foreach ($entities[0] as $entity) {
8477 if ($entity[1]+1-$entities_length <= $left) {
8478 $left--;
8479 $entities_length += strlen($entity[0]);
8480 } else {
8481 // no more characters left
8482 break;
8486 $truncate .= substr($line_matchings[2], 0, $left+$entities_length);
8487 // maximum length is reached, so get off the loop
8488 break;
8489 } else {
8490 $truncate .= $line_matchings[2];
8491 $total_length += $content_length;
8494 // if the maximum length is reached, get off the loop
8495 if($total_length >= $ideal) {
8496 break;
8500 // if the words shouldn't be cut in the middle...
8501 if (!$exact) {
8502 // ...search the last occurence of a space...
8503 for ($k=strlen($truncate);$k>0;$k--) {
8504 if (!empty($truncate[$k]) && ($char = $truncate[$k])) {
8505 if ($char == '.' or $char == ' ') {
8506 $breakpos = $k+1;
8507 break;
8508 } else if (ord($char) >= 0xE0) { // Chinese/Japanese/Korean text
8509 $breakpos = $k; // can be truncated at any UTF-8
8510 break; // character boundary.
8515 if (isset($breakpos)) {
8516 // ...and cut the text in this position
8517 $truncate = substr($truncate, 0, $breakpos);
8521 // add the defined ending to the text
8522 $truncate .= $ending;
8524 // Now calculate the list of open html tags based on the truncate position
8525 $open_tags = array();
8526 foreach ($tagdetails as $taginfo) {
8527 if(isset($breakpos) && $taginfo->pos >= $breakpos) {
8528 // Don't include tags after we made the break!
8529 break;
8531 if($taginfo->open) {
8532 // add tag to the beginning of $open_tags list
8533 array_unshift($open_tags, $taginfo->tag);
8534 } else {
8535 $pos = array_search($taginfo->tag, array_reverse($open_tags, true)); // can have multiple exact same open tags, close the last one
8536 if ($pos !== false) {
8537 unset($open_tags[$pos]);
8542 // close all unclosed html-tags
8543 foreach ($open_tags as $tag) {
8544 $truncate .= '</' . $tag . '>';
8547 return $truncate;
8552 * Given dates in seconds, how many weeks is the date from startdate
8553 * The first week is 1, the second 2 etc ...
8555 * @todo Finish documenting this function
8557 * @uses WEEKSECS
8558 * @param int $startdate Timestamp for the start date
8559 * @param int $thedate Timestamp for the end date
8560 * @return string
8562 function getweek ($startdate, $thedate) {
8563 if ($thedate < $startdate) { // error
8564 return 0;
8567 return floor(($thedate - $startdate) / WEEKSECS) + 1;
8571 * returns a randomly generated password of length $maxlen. inspired by
8573 * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and
8574 * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254}
8576 * @global object
8577 * @param int $maxlen The maximum size of the password being generated.
8578 * @return string
8580 function generate_password($maxlen=10) {
8581 global $CFG;
8583 if (empty($CFG->passwordpolicy)) {
8584 $fillers = PASSWORD_DIGITS;
8585 $wordlist = file($CFG->wordlist);
8586 $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8587 $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8588 $filler1 = $fillers[rand(0, strlen($fillers) - 1)];
8589 $password = $word1 . $filler1 . $word2;
8590 } else {
8591 $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0;
8592 $digits = $CFG->minpassworddigits;
8593 $lower = $CFG->minpasswordlower;
8594 $upper = $CFG->minpasswordupper;
8595 $nonalphanum = $CFG->minpasswordnonalphanum;
8596 $total = $lower + $upper + $digits + $nonalphanum;
8597 // minlength should be the greater one of the two ( $minlen and $total )
8598 $minlen = $minlen < $total ? $total : $minlen;
8599 // maxlen can never be smaller than minlen
8600 $maxlen = $minlen > $maxlen ? $minlen : $maxlen;
8601 $additional = $maxlen - $total;
8603 // Make sure we have enough characters to fulfill
8604 // complexity requirements
8605 $passworddigits = PASSWORD_DIGITS;
8606 while ($digits > strlen($passworddigits)) {
8607 $passworddigits .= PASSWORD_DIGITS;
8609 $passwordlower = PASSWORD_LOWER;
8610 while ($lower > strlen($passwordlower)) {
8611 $passwordlower .= PASSWORD_LOWER;
8613 $passwordupper = PASSWORD_UPPER;
8614 while ($upper > strlen($passwordupper)) {
8615 $passwordupper .= PASSWORD_UPPER;
8617 $passwordnonalphanum = PASSWORD_NONALPHANUM;
8618 while ($nonalphanum > strlen($passwordnonalphanum)) {
8619 $passwordnonalphanum .= PASSWORD_NONALPHANUM;
8622 // Now mix and shuffle it all
8623 $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) .
8624 substr(str_shuffle ($passwordupper), 0, $upper) .
8625 substr(str_shuffle ($passworddigits), 0, $digits) .
8626 substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) .
8627 substr(str_shuffle ($passwordlower .
8628 $passwordupper .
8629 $passworddigits .
8630 $passwordnonalphanum), 0 , $additional));
8633 return substr ($password, 0, $maxlen);
8637 * Given a float, prints it nicely.
8638 * Localized floats must not be used in calculations!
8640 * @param float $float The float to print
8641 * @param int $places The number of decimal places to print.
8642 * @param bool $localized use localized decimal separator
8643 * @return string locale float
8645 function format_float($float, $decimalpoints=1, $localized=true) {
8646 if (is_null($float)) {
8647 return '';
8649 if ($localized) {
8650 return number_format($float, $decimalpoints, get_string('decsep', 'langconfig'), '');
8651 } else {
8652 return number_format($float, $decimalpoints, '.', '');
8657 * Converts locale specific floating point/comma number back to standard PHP float value
8658 * Do NOT try to do any math operations before this conversion on any user submitted floats!
8660 * @param string $locale_float locale aware float representation
8661 * @return float
8663 function unformat_float($locale_float) {
8664 $locale_float = trim($locale_float);
8666 if ($locale_float == '') {
8667 return null;
8670 $locale_float = str_replace(' ', '', $locale_float); // no spaces - those might be used as thousand separators
8672 return (float)str_replace(get_string('decsep', 'langconfig'), '.', $locale_float);
8676 * Given a simple array, this shuffles it up just like shuffle()
8677 * Unlike PHP's shuffle() this function works on any machine.
8679 * @param array $array The array to be rearranged
8680 * @return array
8682 function swapshuffle($array) {
8684 srand ((double) microtime() * 10000000);
8685 $last = count($array) - 1;
8686 for ($i=0;$i<=$last;$i++) {
8687 $from = rand(0,$last);
8688 $curr = $array[$i];
8689 $array[$i] = $array[$from];
8690 $array[$from] = $curr;
8692 return $array;
8696 * Like {@link swapshuffle()}, but works on associative arrays
8698 * @param array $array The associative array to be rearranged
8699 * @return array
8701 function swapshuffle_assoc($array) {
8703 $newarray = array();
8704 $newkeys = swapshuffle(array_keys($array));
8706 foreach ($newkeys as $newkey) {
8707 $newarray[$newkey] = $array[$newkey];
8709 return $newarray;
8713 * Given an arbitrary array, and a number of draws,
8714 * this function returns an array with that amount
8715 * of items. The indexes are retained.
8717 * @todo Finish documenting this function
8719 * @param array $array
8720 * @param int $draws
8721 * @return array
8723 function draw_rand_array($array, $draws) {
8724 srand ((double) microtime() * 10000000);
8726 $return = array();
8728 $last = count($array);
8730 if ($draws > $last) {
8731 $draws = $last;
8734 while ($draws > 0) {
8735 $last--;
8737 $keys = array_keys($array);
8738 $rand = rand(0, $last);
8740 $return[$keys[$rand]] = $array[$keys[$rand]];
8741 unset($array[$keys[$rand]]);
8743 $draws--;
8746 return $return;
8750 * Calculate the difference between two microtimes
8752 * @param string $a The first Microtime
8753 * @param string $b The second Microtime
8754 * @return string
8756 function microtime_diff($a, $b) {
8757 list($a_dec, $a_sec) = explode(' ', $a);
8758 list($b_dec, $b_sec) = explode(' ', $b);
8759 return $b_sec - $a_sec + $b_dec - $a_dec;
8763 * Given a list (eg a,b,c,d,e) this function returns
8764 * an array of 1->a, 2->b, 3->c etc
8766 * @param string $list The string to explode into array bits
8767 * @param string $separator The separator used within the list string
8768 * @return array The now assembled array
8770 function make_menu_from_list($list, $separator=',') {
8772 $array = array_reverse(explode($separator, $list), true);
8773 foreach ($array as $key => $item) {
8774 $outarray[$key+1] = trim($item);
8776 return $outarray;
8780 * Creates an array that represents all the current grades that
8781 * can be chosen using the given grading type.
8783 * Negative numbers
8784 * are scales, zero is no grade, and positive numbers are maximum
8785 * grades.
8787 * @todo Finish documenting this function or better deprecated this completely!
8789 * @param int $gradingtype
8790 * @return array
8792 function make_grades_menu($gradingtype) {
8793 global $DB;
8795 $grades = array();
8796 if ($gradingtype < 0) {
8797 if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) {
8798 return make_menu_from_list($scale->scale);
8800 } else if ($gradingtype > 0) {
8801 for ($i=$gradingtype; $i>=0; $i--) {
8802 $grades[$i] = $i .' / '. $gradingtype;
8804 return $grades;
8806 return $grades;
8810 * This function returns the number of activities
8811 * using scaleid in a courseid
8813 * @todo Finish documenting this function
8815 * @global object
8816 * @global object
8817 * @param int $courseid ?
8818 * @param int $scaleid ?
8819 * @return int
8821 function course_scale_used($courseid, $scaleid) {
8822 global $CFG, $DB;
8824 $return = 0;
8826 if (!empty($scaleid)) {
8827 if ($cms = get_course_mods($courseid)) {
8828 foreach ($cms as $cm) {
8829 //Check cm->name/lib.php exists
8830 if (file_exists($CFG->dirroot.'/mod/'.$cm->modname.'/lib.php')) {
8831 include_once($CFG->dirroot.'/mod/'.$cm->modname.'/lib.php');
8832 $function_name = $cm->modname.'_scale_used';
8833 if (function_exists($function_name)) {
8834 if ($function_name($cm->instance,$scaleid)) {
8835 $return++;
8842 // check if any course grade item makes use of the scale
8843 $return += $DB->count_records('grade_items', array('courseid'=>$courseid, 'scaleid'=>$scaleid));
8845 // check if any outcome in the course makes use of the scale
8846 $return += $DB->count_records_sql("SELECT COUNT('x')
8847 FROM {grade_outcomes_courses} goc,
8848 {grade_outcomes} go
8849 WHERE go.id = goc.outcomeid
8850 AND go.scaleid = ? AND goc.courseid = ?",
8851 array($scaleid, $courseid));
8853 return $return;
8857 * This function returns the number of activities
8858 * using scaleid in the entire site
8860 * @param int $scaleid
8861 * @param array $courses
8862 * @return int
8864 function site_scale_used($scaleid, &$courses) {
8865 $return = 0;
8867 if (!is_array($courses) || count($courses) == 0) {
8868 $courses = get_courses("all",false,"c.id,c.shortname");
8871 if (!empty($scaleid)) {
8872 if (is_array($courses) && count($courses) > 0) {
8873 foreach ($courses as $course) {
8874 $return += course_scale_used($course->id,$scaleid);
8878 return $return;
8882 * make_unique_id_code
8884 * @todo Finish documenting this function
8886 * @uses $_SERVER
8887 * @param string $extra Extra string to append to the end of the code
8888 * @return string
8890 function make_unique_id_code($extra='') {
8892 $hostname = 'unknownhost';
8893 if (!empty($_SERVER['HTTP_HOST'])) {
8894 $hostname = $_SERVER['HTTP_HOST'];
8895 } else if (!empty($_ENV['HTTP_HOST'])) {
8896 $hostname = $_ENV['HTTP_HOST'];
8897 } else if (!empty($_SERVER['SERVER_NAME'])) {
8898 $hostname = $_SERVER['SERVER_NAME'];
8899 } else if (!empty($_ENV['SERVER_NAME'])) {
8900 $hostname = $_ENV['SERVER_NAME'];
8903 $date = gmdate("ymdHis");
8905 $random = random_string(6);
8907 if ($extra) {
8908 return $hostname .'+'. $date .'+'. $random .'+'. $extra;
8909 } else {
8910 return $hostname .'+'. $date .'+'. $random;
8916 * Function to check the passed address is within the passed subnet
8918 * The parameter is a comma separated string of subnet definitions.
8919 * Subnet strings can be in one of three formats:
8920 * 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn (number of bits in net mask)
8921 * 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)
8922 * 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. (incomplete address, a bit non-technical ;-)
8923 * Code for type 1 modified from user posted comments by mediator at
8924 * {@link http://au.php.net/manual/en/function.ip2long.php}
8926 * @param string $addr The address you are checking
8927 * @param string $subnetstr The string of subnet addresses
8928 * @return bool
8930 function address_in_subnet($addr, $subnetstr) {
8932 if ($addr == '0.0.0.0') {
8933 return false;
8935 $subnets = explode(',', $subnetstr);
8936 $found = false;
8937 $addr = trim($addr);
8938 $addr = cleanremoteaddr($addr, false); // normalise
8939 if ($addr === null) {
8940 return false;
8942 $addrparts = explode(':', $addr);
8944 $ipv6 = strpos($addr, ':');
8946 foreach ($subnets as $subnet) {
8947 $subnet = trim($subnet);
8948 if ($subnet === '') {
8949 continue;
8952 if (strpos($subnet, '/') !== false) {
8953 ///1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn
8954 list($ip, $mask) = explode('/', $subnet);
8955 $mask = trim($mask);
8956 if (!is_number($mask)) {
8957 continue; // incorect mask number, eh?
8959 $ip = cleanremoteaddr($ip, false); // normalise
8960 if ($ip === null) {
8961 continue;
8963 if (strpos($ip, ':') !== false) {
8964 // IPv6
8965 if (!$ipv6) {
8966 continue;
8968 if ($mask > 128 or $mask < 0) {
8969 continue; // nonsense
8971 if ($mask == 0) {
8972 return true; // any address
8974 if ($mask == 128) {
8975 if ($ip === $addr) {
8976 return true;
8978 continue;
8980 $ipparts = explode(':', $ip);
8981 $modulo = $mask % 16;
8982 $ipnet = array_slice($ipparts, 0, ($mask-$modulo)/16);
8983 $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16);
8984 if (implode(':', $ipnet) === implode(':', $addrnet)) {
8985 if ($modulo == 0) {
8986 return true;
8988 $pos = ($mask-$modulo)/16;
8989 $ipnet = hexdec($ipparts[$pos]);
8990 $addrnet = hexdec($addrparts[$pos]);
8991 $mask = 0xffff << (16 - $modulo);
8992 if (($addrnet & $mask) == ($ipnet & $mask)) {
8993 return true;
8997 } else {
8998 // IPv4
8999 if ($ipv6) {
9000 continue;
9002 if ($mask > 32 or $mask < 0) {
9003 continue; // nonsense
9005 if ($mask == 0) {
9006 return true;
9008 if ($mask == 32) {
9009 if ($ip === $addr) {
9010 return true;
9012 continue;
9014 $mask = 0xffffffff << (32 - $mask);
9015 if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
9016 return true;
9020 } else if (strpos($subnet, '-') !== false) {
9021 /// 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.
9022 $parts = explode('-', $subnet);
9023 if (count($parts) != 2) {
9024 continue;
9027 if (strpos($subnet, ':') !== false) {
9028 // IPv6
9029 if (!$ipv6) {
9030 continue;
9032 $ipstart = cleanremoteaddr(trim($parts[0]), false); // normalise
9033 if ($ipstart === null) {
9034 continue;
9036 $ipparts = explode(':', $ipstart);
9037 $start = hexdec(array_pop($ipparts));
9038 $ipparts[] = trim($parts[1]);
9039 $ipend = cleanremoteaddr(implode(':', $ipparts), false); // normalise
9040 if ($ipend === null) {
9041 continue;
9043 $ipparts[7] = '';
9044 $ipnet = implode(':', $ipparts);
9045 if (strpos($addr, $ipnet) !== 0) {
9046 continue;
9048 $ipparts = explode(':', $ipend);
9049 $end = hexdec($ipparts[7]);
9051 $addrend = hexdec($addrparts[7]);
9053 if (($addrend >= $start) and ($addrend <= $end)) {
9054 return true;
9057 } else {
9058 // IPv4
9059 if ($ipv6) {
9060 continue;
9062 $ipstart = cleanremoteaddr(trim($parts[0]), false); // normalise
9063 if ($ipstart === null) {
9064 continue;
9066 $ipparts = explode('.', $ipstart);
9067 $ipparts[3] = trim($parts[1]);
9068 $ipend = cleanremoteaddr(implode('.', $ipparts), false); // normalise
9069 if ($ipend === null) {
9070 continue;
9073 if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
9074 return true;
9078 } else {
9079 /// 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
9080 if (strpos($subnet, ':') !== false) {
9081 // IPv6
9082 if (!$ipv6) {
9083 continue;
9085 $parts = explode(':', $subnet);
9086 $count = count($parts);
9087 if ($parts[$count-1] === '') {
9088 unset($parts[$count-1]); // trim trailing :
9089 $count--;
9090 $subnet = implode('.', $parts);
9092 $isip = cleanremoteaddr($subnet, false); // normalise
9093 if ($isip !== null) {
9094 if ($isip === $addr) {
9095 return true;
9097 continue;
9098 } else if ($count > 8) {
9099 continue;
9101 $zeros = array_fill(0, 8-$count, '0');
9102 $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16);
9103 if (address_in_subnet($addr, $subnet)) {
9104 return true;
9107 } else {
9108 // IPv4
9109 if ($ipv6) {
9110 continue;
9112 $parts = explode('.', $subnet);
9113 $count = count($parts);
9114 if ($parts[$count-1] === '') {
9115 unset($parts[$count-1]); // trim trailing .
9116 $count--;
9117 $subnet = implode('.', $parts);
9119 if ($count == 4) {
9120 $subnet = cleanremoteaddr($subnet, false); // normalise
9121 if ($subnet === $addr) {
9122 return true;
9124 continue;
9125 } else if ($count > 4) {
9126 continue;
9128 $zeros = array_fill(0, 4-$count, '0');
9129 $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8);
9130 if (address_in_subnet($addr, $subnet)) {
9131 return true;
9137 return false;
9141 * For outputting debugging info
9143 * @uses STDOUT
9144 * @param string $string The string to write
9145 * @param string $eol The end of line char(s) to use
9146 * @param string $sleep Period to make the application sleep
9147 * This ensures any messages have time to display before redirect
9149 function mtrace($string, $eol="\n", $sleep=0) {
9151 if (defined('STDOUT')) {
9152 fwrite(STDOUT, $string.$eol);
9153 } else {
9154 echo $string . $eol;
9157 flush();
9159 //delay to keep message on user's screen in case of subsequent redirect
9160 if ($sleep) {
9161 sleep($sleep);
9166 * Replace 1 or more slashes or backslashes to 1 slash
9168 * @param string $path The path to strip
9169 * @return string the path with double slashes removed
9171 function cleardoubleslashes ($path) {
9172 return preg_replace('/(\/|\\\){1,}/','/',$path);
9176 * Is current ip in give list?
9178 * @param string $list
9179 * @return bool
9181 function remoteip_in_list($list){
9182 $inlist = false;
9183 $client_ip = getremoteaddr(null);
9185 if(!$client_ip){
9186 // ensure access on cli
9187 return true;
9190 $list = explode("\n", $list);
9191 foreach($list as $subnet) {
9192 $subnet = trim($subnet);
9193 if (address_in_subnet($client_ip, $subnet)) {
9194 $inlist = true;
9195 break;
9198 return $inlist;
9202 * Returns most reliable client address
9204 * @global object
9205 * @param string $default If an address can't be determined, then return this
9206 * @return string The remote IP address
9208 function getremoteaddr($default='0.0.0.0') {
9209 global $CFG;
9211 if (empty($CFG->getremoteaddrconf)) {
9212 // This will happen, for example, before just after the upgrade, as the
9213 // user is redirected to the admin screen.
9214 $variablestoskip = 0;
9215 } else {
9216 $variablestoskip = $CFG->getremoteaddrconf;
9218 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) {
9219 if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
9220 $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']);
9221 return $address ? $address : $default;
9224 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
9225 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
9226 $address = cleanremoteaddr($_SERVER['HTTP_X_FORWARDED_FOR']);
9227 return $address ? $address : $default;
9230 if (!empty($_SERVER['REMOTE_ADDR'])) {
9231 $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']);
9232 return $address ? $address : $default;
9233 } else {
9234 return $default;
9239 * Cleans an ip address. Internal addresses are now allowed.
9240 * (Originally local addresses were not allowed.)
9242 * @param string $addr IPv4 or IPv6 address
9243 * @param bool $compress use IPv6 address compression
9244 * @return string normalised ip address string, null if error
9246 function cleanremoteaddr($addr, $compress=false) {
9247 $addr = trim($addr);
9249 //TODO: maybe add a separate function is_addr_public() or something like this
9251 if (strpos($addr, ':') !== false) {
9252 // can be only IPv6
9253 $parts = explode(':', $addr);
9254 $count = count($parts);
9256 if (strpos($parts[$count-1], '.') !== false) {
9257 //legacy ipv4 notation
9258 $last = array_pop($parts);
9259 $ipv4 = cleanremoteaddr($last, true);
9260 if ($ipv4 === null) {
9261 return null;
9263 $bits = explode('.', $ipv4);
9264 $parts[] = dechex($bits[0]).dechex($bits[1]);
9265 $parts[] = dechex($bits[2]).dechex($bits[3]);
9266 $count = count($parts);
9267 $addr = implode(':', $parts);
9270 if ($count < 3 or $count > 8) {
9271 return null; // severly malformed
9274 if ($count != 8) {
9275 if (strpos($addr, '::') === false) {
9276 return null; // malformed
9278 // uncompress ::
9279 $insertat = array_search('', $parts, true);
9280 $missing = array_fill(0, 1 + 8 - $count, '0');
9281 array_splice($parts, $insertat, 1, $missing);
9282 foreach ($parts as $key=>$part) {
9283 if ($part === '') {
9284 $parts[$key] = '0';
9289 $adr = implode(':', $parts);
9290 if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) {
9291 return null; // incorrect format - sorry
9294 // normalise 0s and case
9295 $parts = array_map('hexdec', $parts);
9296 $parts = array_map('dechex', $parts);
9298 $result = implode(':', $parts);
9300 if (!$compress) {
9301 return $result;
9304 if ($result === '0:0:0:0:0:0:0:0') {
9305 return '::'; // all addresses
9308 $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1);
9309 if ($compressed !== $result) {
9310 return $compressed;
9313 $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
9314 if ($compressed !== $result) {
9315 return $compressed;
9318 $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
9319 if ($compressed !== $result) {
9320 return $compressed;
9323 return $result;
9326 // first get all things that look like IPv4 addresses
9327 $parts = array();
9328 if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
9329 return null;
9331 unset($parts[0]);
9333 foreach ($parts as $key=>$match) {
9334 if ($match > 255) {
9335 return null;
9337 $parts[$key] = (int)$match; // normalise 0s
9340 return implode('.', $parts);
9344 * This function will make a complete copy of anything it's given,
9345 * regardless of whether it's an object or not.
9347 * @param mixed $thing Something you want cloned
9348 * @return mixed What ever it is you passed it
9350 function fullclone($thing) {
9351 return unserialize(serialize($thing));
9356 * This function expects to called during shutdown
9357 * should be set via register_shutdown_function()
9358 * in lib/setup.php .
9360 * @return void
9362 function moodle_request_shutdown() {
9363 global $CFG;
9365 // help apache server if possible
9366 $apachereleasemem = false;
9367 if (function_exists('apache_child_terminate') && function_exists('memory_get_usage')
9368 && ini_get_bool('child_terminate')) {
9370 $limit = (empty($CFG->apachemaxmem) ? 64*1024*1024 : $CFG->apachemaxmem); //64MB default
9371 if (memory_get_usage() > get_real_size($limit)) {
9372 $apachereleasemem = $limit;
9373 @apache_child_terminate();
9377 // deal with perf logging
9378 if (defined('MDL_PERF') || (!empty($CFG->perfdebug) and $CFG->perfdebug > 7)) {
9379 if ($apachereleasemem) {
9380 error_log('Mem usage over '.$apachereleasemem.': marking Apache child for reaping.');
9382 if (defined('MDL_PERFTOLOG')) {
9383 $perf = get_performance_info();
9384 error_log("PERF: " . $perf['txt']);
9386 if (defined('MDL_PERFINC')) {
9387 $inc = get_included_files();
9388 $ts = 0;
9389 foreach($inc as $f) {
9390 if (preg_match(':^/:', $f)) {
9391 $fs = filesize($f);
9392 $ts += $fs;
9393 $hfs = display_size($fs);
9394 error_log(substr($f,strlen($CFG->dirroot)) . " size: $fs ($hfs)"
9395 , NULL, NULL, 0);
9396 } else {
9397 error_log($f , NULL, NULL, 0);
9400 if ($ts > 0 ) {
9401 $hts = display_size($ts);
9402 error_log("Total size of files included: $ts ($hts)");
9409 * If new messages are waiting for the current user, then insert
9410 * JavaScript to pop up the messaging window into the page
9412 * @global moodle_page $PAGE
9413 * @return void
9415 function message_popup_window() {
9416 global $USER, $DB, $PAGE, $CFG, $SITE;
9418 if (!$PAGE->get_popup_notification_allowed() || empty($CFG->messaging)) {
9419 return;
9422 if (!isloggedin() || isguestuser()) {
9423 return;
9426 if (!isset($USER->message_lastpopup)) {
9427 $USER->message_lastpopup = 0;
9428 } else if ($USER->message_lastpopup > (time()-120)) {
9429 //dont run the query to check whether to display a popup if its been run in the last 2 minutes
9430 return;
9433 //a quick query to check whether the user has new messages
9434 $messagecount = $DB->count_records('message', array('useridto' => $USER->id));
9435 if ($messagecount<1) {
9436 return;
9439 //got unread messages so now do another query that joins with the user table
9440 $messagesql = "SELECT m.id, m.smallmessage, m.fullmessageformat, m.notification, u.firstname, u.lastname
9441 FROM {message} m
9442 JOIN {message_working} mw ON m.id=mw.unreadmessageid
9443 JOIN {message_processors} p ON mw.processorid=p.id
9444 JOIN {user} u ON m.useridfrom=u.id
9445 WHERE m.useridto = :userid
9446 AND p.name='popup'";
9448 //if the user was last notified over an hour ago we can renotify them of old messages
9449 //so don't worry about when the new message was sent
9450 $lastnotifiedlongago = $USER->message_lastpopup < (time()-3600);
9451 if (!$lastnotifiedlongago) {
9452 $messagesql .= 'AND m.timecreated > :lastpopuptime';
9455 $message_users = $DB->get_records_sql($messagesql, array('userid'=>$USER->id, 'lastpopuptime'=>$USER->message_lastpopup));
9457 //if we have new messages to notify the user about
9458 if (!empty($message_users)) {
9460 $strmessages = '';
9461 if (count($message_users)>1) {
9462 $strmessages = get_string('unreadnewmessages', 'message', count($message_users));
9463 } else {
9464 $message_users = reset($message_users);
9466 //show who the message is from if its not a notification
9467 if (!$message_users->notification) {
9468 $strmessages = get_string('unreadnewmessage', 'message', fullname($message_users) );
9471 //try to display the small version of the message
9472 $smallmessage = null;
9473 if (!empty($message_users->smallmessage)) {
9474 //display the first 200 chars of the message in the popup
9475 $textlib = textlib_get_instance();
9476 $smallmessage = null;
9477 if ($textlib->strlen($message_users->smallmessage) > 200) {
9478 $smallmessage = $textlib->substr($message_users->smallmessage,0,200).'...';
9479 } else {
9480 $smallmessage = $message_users->smallmessage;
9483 //prevent html symbols being displayed
9484 if ($message_users->fullmessageformat == FORMAT_HTML) {
9485 $smallmessage = html_to_text($smallmessage);
9486 } else {
9487 $smallmessage = s($smallmessage);
9489 } else if ($message_users->notification) {
9490 //its a notification with no smallmessage so just say they have a notification
9491 $smallmessage = get_string('unreadnewnotification', 'message');
9493 if (!empty($smallmessage)) {
9494 $strmessages .= '<div id="usermessage">'.s($smallmessage).'</div>';
9498 $strgomessage = get_string('gotomessages', 'message');
9499 $strstaymessage = get_string('ignore','admin');
9501 $url = $CFG->wwwroot.'/message/index.php';
9502 $content = html_writer::start_tag('div', array('id'=>'newmessageoverlay','class'=>'mdl-align')).
9503 html_writer::start_tag('div', array('id'=>'newmessagetext')).
9504 $strmessages.
9505 html_writer::end_tag('div').
9507 html_writer::start_tag('div', array('id'=>'newmessagelinks')).
9508 html_writer::link($url, $strgomessage, array('id'=>'notificationyes')).'&nbsp;&nbsp;&nbsp;'.
9509 html_writer::link('', $strstaymessage, array('id'=>'notificationno')).
9510 html_writer::end_tag('div');
9511 html_writer::end_tag('div');
9513 $PAGE->requires->js_init_call('M.core_message.init_notification', array('', $content, $url));
9515 $USER->message_lastpopup = time();
9520 * Used to make sure that $min <= $value <= $max
9522 * Make sure that value is between min, and max
9524 * @param int $min The minimum value
9525 * @param int $value The value to check
9526 * @param int $max The maximum value
9528 function bounded_number($min, $value, $max) {
9529 if($value < $min) {
9530 return $min;
9532 if($value > $max) {
9533 return $max;
9535 return $value;
9539 * Check if there is a nested array within the passed array
9541 * @param array $array
9542 * @return bool true if there is a nested array false otherwise
9544 function array_is_nested($array) {
9545 foreach ($array as $value) {
9546 if (is_array($value)) {
9547 return true;
9550 return false;
9554 * get_performance_info() pairs up with init_performance_info()
9555 * loaded in setup.php. Returns an array with 'html' and 'txt'
9556 * values ready for use, and each of the individual stats provided
9557 * separately as well.
9559 * @global object
9560 * @global object
9561 * @global object
9562 * @return array
9564 function get_performance_info() {
9565 global $CFG, $PERF, $DB, $PAGE;
9567 $info = array();
9568 $info['html'] = ''; // holds userfriendly HTML representation
9569 $info['txt'] = me() . ' '; // holds log-friendly representation
9571 $info['realtime'] = microtime_diff($PERF->starttime, microtime());
9573 $info['html'] .= '<span class="timeused">'.$info['realtime'].' secs</span> ';
9574 $info['txt'] .= 'time: '.$info['realtime'].'s ';
9576 if (function_exists('memory_get_usage')) {
9577 $info['memory_total'] = memory_get_usage();
9578 $info['memory_growth'] = memory_get_usage() - $PERF->startmemory;
9579 $info['html'] .= '<span class="memoryused">RAM: '.display_size($info['memory_total']).'</span> ';
9580 $info['txt'] .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '.$info['memory_growth'].'B ('.display_size($info['memory_growth']).') ';
9583 if (function_exists('memory_get_peak_usage')) {
9584 $info['memory_peak'] = memory_get_peak_usage();
9585 $info['html'] .= '<span class="memoryused">RAM peak: '.display_size($info['memory_peak']).'</span> ';
9586 $info['txt'] .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') ';
9589 $inc = get_included_files();
9590 //error_log(print_r($inc,1));
9591 $info['includecount'] = count($inc);
9592 $info['html'] .= '<span class="included">Included '.$info['includecount'].' files</span> ';
9593 $info['txt'] .= 'includecount: '.$info['includecount'].' ';
9595 $filtermanager = filter_manager::instance();
9596 if (method_exists($filtermanager, 'get_performance_summary')) {
9597 list($filterinfo, $nicenames) = $filtermanager->get_performance_summary();
9598 $info = array_merge($filterinfo, $info);
9599 foreach ($filterinfo as $key => $value) {
9600 $info['html'] .= "<span class='$key'>$nicenames[$key]: $value </span> ";
9601 $info['txt'] .= "$key: $value ";
9605 $stringmanager = get_string_manager();
9606 if (method_exists($stringmanager, 'get_performance_summary')) {
9607 list($filterinfo, $nicenames) = $stringmanager->get_performance_summary();
9608 $info = array_merge($filterinfo, $info);
9609 foreach ($filterinfo as $key => $value) {
9610 $info['html'] .= "<span class='$key'>$nicenames[$key]: $value </span> ";
9611 $info['txt'] .= "$key: $value ";
9615 $jsmodules = $PAGE->requires->get_loaded_modules();
9616 if ($jsmodules) {
9617 $yuicount = 0;
9618 $othercount = 0;
9619 $details = '';
9620 foreach ($jsmodules as $module => $backtraces) {
9621 if (strpos($module, 'yui') === 0) {
9622 $yuicount += 1;
9623 } else {
9624 $othercount += 1;
9626 $details .= "<div class='yui-module'><p>$module</p>";
9627 foreach ($backtraces as $backtrace) {
9628 $details .= "<div class='backtrace'>$backtrace</div>";
9630 $details .= '</div>';
9632 $info['html'] .= "<span class='includedyuimodules'>Included YUI modules: $yuicount</span> ";
9633 $info['txt'] .= "includedyuimodules: $yuicount ";
9634 $info['html'] .= "<span class='includedjsmodules'>Other JavaScript modules: $othercount</span> ";
9635 $info['txt'] .= "includedjsmodules: $othercount ";
9636 // Slightly odd to output the details in a display: none div. The point
9637 // Is that it takes a lot of space, and if you care you can reveal it
9638 // using firebug.
9639 $info['html'] .= '<div id="yui-module-debug" class="notifytiny">'.$details.'</div>';
9642 if (!empty($PERF->logwrites)) {
9643 $info['logwrites'] = $PERF->logwrites;
9644 $info['html'] .= '<span class="logwrites">Log DB writes '.$info['logwrites'].'</span> ';
9645 $info['txt'] .= 'logwrites: '.$info['logwrites'].' ';
9648 $info['dbqueries'] = $DB->perf_get_reads().'/'.($DB->perf_get_writes() - $PERF->logwrites);
9649 $info['html'] .= '<span class="dbqueries">DB reads/writes: '.$info['dbqueries'].'</span> ';
9650 $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' ';
9652 if (!empty($PERF->profiling) && $PERF->profiling) {
9653 require_once($CFG->dirroot .'/lib/profilerlib.php');
9654 $info['html'] .= '<span class="profilinginfo">'.Profiler::get_profiling(array('-R')).'</span>';
9657 if (function_exists('posix_times')) {
9658 $ptimes = posix_times();
9659 if (is_array($ptimes)) {
9660 foreach ($ptimes as $key => $val) {
9661 $info[$key] = $ptimes[$key] - $PERF->startposixtimes[$key];
9663 $info['html'] .= "<span class=\"posixtimes\">ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</span> ";
9664 $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] ";
9668 // Grab the load average for the last minute
9669 // /proc will only work under some linux configurations
9670 // while uptime is there under MacOSX/Darwin and other unices
9671 if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) {
9672 list($server_load) = explode(' ', $loadavg[0]);
9673 unset($loadavg);
9674 } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) {
9675 if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) {
9676 $server_load = $matches[1];
9677 } else {
9678 trigger_error('Could not parse uptime output!');
9681 if (!empty($server_load)) {
9682 $info['serverload'] = $server_load;
9683 $info['html'] .= '<span class="serverload">Load average: '.$info['serverload'].'</span> ';
9684 $info['txt'] .= "serverload: {$info['serverload']} ";
9687 // Display size of session if session started
9688 if (session_id()) {
9689 $info['sessionsize'] = display_size(strlen(session_encode()));
9690 $info['html'] .= '<span class="sessionsize">Session: ' . $info['sessionsize'] . '</span> ';
9691 $info['txt'] .= "Session: {$info['sessionsize']} ";
9694 /* if (isset($rcache->hits) && isset($rcache->misses)) {
9695 $info['rcachehits'] = $rcache->hits;
9696 $info['rcachemisses'] = $rcache->misses;
9697 $info['html'] .= '<span class="rcache">Record cache hit/miss ratio : '.
9698 "{$rcache->hits}/{$rcache->misses}</span> ";
9699 $info['txt'] .= 'rcache: '.
9700 "{$rcache->hits}/{$rcache->misses} ";
9702 $info['html'] = '<div class="performanceinfo siteinfo">'.$info['html'].'</div>';
9703 return $info;
9707 * @todo Document this function linux people
9709 function apd_get_profiling() {
9710 return shell_exec('pprofp -u ' . ini_get('apd.dumpdir') . '/pprof.' . getmypid() . '.*');
9714 * Delete directory or only it's content
9716 * @param string $dir directory path
9717 * @param bool $content_only
9718 * @return bool success, true also if dir does not exist
9720 function remove_dir($dir, $content_only=false) {
9721 if (!file_exists($dir)) {
9722 // nothing to do
9723 return true;
9725 $handle = opendir($dir);
9726 $result = true;
9727 while (false!==($item = readdir($handle))) {
9728 if($item != '.' && $item != '..') {
9729 if(is_dir($dir.'/'.$item)) {
9730 $result = remove_dir($dir.'/'.$item) && $result;
9731 }else{
9732 $result = unlink($dir.'/'.$item) && $result;
9736 closedir($handle);
9737 if ($content_only) {
9738 return $result;
9740 return rmdir($dir); // if anything left the result will be false, no need for && $result
9744 * Detect if an object or a class contains a given property
9745 * will take an actual object or the name of a class
9747 * @param mix $obj Name of class or real object to test
9748 * @param string $property name of property to find
9749 * @return bool true if property exists
9751 function object_property_exists( $obj, $property ) {
9752 if (is_string( $obj )) {
9753 $properties = get_class_vars( $obj );
9755 else {
9756 $properties = get_object_vars( $obj );
9758 return array_key_exists( $property, $properties );
9763 * Detect a custom script replacement in the data directory that will
9764 * replace an existing moodle script
9766 * @param string $urlpath path to the original script
9767 * @return string|bool full path name if a custom script exists, false if no custom script exists
9769 function custom_script_path($urlpath='') {
9770 global $CFG;
9772 // set default $urlpath, if necessary
9773 if (empty($urlpath)) {
9774 $urlpath = qualified_me(); // e.g. http://www.this-server.com/moodle/this-script.php
9777 // $urlpath is invalid if it is empty or does not start with the Moodle wwwroot
9778 if (empty($urlpath) or (strpos($urlpath, $CFG->wwwroot) === false )) {
9779 return false;
9782 // replace wwwroot with the path to the customscripts folder and clean path
9783 $scriptpath = $CFG->customscripts . clean_param(substr($urlpath, strlen($CFG->wwwroot)), PARAM_PATH);
9785 // remove the query string, if any
9786 if (($strpos = strpos($scriptpath, '?')) !== false) {
9787 $scriptpath = substr($scriptpath, 0, $strpos);
9790 // remove trailing slashes, if any
9791 $scriptpath = rtrim($scriptpath, '/\\');
9793 // append index.php, if necessary
9794 if (is_dir($scriptpath)) {
9795 $scriptpath .= '/index.php';
9798 // check the custom script exists
9799 if (file_exists($scriptpath)) {
9800 return $scriptpath;
9801 } else {
9802 return false;
9807 * Returns whether or not the user object is a remote MNET user. This function
9808 * is in moodlelib because it does not rely on loading any of the MNET code.
9810 * @global object
9811 * @param object $user A valid user object
9812 * @return bool True if the user is from a remote Moodle.
9814 function is_mnet_remote_user($user) {
9815 global $CFG;
9817 if (!isset($CFG->mnet_localhost_id)) {
9818 include_once $CFG->dirroot . '/mnet/lib.php';
9819 $env = new mnet_environment();
9820 $env->init();
9821 unset($env);
9824 return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id);
9828 * This function will search for browser prefereed languages, setting Moodle
9829 * to use the best one available if $SESSION->lang is undefined
9831 * @global object
9832 * @global object
9833 * @global object
9835 function setup_lang_from_browser() {
9837 global $CFG, $SESSION, $USER;
9839 if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) {
9840 // Lang is defined in session or user profile, nothing to do
9841 return;
9844 if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do
9845 return;
9848 /// Extract and clean langs from headers
9849 $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
9850 $rawlangs = str_replace('-', '_', $rawlangs); // we are using underscores
9851 $rawlangs = explode(',', $rawlangs); // Convert to array
9852 $langs = array();
9854 $order = 1.0;
9855 foreach ($rawlangs as $lang) {
9856 if (strpos($lang, ';') === false) {
9857 $langs[(string)$order] = $lang;
9858 $order = $order-0.01;
9859 } else {
9860 $parts = explode(';', $lang);
9861 $pos = strpos($parts[1], '=');
9862 $langs[substr($parts[1], $pos+1)] = $parts[0];
9865 krsort($langs, SORT_NUMERIC);
9867 /// Look for such langs under standard locations
9868 foreach ($langs as $lang) {
9869 $lang = strtolower(clean_param($lang, PARAM_SAFEDIR)); // clean it properly for include
9870 if (get_string_manager()->translation_exists($lang, false)) {
9871 $SESSION->lang = $lang; /// Lang exists, set it in session
9872 break; /// We have finished. Go out
9875 return;
9879 * check if $url matches anything in proxybypass list
9881 * any errors just result in the proxy being used (least bad)
9883 * @global object
9884 * @param string $url url to check
9885 * @return boolean true if we should bypass the proxy
9887 function is_proxybypass( $url ) {
9888 global $CFG;
9890 // sanity check
9891 if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) {
9892 return false;
9895 // get the host part out of the url
9896 if (!$host = parse_url( $url, PHP_URL_HOST )) {
9897 return false;
9900 // get the possible bypass hosts into an array
9901 $matches = explode( ',', $CFG->proxybypass );
9903 // check for a match
9904 // (IPs need to match the left hand side and hosts the right of the url,
9905 // but we can recklessly check both as there can't be a false +ve)
9906 $bypass = false;
9907 foreach ($matches as $match) {
9908 $match = trim($match);
9910 // try for IP match (Left side)
9911 $lhs = substr($host,0,strlen($match));
9912 if (strcasecmp($match,$lhs)==0) {
9913 return true;
9916 // try for host match (Right side)
9917 $rhs = substr($host,-strlen($match));
9918 if (strcasecmp($match,$rhs)==0) {
9919 return true;
9923 // nothing matched.
9924 return false;
9928 ////////////////////////////////////////////////////////////////////////////////
9931 * Check if the passed navigation is of the new style
9933 * @param mixed $navigation
9934 * @return bool true for yes false for no
9936 function is_newnav($navigation) {
9937 if (is_array($navigation) && !empty($navigation['newnav'])) {
9938 return true;
9939 } else {
9940 return false;
9945 * Checks whether the given variable name is defined as a variable within the given object.
9947 * This will NOT work with stdClass objects, which have no class variables.
9949 * @param string $var The variable name
9950 * @param object $object The object to check
9951 * @return boolean
9953 function in_object_vars($var, $object) {
9954 $class_vars = get_class_vars(get_class($object));
9955 $class_vars = array_keys($class_vars);
9956 return in_array($var, $class_vars);
9960 * Returns an array without repeated objects.
9961 * This function is similar to array_unique, but for arrays that have objects as values
9963 * @param array $array
9964 * @param bool $keep_key_assoc
9965 * @return array
9967 function object_array_unique($array, $keep_key_assoc = true) {
9968 $duplicate_keys = array();
9969 $tmp = array();
9971 foreach ($array as $key=>$val) {
9972 // convert objects to arrays, in_array() does not support objects
9973 if (is_object($val)) {
9974 $val = (array)$val;
9977 if (!in_array($val, $tmp)) {
9978 $tmp[] = $val;
9979 } else {
9980 $duplicate_keys[] = $key;
9984 foreach ($duplicate_keys as $key) {
9985 unset($array[$key]);
9988 return $keep_key_assoc ? $array : array_values($array);
9992 * Is a userid the primary administrator?
9994 * @param int $userid int id of user to check
9995 * @return boolean
9997 function is_primary_admin($userid){
9998 $primaryadmin = get_admin();
10000 if($userid == $primaryadmin->id){
10001 return true;
10002 }else{
10003 return false;
10008 * Returns the site identifier
10010 * @global object
10011 * @return string $CFG->siteidentifier, first making sure it is properly initialised.
10013 function get_site_identifier() {
10014 global $CFG;
10015 // Check to see if it is missing. If so, initialise it.
10016 if (empty($CFG->siteidentifier)) {
10017 set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']);
10019 // Return it.
10020 return $CFG->siteidentifier;
10024 * Check whether the given password has no more than the specified
10025 * number of consecutive identical characters.
10027 * @param string $password password to be checked against the password policy
10028 * @param integer $maxchars maximum number of consecutive identical characters
10030 function check_consecutive_identical_characters($password, $maxchars) {
10032 if ($maxchars < 1) {
10033 return true; // 0 is to disable this check
10035 if (strlen($password) <= $maxchars) {
10036 return true; // too short to fail this test
10039 $previouschar = '';
10040 $consecutivecount = 1;
10041 foreach (str_split($password) as $char) {
10042 if ($char != $previouschar) {
10043 $consecutivecount = 1;
10045 else {
10046 $consecutivecount++;
10047 if ($consecutivecount > $maxchars) {
10048 return false; // check failed already
10052 $previouschar = $char;
10055 return true;
10059 * helper function to do partial function binding
10060 * so we can use it for preg_replace_callback, for example
10061 * this works with php functions, user functions, static methods and class methods
10062 * it returns you a callback that you can pass on like so:
10064 * $callback = partial('somefunction', $arg1, $arg2);
10065 * or
10066 * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
10067 * or even
10068 * $obj = new someclass();
10069 * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2);
10071 * and then the arguments that are passed through at calltime are appended to the argument list.
10073 * @param mixed $function a php callback
10074 * $param mixed $arg1.. $argv arguments to partially bind with
10076 * @return callback
10078 function partial() {
10079 if (!class_exists('partial')) {
10080 class partial{
10081 var $values = array();
10082 var $func;
10084 function __construct($func, $args) {
10085 $this->values = $args;
10086 $this->func = $func;
10089 function method() {
10090 $args = func_get_args();
10091 return call_user_func_array($this->func, array_merge($this->values, $args));
10095 $args = func_get_args();
10096 $func = array_shift($args);
10097 $p = new partial($func, $args);
10098 return array($p, 'method');
10102 * helper function to load up and initialise the mnet environment
10103 * this must be called before you use mnet functions.
10105 * @return mnet_environment the equivalent of old $MNET global
10107 function get_mnet_environment() {
10108 global $CFG;
10109 require_once($CFG->dirroot . '/mnet/lib.php');
10110 static $instance = null;
10111 if (empty($instance)) {
10112 $instance = new mnet_environment();
10113 $instance->init();
10115 return $instance;
10119 * during xmlrpc server code execution, any code wishing to access
10120 * information about the remote peer must use this to get it.
10122 * @return mnet_remote_client the equivalent of old $MNET_REMOTE_CLIENT global
10124 function get_mnet_remote_client() {
10125 if (!defined('MNET_SERVER')) {
10126 debugging(get_string('notinxmlrpcserver', 'mnet'));
10127 return false;
10129 global $MNET_REMOTE_CLIENT;
10130 if (isset($MNET_REMOTE_CLIENT)) {
10131 return $MNET_REMOTE_CLIENT;
10133 return false;
10137 * during the xmlrpc server code execution, this will be called
10138 * to setup the object returned by {@see get_mnet_remote_client}
10140 * @param mnet_remote_client $client the client to set up
10142 function set_mnet_remote_client($client) {
10143 if (!defined('MNET_SERVER')) {
10144 throw new moodle_exception('notinxmlrpcserver', 'mnet');
10146 global $MNET_REMOTE_CLIENT;
10147 $MNET_REMOTE_CLIENT = $client;
10151 * return the jump url for a given remote user
10152 * this is used for rewriting forum post links in emails, etc
10154 * @param stdclass $user the user to get the idp url for
10156 function mnet_get_idp_jump_url($user) {
10157 global $CFG;
10159 static $mnetjumps = array();
10160 if (!array_key_exists($user->mnethostid, $mnetjumps)) {
10161 $idp = mnet_get_peer_host($user->mnethostid);
10162 $idpjumppath = mnet_get_app_jumppath($idp->applicationid);
10163 $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl=';
10165 return $mnetjumps[$user->mnethostid];
10169 * Gets the homepage to use for the current user
10171 * @return int One of HOMEPAGE_*
10173 function get_home_page() {
10174 global $CFG;
10176 if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) {
10177 if ($CFG->defaulthomepage == HOMEPAGE_MY) {
10178 return HOMEPAGE_MY;
10179 } else {
10180 return (int)get_user_preferences('user_home_page_preference', HOMEPAGE_MY);
10183 return HOMEPAGE_SITE;