Merge branch 'wip-mdl-32234-m21' of git://github.com/rajeshtaneja/moodle into MOODLE_...
[moodle.git] / lib / moodlelib.php
blob14570a6e70734535cbf04abfede8e1fc38742c34
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
8140 function notify_login_failures() {
8141 global $CFG, $DB, $OUTPUT;
8143 $recip = get_users_from_config($CFG->notifyloginfailures, 'moodle/site:config');
8145 if (empty($CFG->lastnotifyfailure)) {
8146 $CFG->lastnotifyfailure=0;
8149 // we need to deal with the threshold stuff first.
8150 if (empty($CFG->notifyloginthreshold)) {
8151 $CFG->notifyloginthreshold = 10; // default to something sensible.
8154 /// Get all the IPs with more than notifyloginthreshold failures since lastnotifyfailure
8155 /// and insert them into the cache_flags temp table
8156 $sql = "SELECT ip, COUNT(*)
8157 FROM {log}
8158 WHERE module = 'login' AND action = 'error'
8159 AND time > ?
8160 GROUP BY ip
8161 HAVING COUNT(*) >= ?";
8162 $params = array($CFG->lastnotifyfailure, $CFG->notifyloginthreshold);
8163 $rs = $DB->get_recordset_sql($sql, $params);
8164 foreach ($rs as $iprec) {
8165 if (!empty($iprec->ip)) {
8166 set_cache_flag('login_failure_by_ip', $iprec->ip, '1', 0);
8169 $rs->close();
8171 /// Get all the INFOs with more than notifyloginthreshold failures since lastnotifyfailure
8172 /// and insert them into the cache_flags temp table
8173 $sql = "SELECT info, count(*)
8174 FROM {log}
8175 WHERE module = 'login' AND action = 'error'
8176 AND time > ?
8177 GROUP BY info
8178 HAVING count(*) >= ?";
8179 $params = array($CFG->lastnotifyfailure, $CFG->notifyloginthreshold);
8180 $rs = $DB->get_recordset_sql($sql, $params);
8181 foreach ($rs as $inforec) {
8182 if (!empty($inforec->info)) {
8183 set_cache_flag('login_failure_by_info', $inforec->info, '1', 0);
8186 $rs->close();
8188 /// Now, select all the login error logged records belonging to the ips and infos
8189 /// since lastnotifyfailure, that we have stored in the cache_flags table
8190 $sql = "SELECT * FROM (
8191 SELECT l.*, u.firstname, u.lastname
8192 FROM {log} l
8193 JOIN {cache_flags} cf ON l.ip = cf.name
8194 LEFT JOIN {user} u ON l.userid = u.id
8195 WHERE l.module = 'login' AND l.action = 'error'
8196 AND l.time > ?
8197 AND cf.flagtype = 'login_failure_by_ip'
8198 UNION ALL
8199 SELECT l.*, u.firstname, u.lastname
8200 FROM {log} l
8201 JOIN {cache_flags} cf ON l.info = cf.name
8202 LEFT JOIN {user} u ON l.userid = u.id
8203 WHERE l.module = 'login' AND l.action = 'error'
8204 AND l.time > ?
8205 AND cf.flagtype = 'login_failure_by_info') t
8206 ORDER BY t.time DESC";
8207 $params = array($CFG->lastnotifyfailure, $CFG->lastnotifyfailure);
8209 /// Init some variables
8210 $count = 0;
8211 $messages = '';
8212 /// Iterate over the logs recordset
8213 $rs = $DB->get_recordset_sql($sql, $params);
8214 foreach ($rs as $log) {
8215 $log->time = userdate($log->time);
8216 $messages .= get_string('notifyloginfailuresmessage','',$log)."\n";
8217 $count++;
8219 $rs->close();
8221 /// If we haven't run in the last hour and
8222 /// we have something useful to report and we
8223 /// are actually supposed to be reporting to somebody
8224 if ((time() - HOURSECS) > $CFG->lastnotifyfailure && $count > 0 && is_array($recip) && count($recip) > 0) {
8225 $site = get_site();
8226 $subject = get_string('notifyloginfailuressubject', '', format_string($site->fullname));
8227 /// Calculate the complete body of notification (start + messages + end)
8228 $body = get_string('notifyloginfailuresmessagestart', '', $CFG->wwwroot) .
8229 (($CFG->lastnotifyfailure != 0) ? '('.userdate($CFG->lastnotifyfailure).')' : '')."\n\n" .
8230 $messages .
8231 "\n\n".get_string('notifyloginfailuresmessageend','',$CFG->wwwroot)."\n\n";
8233 /// For each destination, send mail
8234 mtrace('Emailing admins about '. $count .' failed login attempts');
8235 foreach ($recip as $admin) {
8236 //emailing the admins directly rather than putting these through the messaging system
8237 email_to_user($admin,get_admin(), $subject, $body);
8240 /// Update lastnotifyfailure with current time
8241 set_config('lastnotifyfailure', time());
8244 /// Finally, delete all the temp records we have created in cache_flags
8245 $DB->delete_records_select('cache_flags', "flagtype IN ('login_failure_by_ip', 'login_failure_by_info')");
8249 * Sets the system locale
8251 * @todo Finish documenting this function
8253 * @global object
8254 * @param string $locale Can be used to force a locale
8256 function moodle_setlocale($locale='') {
8257 global $CFG;
8259 static $currentlocale = ''; // last locale caching
8261 $oldlocale = $currentlocale;
8263 /// Fetch the correct locale based on ostype
8264 if ($CFG->ostype == 'WINDOWS') {
8265 $stringtofetch = 'localewin';
8266 } else {
8267 $stringtofetch = 'locale';
8270 /// the priority is the same as in get_string() - parameter, config, course, session, user, global language
8271 if (!empty($locale)) {
8272 $currentlocale = $locale;
8273 } else if (!empty($CFG->locale)) { // override locale for all language packs
8274 $currentlocale = $CFG->locale;
8275 } else {
8276 $currentlocale = get_string($stringtofetch, 'langconfig');
8279 /// do nothing if locale already set up
8280 if ($oldlocale == $currentlocale) {
8281 return;
8284 /// Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values,
8285 /// set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7
8286 /// Some day, numeric, monetary and other categories should be set too, I think. :-/
8288 /// Get current values
8289 $monetary= setlocale (LC_MONETARY, 0);
8290 $numeric = setlocale (LC_NUMERIC, 0);
8291 $ctype = setlocale (LC_CTYPE, 0);
8292 if ($CFG->ostype != 'WINDOWS') {
8293 $messages= setlocale (LC_MESSAGES, 0);
8295 /// Set locale to all
8296 setlocale (LC_ALL, $currentlocale);
8297 /// Set old values
8298 setlocale (LC_MONETARY, $monetary);
8299 setlocale (LC_NUMERIC, $numeric);
8300 if ($CFG->ostype != 'WINDOWS') {
8301 setlocale (LC_MESSAGES, $messages);
8303 if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') { // To workaround a well-known PHP problem with Turkish letter Ii
8304 setlocale (LC_CTYPE, $ctype);
8309 * Converts string to lowercase using most compatible function available.
8311 * @todo Remove this function when no longer in use
8312 * @deprecated Use textlib->strtolower($text) instead.
8314 * @param string $string The string to convert to all lowercase characters.
8315 * @param string $encoding The encoding on the string.
8316 * @return string
8318 function moodle_strtolower ($string, $encoding='') {
8320 //If not specified use utf8
8321 if (empty($encoding)) {
8322 $encoding = 'UTF-8';
8324 //Use text services
8325 $textlib = textlib_get_instance();
8327 return $textlib->strtolower($string, $encoding);
8331 * Count words in a string.
8333 * Words are defined as things between whitespace.
8335 * @param string $string The text to be searched for words.
8336 * @return int The count of words in the specified string
8338 function count_words($string) {
8339 $string = strip_tags($string);
8340 return count(preg_split("/\w\b/", $string)) - 1;
8343 /** Count letters in a string.
8345 * Letters are defined as chars not in tags and different from whitespace.
8347 * @param string $string The text to be searched for letters.
8348 * @return int The count of letters in the specified text.
8350 function count_letters($string) {
8351 /// Loading the textlib singleton instance. We are going to need it.
8352 $textlib = textlib_get_instance();
8354 $string = strip_tags($string); // Tags are out now
8355 $string = preg_replace('/[[:space:]]*/','',$string); //Whitespace are out now
8357 return $textlib->strlen($string);
8361 * Generate and return a random string of the specified length.
8363 * @param int $length The length of the string to be created.
8364 * @return string
8366 function random_string ($length=15) {
8367 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
8368 $pool .= 'abcdefghijklmnopqrstuvwxyz';
8369 $pool .= '0123456789';
8370 $poollen = strlen($pool);
8371 mt_srand ((double) microtime() * 1000000);
8372 $string = '';
8373 for ($i = 0; $i < $length; $i++) {
8374 $string .= substr($pool, (mt_rand()%($poollen)), 1);
8376 return $string;
8380 * Generate a complex random string (useful for md5 salts)
8382 * This function is based on the above {@link random_string()} however it uses a
8383 * larger pool of characters and generates a string between 24 and 32 characters
8385 * @param int $length Optional if set generates a string to exactly this length
8386 * @return string
8388 function complex_random_string($length=null) {
8389 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
8390 $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
8391 $poollen = strlen($pool);
8392 mt_srand ((double) microtime() * 1000000);
8393 if ($length===null) {
8394 $length = floor(rand(24,32));
8396 $string = '';
8397 for ($i = 0; $i < $length; $i++) {
8398 $string .= $pool[(mt_rand()%$poollen)];
8400 return $string;
8404 * Given some text (which may contain HTML) and an ideal length,
8405 * this function truncates the text neatly on a word boundary if possible
8407 * @global object
8408 * @param string $text - text to be shortened
8409 * @param int $ideal - ideal string length
8410 * @param boolean $exact if false, $text will not be cut mid-word
8411 * @param string $ending The string to append if the passed string is truncated
8412 * @return string $truncate - shortened string
8414 function shorten_text($text, $ideal=30, $exact = false, $ending='...') {
8416 global $CFG;
8418 // if the plain text is shorter than the maximum length, return the whole text
8419 if (strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) {
8420 return $text;
8423 // Splits on HTML tags. Each open/close/empty tag will be the first thing
8424 // and only tag in its 'line'
8425 preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
8427 $total_length = strlen($ending);
8428 $truncate = '';
8430 // This array stores information about open and close tags and their position
8431 // in the truncated string. Each item in the array is an object with fields
8432 // ->open (true if open), ->tag (tag name in lower case), and ->pos
8433 // (byte position in truncated text)
8434 $tagdetails = array();
8436 foreach ($lines as $line_matchings) {
8437 // if there is any html-tag in this line, handle it and add it (uncounted) to the output
8438 if (!empty($line_matchings[1])) {
8439 // if it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>)
8440 if (preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $line_matchings[1])) {
8441 // do nothing
8442 // if tag is a closing tag (f.e. </b>)
8443 } else if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $line_matchings[1], $tag_matchings)) {
8444 // record closing tag
8445 $tagdetails[] = (object)array('open'=>false,
8446 'tag'=>strtolower($tag_matchings[1]), 'pos'=>strlen($truncate));
8447 // if tag is an opening tag (f.e. <b>)
8448 } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $line_matchings[1], $tag_matchings)) {
8449 // record opening tag
8450 $tagdetails[] = (object)array('open'=>true,
8451 'tag'=>strtolower($tag_matchings[1]), 'pos'=>strlen($truncate));
8453 // add html-tag to $truncate'd text
8454 $truncate .= $line_matchings[1];
8457 // calculate the length of the plain text part of the line; handle entities as one character
8458 $content_length = strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $line_matchings[2]));
8459 if ($total_length+$content_length > $ideal) {
8460 // the number of characters which are left
8461 $left = $ideal - $total_length;
8462 $entities_length = 0;
8463 // search for html entities
8464 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)) {
8465 // calculate the real length of all entities in the legal range
8466 foreach ($entities[0] as $entity) {
8467 if ($entity[1]+1-$entities_length <= $left) {
8468 $left--;
8469 $entities_length += strlen($entity[0]);
8470 } else {
8471 // no more characters left
8472 break;
8476 $truncate .= substr($line_matchings[2], 0, $left+$entities_length);
8477 // maximum length is reached, so get off the loop
8478 break;
8479 } else {
8480 $truncate .= $line_matchings[2];
8481 $total_length += $content_length;
8484 // if the maximum length is reached, get off the loop
8485 if($total_length >= $ideal) {
8486 break;
8490 // if the words shouldn't be cut in the middle...
8491 if (!$exact) {
8492 // ...search the last occurence of a space...
8493 for ($k=strlen($truncate);$k>0;$k--) {
8494 if (!empty($truncate[$k]) && ($char = $truncate[$k])) {
8495 if ($char == '.' or $char == ' ') {
8496 $breakpos = $k+1;
8497 break;
8498 } else if (ord($char) >= 0xE0) { // Chinese/Japanese/Korean text
8499 $breakpos = $k; // can be truncated at any UTF-8
8500 break; // character boundary.
8505 if (isset($breakpos)) {
8506 // ...and cut the text in this position
8507 $truncate = substr($truncate, 0, $breakpos);
8511 // add the defined ending to the text
8512 $truncate .= $ending;
8514 // Now calculate the list of open html tags based on the truncate position
8515 $open_tags = array();
8516 foreach ($tagdetails as $taginfo) {
8517 if(isset($breakpos) && $taginfo->pos >= $breakpos) {
8518 // Don't include tags after we made the break!
8519 break;
8521 if($taginfo->open) {
8522 // add tag to the beginning of $open_tags list
8523 array_unshift($open_tags, $taginfo->tag);
8524 } else {
8525 $pos = array_search($taginfo->tag, array_reverse($open_tags, true)); // can have multiple exact same open tags, close the last one
8526 if ($pos !== false) {
8527 unset($open_tags[$pos]);
8532 // close all unclosed html-tags
8533 foreach ($open_tags as $tag) {
8534 $truncate .= '</' . $tag . '>';
8537 return $truncate;
8542 * Given dates in seconds, how many weeks is the date from startdate
8543 * The first week is 1, the second 2 etc ...
8545 * @todo Finish documenting this function
8547 * @uses WEEKSECS
8548 * @param int $startdate Timestamp for the start date
8549 * @param int $thedate Timestamp for the end date
8550 * @return string
8552 function getweek ($startdate, $thedate) {
8553 if ($thedate < $startdate) { // error
8554 return 0;
8557 return floor(($thedate - $startdate) / WEEKSECS) + 1;
8561 * returns a randomly generated password of length $maxlen. inspired by
8563 * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and
8564 * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254}
8566 * @global object
8567 * @param int $maxlen The maximum size of the password being generated.
8568 * @return string
8570 function generate_password($maxlen=10) {
8571 global $CFG;
8573 if (empty($CFG->passwordpolicy)) {
8574 $fillers = PASSWORD_DIGITS;
8575 $wordlist = file($CFG->wordlist);
8576 $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8577 $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8578 $filler1 = $fillers[rand(0, strlen($fillers) - 1)];
8579 $password = $word1 . $filler1 . $word2;
8580 } else {
8581 $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0;
8582 $digits = $CFG->minpassworddigits;
8583 $lower = $CFG->minpasswordlower;
8584 $upper = $CFG->minpasswordupper;
8585 $nonalphanum = $CFG->minpasswordnonalphanum;
8586 $total = $lower + $upper + $digits + $nonalphanum;
8587 // minlength should be the greater one of the two ( $minlen and $total )
8588 $minlen = $minlen < $total ? $total : $minlen;
8589 // maxlen can never be smaller than minlen
8590 $maxlen = $minlen > $maxlen ? $minlen : $maxlen;
8591 $additional = $maxlen - $total;
8593 // Make sure we have enough characters to fulfill
8594 // complexity requirements
8595 $passworddigits = PASSWORD_DIGITS;
8596 while ($digits > strlen($passworddigits)) {
8597 $passworddigits .= PASSWORD_DIGITS;
8599 $passwordlower = PASSWORD_LOWER;
8600 while ($lower > strlen($passwordlower)) {
8601 $passwordlower .= PASSWORD_LOWER;
8603 $passwordupper = PASSWORD_UPPER;
8604 while ($upper > strlen($passwordupper)) {
8605 $passwordupper .= PASSWORD_UPPER;
8607 $passwordnonalphanum = PASSWORD_NONALPHANUM;
8608 while ($nonalphanum > strlen($passwordnonalphanum)) {
8609 $passwordnonalphanum .= PASSWORD_NONALPHANUM;
8612 // Now mix and shuffle it all
8613 $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) .
8614 substr(str_shuffle ($passwordupper), 0, $upper) .
8615 substr(str_shuffle ($passworddigits), 0, $digits) .
8616 substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) .
8617 substr(str_shuffle ($passwordlower .
8618 $passwordupper .
8619 $passworddigits .
8620 $passwordnonalphanum), 0 , $additional));
8623 return substr ($password, 0, $maxlen);
8627 * Given a float, prints it nicely.
8628 * Localized floats must not be used in calculations!
8630 * @param float $float The float to print
8631 * @param int $places The number of decimal places to print.
8632 * @param bool $localized use localized decimal separator
8633 * @return string locale float
8635 function format_float($float, $decimalpoints=1, $localized=true) {
8636 if (is_null($float)) {
8637 return '';
8639 if ($localized) {
8640 return number_format($float, $decimalpoints, get_string('decsep', 'langconfig'), '');
8641 } else {
8642 return number_format($float, $decimalpoints, '.', '');
8647 * Converts locale specific floating point/comma number back to standard PHP float value
8648 * Do NOT try to do any math operations before this conversion on any user submitted floats!
8650 * @param string $locale_float locale aware float representation
8651 * @return float
8653 function unformat_float($locale_float) {
8654 $locale_float = trim($locale_float);
8656 if ($locale_float == '') {
8657 return null;
8660 $locale_float = str_replace(' ', '', $locale_float); // no spaces - those might be used as thousand separators
8662 return (float)str_replace(get_string('decsep', 'langconfig'), '.', $locale_float);
8666 * Given a simple array, this shuffles it up just like shuffle()
8667 * Unlike PHP's shuffle() this function works on any machine.
8669 * @param array $array The array to be rearranged
8670 * @return array
8672 function swapshuffle($array) {
8674 srand ((double) microtime() * 10000000);
8675 $last = count($array) - 1;
8676 for ($i=0;$i<=$last;$i++) {
8677 $from = rand(0,$last);
8678 $curr = $array[$i];
8679 $array[$i] = $array[$from];
8680 $array[$from] = $curr;
8682 return $array;
8686 * Like {@link swapshuffle()}, but works on associative arrays
8688 * @param array $array The associative array to be rearranged
8689 * @return array
8691 function swapshuffle_assoc($array) {
8693 $newarray = array();
8694 $newkeys = swapshuffle(array_keys($array));
8696 foreach ($newkeys as $newkey) {
8697 $newarray[$newkey] = $array[$newkey];
8699 return $newarray;
8703 * Given an arbitrary array, and a number of draws,
8704 * this function returns an array with that amount
8705 * of items. The indexes are retained.
8707 * @todo Finish documenting this function
8709 * @param array $array
8710 * @param int $draws
8711 * @return array
8713 function draw_rand_array($array, $draws) {
8714 srand ((double) microtime() * 10000000);
8716 $return = array();
8718 $last = count($array);
8720 if ($draws > $last) {
8721 $draws = $last;
8724 while ($draws > 0) {
8725 $last--;
8727 $keys = array_keys($array);
8728 $rand = rand(0, $last);
8730 $return[$keys[$rand]] = $array[$keys[$rand]];
8731 unset($array[$keys[$rand]]);
8733 $draws--;
8736 return $return;
8740 * Calculate the difference between two microtimes
8742 * @param string $a The first Microtime
8743 * @param string $b The second Microtime
8744 * @return string
8746 function microtime_diff($a, $b) {
8747 list($a_dec, $a_sec) = explode(' ', $a);
8748 list($b_dec, $b_sec) = explode(' ', $b);
8749 return $b_sec - $a_sec + $b_dec - $a_dec;
8753 * Given a list (eg a,b,c,d,e) this function returns
8754 * an array of 1->a, 2->b, 3->c etc
8756 * @param string $list The string to explode into array bits
8757 * @param string $separator The separator used within the list string
8758 * @return array The now assembled array
8760 function make_menu_from_list($list, $separator=',') {
8762 $array = array_reverse(explode($separator, $list), true);
8763 foreach ($array as $key => $item) {
8764 $outarray[$key+1] = trim($item);
8766 return $outarray;
8770 * Creates an array that represents all the current grades that
8771 * can be chosen using the given grading type.
8773 * Negative numbers
8774 * are scales, zero is no grade, and positive numbers are maximum
8775 * grades.
8777 * @todo Finish documenting this function or better deprecated this completely!
8779 * @param int $gradingtype
8780 * @return array
8782 function make_grades_menu($gradingtype) {
8783 global $DB;
8785 $grades = array();
8786 if ($gradingtype < 0) {
8787 if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) {
8788 return make_menu_from_list($scale->scale);
8790 } else if ($gradingtype > 0) {
8791 for ($i=$gradingtype; $i>=0; $i--) {
8792 $grades[$i] = $i .' / '. $gradingtype;
8794 return $grades;
8796 return $grades;
8800 * This function returns the number of activities
8801 * using scaleid in a courseid
8803 * @todo Finish documenting this function
8805 * @global object
8806 * @global object
8807 * @param int $courseid ?
8808 * @param int $scaleid ?
8809 * @return int
8811 function course_scale_used($courseid, $scaleid) {
8812 global $CFG, $DB;
8814 $return = 0;
8816 if (!empty($scaleid)) {
8817 if ($cms = get_course_mods($courseid)) {
8818 foreach ($cms as $cm) {
8819 //Check cm->name/lib.php exists
8820 if (file_exists($CFG->dirroot.'/mod/'.$cm->modname.'/lib.php')) {
8821 include_once($CFG->dirroot.'/mod/'.$cm->modname.'/lib.php');
8822 $function_name = $cm->modname.'_scale_used';
8823 if (function_exists($function_name)) {
8824 if ($function_name($cm->instance,$scaleid)) {
8825 $return++;
8832 // check if any course grade item makes use of the scale
8833 $return += $DB->count_records('grade_items', array('courseid'=>$courseid, 'scaleid'=>$scaleid));
8835 // check if any outcome in the course makes use of the scale
8836 $return += $DB->count_records_sql("SELECT COUNT('x')
8837 FROM {grade_outcomes_courses} goc,
8838 {grade_outcomes} go
8839 WHERE go.id = goc.outcomeid
8840 AND go.scaleid = ? AND goc.courseid = ?",
8841 array($scaleid, $courseid));
8843 return $return;
8847 * This function returns the number of activities
8848 * using scaleid in the entire site
8850 * @param int $scaleid
8851 * @param array $courses
8852 * @return int
8854 function site_scale_used($scaleid, &$courses) {
8855 $return = 0;
8857 if (!is_array($courses) || count($courses) == 0) {
8858 $courses = get_courses("all",false,"c.id,c.shortname");
8861 if (!empty($scaleid)) {
8862 if (is_array($courses) && count($courses) > 0) {
8863 foreach ($courses as $course) {
8864 $return += course_scale_used($course->id,$scaleid);
8868 return $return;
8872 * make_unique_id_code
8874 * @todo Finish documenting this function
8876 * @uses $_SERVER
8877 * @param string $extra Extra string to append to the end of the code
8878 * @return string
8880 function make_unique_id_code($extra='') {
8882 $hostname = 'unknownhost';
8883 if (!empty($_SERVER['HTTP_HOST'])) {
8884 $hostname = $_SERVER['HTTP_HOST'];
8885 } else if (!empty($_ENV['HTTP_HOST'])) {
8886 $hostname = $_ENV['HTTP_HOST'];
8887 } else if (!empty($_SERVER['SERVER_NAME'])) {
8888 $hostname = $_SERVER['SERVER_NAME'];
8889 } else if (!empty($_ENV['SERVER_NAME'])) {
8890 $hostname = $_ENV['SERVER_NAME'];
8893 $date = gmdate("ymdHis");
8895 $random = random_string(6);
8897 if ($extra) {
8898 return $hostname .'+'. $date .'+'. $random .'+'. $extra;
8899 } else {
8900 return $hostname .'+'. $date .'+'. $random;
8906 * Function to check the passed address is within the passed subnet
8908 * The parameter is a comma separated string of subnet definitions.
8909 * Subnet strings can be in one of three formats:
8910 * 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn (number of bits in net mask)
8911 * 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)
8912 * 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. (incomplete address, a bit non-technical ;-)
8913 * Code for type 1 modified from user posted comments by mediator at
8914 * {@link http://au.php.net/manual/en/function.ip2long.php}
8916 * @param string $addr The address you are checking
8917 * @param string $subnetstr The string of subnet addresses
8918 * @return bool
8920 function address_in_subnet($addr, $subnetstr) {
8922 if ($addr == '0.0.0.0') {
8923 return false;
8925 $subnets = explode(',', $subnetstr);
8926 $found = false;
8927 $addr = trim($addr);
8928 $addr = cleanremoteaddr($addr, false); // normalise
8929 if ($addr === null) {
8930 return false;
8932 $addrparts = explode(':', $addr);
8934 $ipv6 = strpos($addr, ':');
8936 foreach ($subnets as $subnet) {
8937 $subnet = trim($subnet);
8938 if ($subnet === '') {
8939 continue;
8942 if (strpos($subnet, '/') !== false) {
8943 ///1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn
8944 list($ip, $mask) = explode('/', $subnet);
8945 $mask = trim($mask);
8946 if (!is_number($mask)) {
8947 continue; // incorect mask number, eh?
8949 $ip = cleanremoteaddr($ip, false); // normalise
8950 if ($ip === null) {
8951 continue;
8953 if (strpos($ip, ':') !== false) {
8954 // IPv6
8955 if (!$ipv6) {
8956 continue;
8958 if ($mask > 128 or $mask < 0) {
8959 continue; // nonsense
8961 if ($mask == 0) {
8962 return true; // any address
8964 if ($mask == 128) {
8965 if ($ip === $addr) {
8966 return true;
8968 continue;
8970 $ipparts = explode(':', $ip);
8971 $modulo = $mask % 16;
8972 $ipnet = array_slice($ipparts, 0, ($mask-$modulo)/16);
8973 $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16);
8974 if (implode(':', $ipnet) === implode(':', $addrnet)) {
8975 if ($modulo == 0) {
8976 return true;
8978 $pos = ($mask-$modulo)/16;
8979 $ipnet = hexdec($ipparts[$pos]);
8980 $addrnet = hexdec($addrparts[$pos]);
8981 $mask = 0xffff << (16 - $modulo);
8982 if (($addrnet & $mask) == ($ipnet & $mask)) {
8983 return true;
8987 } else {
8988 // IPv4
8989 if ($ipv6) {
8990 continue;
8992 if ($mask > 32 or $mask < 0) {
8993 continue; // nonsense
8995 if ($mask == 0) {
8996 return true;
8998 if ($mask == 32) {
8999 if ($ip === $addr) {
9000 return true;
9002 continue;
9004 $mask = 0xffffffff << (32 - $mask);
9005 if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
9006 return true;
9010 } else if (strpos($subnet, '-') !== false) {
9011 /// 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.
9012 $parts = explode('-', $subnet);
9013 if (count($parts) != 2) {
9014 continue;
9017 if (strpos($subnet, ':') !== false) {
9018 // IPv6
9019 if (!$ipv6) {
9020 continue;
9022 $ipstart = cleanremoteaddr(trim($parts[0]), false); // normalise
9023 if ($ipstart === null) {
9024 continue;
9026 $ipparts = explode(':', $ipstart);
9027 $start = hexdec(array_pop($ipparts));
9028 $ipparts[] = trim($parts[1]);
9029 $ipend = cleanremoteaddr(implode(':', $ipparts), false); // normalise
9030 if ($ipend === null) {
9031 continue;
9033 $ipparts[7] = '';
9034 $ipnet = implode(':', $ipparts);
9035 if (strpos($addr, $ipnet) !== 0) {
9036 continue;
9038 $ipparts = explode(':', $ipend);
9039 $end = hexdec($ipparts[7]);
9041 $addrend = hexdec($addrparts[7]);
9043 if (($addrend >= $start) and ($addrend <= $end)) {
9044 return true;
9047 } else {
9048 // IPv4
9049 if ($ipv6) {
9050 continue;
9052 $ipstart = cleanremoteaddr(trim($parts[0]), false); // normalise
9053 if ($ipstart === null) {
9054 continue;
9056 $ipparts = explode('.', $ipstart);
9057 $ipparts[3] = trim($parts[1]);
9058 $ipend = cleanremoteaddr(implode('.', $ipparts), false); // normalise
9059 if ($ipend === null) {
9060 continue;
9063 if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
9064 return true;
9068 } else {
9069 /// 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
9070 if (strpos($subnet, ':') !== false) {
9071 // IPv6
9072 if (!$ipv6) {
9073 continue;
9075 $parts = explode(':', $subnet);
9076 $count = count($parts);
9077 if ($parts[$count-1] === '') {
9078 unset($parts[$count-1]); // trim trailing :
9079 $count--;
9080 $subnet = implode('.', $parts);
9082 $isip = cleanremoteaddr($subnet, false); // normalise
9083 if ($isip !== null) {
9084 if ($isip === $addr) {
9085 return true;
9087 continue;
9088 } else if ($count > 8) {
9089 continue;
9091 $zeros = array_fill(0, 8-$count, '0');
9092 $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16);
9093 if (address_in_subnet($addr, $subnet)) {
9094 return true;
9097 } else {
9098 // IPv4
9099 if ($ipv6) {
9100 continue;
9102 $parts = explode('.', $subnet);
9103 $count = count($parts);
9104 if ($parts[$count-1] === '') {
9105 unset($parts[$count-1]); // trim trailing .
9106 $count--;
9107 $subnet = implode('.', $parts);
9109 if ($count == 4) {
9110 $subnet = cleanremoteaddr($subnet, false); // normalise
9111 if ($subnet === $addr) {
9112 return true;
9114 continue;
9115 } else if ($count > 4) {
9116 continue;
9118 $zeros = array_fill(0, 4-$count, '0');
9119 $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8);
9120 if (address_in_subnet($addr, $subnet)) {
9121 return true;
9127 return false;
9131 * For outputting debugging info
9133 * @uses STDOUT
9134 * @param string $string The string to write
9135 * @param string $eol The end of line char(s) to use
9136 * @param string $sleep Period to make the application sleep
9137 * This ensures any messages have time to display before redirect
9139 function mtrace($string, $eol="\n", $sleep=0) {
9141 if (defined('STDOUT')) {
9142 fwrite(STDOUT, $string.$eol);
9143 } else {
9144 echo $string . $eol;
9147 flush();
9149 //delay to keep message on user's screen in case of subsequent redirect
9150 if ($sleep) {
9151 sleep($sleep);
9156 * Replace 1 or more slashes or backslashes to 1 slash
9158 * @param string $path The path to strip
9159 * @return string the path with double slashes removed
9161 function cleardoubleslashes ($path) {
9162 return preg_replace('/(\/|\\\){1,}/','/',$path);
9166 * Is current ip in give list?
9168 * @param string $list
9169 * @return bool
9171 function remoteip_in_list($list){
9172 $inlist = false;
9173 $client_ip = getremoteaddr(null);
9175 if(!$client_ip){
9176 // ensure access on cli
9177 return true;
9180 $list = explode("\n", $list);
9181 foreach($list as $subnet) {
9182 $subnet = trim($subnet);
9183 if (address_in_subnet($client_ip, $subnet)) {
9184 $inlist = true;
9185 break;
9188 return $inlist;
9192 * Returns most reliable client address
9194 * @global object
9195 * @param string $default If an address can't be determined, then return this
9196 * @return string The remote IP address
9198 function getremoteaddr($default='0.0.0.0') {
9199 global $CFG;
9201 if (empty($CFG->getremoteaddrconf)) {
9202 // This will happen, for example, before just after the upgrade, as the
9203 // user is redirected to the admin screen.
9204 $variablestoskip = 0;
9205 } else {
9206 $variablestoskip = $CFG->getremoteaddrconf;
9208 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) {
9209 if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
9210 $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']);
9211 return $address ? $address : $default;
9214 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
9215 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
9216 $address = cleanremoteaddr($_SERVER['HTTP_X_FORWARDED_FOR']);
9217 return $address ? $address : $default;
9220 if (!empty($_SERVER['REMOTE_ADDR'])) {
9221 $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']);
9222 return $address ? $address : $default;
9223 } else {
9224 return $default;
9229 * Cleans an ip address. Internal addresses are now allowed.
9230 * (Originally local addresses were not allowed.)
9232 * @param string $addr IPv4 or IPv6 address
9233 * @param bool $compress use IPv6 address compression
9234 * @return string normalised ip address string, null if error
9236 function cleanremoteaddr($addr, $compress=false) {
9237 $addr = trim($addr);
9239 //TODO: maybe add a separate function is_addr_public() or something like this
9241 if (strpos($addr, ':') !== false) {
9242 // can be only IPv6
9243 $parts = explode(':', $addr);
9244 $count = count($parts);
9246 if (strpos($parts[$count-1], '.') !== false) {
9247 //legacy ipv4 notation
9248 $last = array_pop($parts);
9249 $ipv4 = cleanremoteaddr($last, true);
9250 if ($ipv4 === null) {
9251 return null;
9253 $bits = explode('.', $ipv4);
9254 $parts[] = dechex($bits[0]).dechex($bits[1]);
9255 $parts[] = dechex($bits[2]).dechex($bits[3]);
9256 $count = count($parts);
9257 $addr = implode(':', $parts);
9260 if ($count < 3 or $count > 8) {
9261 return null; // severly malformed
9264 if ($count != 8) {
9265 if (strpos($addr, '::') === false) {
9266 return null; // malformed
9268 // uncompress ::
9269 $insertat = array_search('', $parts, true);
9270 $missing = array_fill(0, 1 + 8 - $count, '0');
9271 array_splice($parts, $insertat, 1, $missing);
9272 foreach ($parts as $key=>$part) {
9273 if ($part === '') {
9274 $parts[$key] = '0';
9279 $adr = implode(':', $parts);
9280 if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) {
9281 return null; // incorrect format - sorry
9284 // normalise 0s and case
9285 $parts = array_map('hexdec', $parts);
9286 $parts = array_map('dechex', $parts);
9288 $result = implode(':', $parts);
9290 if (!$compress) {
9291 return $result;
9294 if ($result === '0:0:0:0:0:0:0:0') {
9295 return '::'; // all addresses
9298 $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1);
9299 if ($compressed !== $result) {
9300 return $compressed;
9303 $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
9304 if ($compressed !== $result) {
9305 return $compressed;
9308 $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
9309 if ($compressed !== $result) {
9310 return $compressed;
9313 return $result;
9316 // first get all things that look like IPv4 addresses
9317 $parts = array();
9318 if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
9319 return null;
9321 unset($parts[0]);
9323 foreach ($parts as $key=>$match) {
9324 if ($match > 255) {
9325 return null;
9327 $parts[$key] = (int)$match; // normalise 0s
9330 return implode('.', $parts);
9334 * This function will make a complete copy of anything it's given,
9335 * regardless of whether it's an object or not.
9337 * @param mixed $thing Something you want cloned
9338 * @return mixed What ever it is you passed it
9340 function fullclone($thing) {
9341 return unserialize(serialize($thing));
9346 * This function expects to called during shutdown
9347 * should be set via register_shutdown_function()
9348 * in lib/setup.php .
9350 * @return void
9352 function moodle_request_shutdown() {
9353 global $CFG;
9355 // help apache server if possible
9356 $apachereleasemem = false;
9357 if (function_exists('apache_child_terminate') && function_exists('memory_get_usage')
9358 && ini_get_bool('child_terminate')) {
9360 $limit = (empty($CFG->apachemaxmem) ? 64*1024*1024 : $CFG->apachemaxmem); //64MB default
9361 if (memory_get_usage() > get_real_size($limit)) {
9362 $apachereleasemem = $limit;
9363 @apache_child_terminate();
9367 // deal with perf logging
9368 if (defined('MDL_PERF') || (!empty($CFG->perfdebug) and $CFG->perfdebug > 7)) {
9369 if ($apachereleasemem) {
9370 error_log('Mem usage over '.$apachereleasemem.': marking Apache child for reaping.');
9372 if (defined('MDL_PERFTOLOG')) {
9373 $perf = get_performance_info();
9374 error_log("PERF: " . $perf['txt']);
9376 if (defined('MDL_PERFINC')) {
9377 $inc = get_included_files();
9378 $ts = 0;
9379 foreach($inc as $f) {
9380 if (preg_match(':^/:', $f)) {
9381 $fs = filesize($f);
9382 $ts += $fs;
9383 $hfs = display_size($fs);
9384 error_log(substr($f,strlen($CFG->dirroot)) . " size: $fs ($hfs)"
9385 , NULL, NULL, 0);
9386 } else {
9387 error_log($f , NULL, NULL, 0);
9390 if ($ts > 0 ) {
9391 $hts = display_size($ts);
9392 error_log("Total size of files included: $ts ($hts)");
9399 * If new messages are waiting for the current user, then insert
9400 * JavaScript to pop up the messaging window into the page
9402 * @global moodle_page $PAGE
9403 * @return void
9405 function message_popup_window() {
9406 global $USER, $DB, $PAGE, $CFG, $SITE;
9408 if (!$PAGE->get_popup_notification_allowed() || empty($CFG->messaging)) {
9409 return;
9412 if (!isloggedin() || isguestuser()) {
9413 return;
9416 if (!isset($USER->message_lastpopup)) {
9417 $USER->message_lastpopup = 0;
9418 } else if ($USER->message_lastpopup > (time()-120)) {
9419 //dont run the query to check whether to display a popup if its been run in the last 2 minutes
9420 return;
9423 //a quick query to check whether the user has new messages
9424 $messagecount = $DB->count_records('message', array('useridto' => $USER->id));
9425 if ($messagecount<1) {
9426 return;
9429 //got unread messages so now do another query that joins with the user table
9430 $messagesql = "SELECT m.id, m.smallmessage, m.fullmessageformat, m.notification, u.firstname, u.lastname
9431 FROM {message} m
9432 JOIN {message_working} mw ON m.id=mw.unreadmessageid
9433 JOIN {message_processors} p ON mw.processorid=p.id
9434 JOIN {user} u ON m.useridfrom=u.id
9435 WHERE m.useridto = :userid
9436 AND p.name='popup'";
9438 //if the user was last notified over an hour ago we can renotify them of old messages
9439 //so don't worry about when the new message was sent
9440 $lastnotifiedlongago = $USER->message_lastpopup < (time()-3600);
9441 if (!$lastnotifiedlongago) {
9442 $messagesql .= 'AND m.timecreated > :lastpopuptime';
9445 $message_users = $DB->get_records_sql($messagesql, array('userid'=>$USER->id, 'lastpopuptime'=>$USER->message_lastpopup));
9447 //if we have new messages to notify the user about
9448 if (!empty($message_users)) {
9450 $strmessages = '';
9451 if (count($message_users)>1) {
9452 $strmessages = get_string('unreadnewmessages', 'message', count($message_users));
9453 } else {
9454 $message_users = reset($message_users);
9456 //show who the message is from if its not a notification
9457 if (!$message_users->notification) {
9458 $strmessages = get_string('unreadnewmessage', 'message', fullname($message_users) );
9461 //try to display the small version of the message
9462 $smallmessage = null;
9463 if (!empty($message_users->smallmessage)) {
9464 //display the first 200 chars of the message in the popup
9465 $textlib = textlib_get_instance();
9466 $smallmessage = null;
9467 if ($textlib->strlen($message_users->smallmessage) > 200) {
9468 $smallmessage = $textlib->substr($message_users->smallmessage,0,200).'...';
9469 } else {
9470 $smallmessage = $message_users->smallmessage;
9473 //prevent html symbols being displayed
9474 if ($message_users->fullmessageformat == FORMAT_HTML) {
9475 $smallmessage = html_to_text($smallmessage);
9476 } else {
9477 $smallmessage = s($smallmessage);
9479 } else if ($message_users->notification) {
9480 //its a notification with no smallmessage so just say they have a notification
9481 $smallmessage = get_string('unreadnewnotification', 'message');
9483 if (!empty($smallmessage)) {
9484 $strmessages .= '<div id="usermessage">'.s($smallmessage).'</div>';
9488 $strgomessage = get_string('gotomessages', 'message');
9489 $strstaymessage = get_string('ignore','admin');
9491 $url = $CFG->wwwroot.'/message/index.php';
9492 $content = html_writer::start_tag('div', array('id'=>'newmessageoverlay','class'=>'mdl-align')).
9493 html_writer::start_tag('div', array('id'=>'newmessagetext')).
9494 $strmessages.
9495 html_writer::end_tag('div').
9497 html_writer::start_tag('div', array('id'=>'newmessagelinks')).
9498 html_writer::link($url, $strgomessage, array('id'=>'notificationyes')).'&nbsp;&nbsp;&nbsp;'.
9499 html_writer::link('', $strstaymessage, array('id'=>'notificationno')).
9500 html_writer::end_tag('div');
9501 html_writer::end_tag('div');
9503 $PAGE->requires->js_init_call('M.core_message.init_notification', array('', $content, $url));
9505 $USER->message_lastpopup = time();
9510 * Used to make sure that $min <= $value <= $max
9512 * Make sure that value is between min, and max
9514 * @param int $min The minimum value
9515 * @param int $value The value to check
9516 * @param int $max The maximum value
9518 function bounded_number($min, $value, $max) {
9519 if($value < $min) {
9520 return $min;
9522 if($value > $max) {
9523 return $max;
9525 return $value;
9529 * Check if there is a nested array within the passed array
9531 * @param array $array
9532 * @return bool true if there is a nested array false otherwise
9534 function array_is_nested($array) {
9535 foreach ($array as $value) {
9536 if (is_array($value)) {
9537 return true;
9540 return false;
9544 * get_performance_info() pairs up with init_performance_info()
9545 * loaded in setup.php. Returns an array with 'html' and 'txt'
9546 * values ready for use, and each of the individual stats provided
9547 * separately as well.
9549 * @global object
9550 * @global object
9551 * @global object
9552 * @return array
9554 function get_performance_info() {
9555 global $CFG, $PERF, $DB, $PAGE;
9557 $info = array();
9558 $info['html'] = ''; // holds userfriendly HTML representation
9559 $info['txt'] = me() . ' '; // holds log-friendly representation
9561 $info['realtime'] = microtime_diff($PERF->starttime, microtime());
9563 $info['html'] .= '<span class="timeused">'.$info['realtime'].' secs</span> ';
9564 $info['txt'] .= 'time: '.$info['realtime'].'s ';
9566 if (function_exists('memory_get_usage')) {
9567 $info['memory_total'] = memory_get_usage();
9568 $info['memory_growth'] = memory_get_usage() - $PERF->startmemory;
9569 $info['html'] .= '<span class="memoryused">RAM: '.display_size($info['memory_total']).'</span> ';
9570 $info['txt'] .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '.$info['memory_growth'].'B ('.display_size($info['memory_growth']).') ';
9573 if (function_exists('memory_get_peak_usage')) {
9574 $info['memory_peak'] = memory_get_peak_usage();
9575 $info['html'] .= '<span class="memoryused">RAM peak: '.display_size($info['memory_peak']).'</span> ';
9576 $info['txt'] .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') ';
9579 $inc = get_included_files();
9580 //error_log(print_r($inc,1));
9581 $info['includecount'] = count($inc);
9582 $info['html'] .= '<span class="included">Included '.$info['includecount'].' files</span> ';
9583 $info['txt'] .= 'includecount: '.$info['includecount'].' ';
9585 $filtermanager = filter_manager::instance();
9586 if (method_exists($filtermanager, 'get_performance_summary')) {
9587 list($filterinfo, $nicenames) = $filtermanager->get_performance_summary();
9588 $info = array_merge($filterinfo, $info);
9589 foreach ($filterinfo as $key => $value) {
9590 $info['html'] .= "<span class='$key'>$nicenames[$key]: $value </span> ";
9591 $info['txt'] .= "$key: $value ";
9595 $stringmanager = get_string_manager();
9596 if (method_exists($stringmanager, 'get_performance_summary')) {
9597 list($filterinfo, $nicenames) = $stringmanager->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 $jsmodules = $PAGE->requires->get_loaded_modules();
9606 if ($jsmodules) {
9607 $yuicount = 0;
9608 $othercount = 0;
9609 $details = '';
9610 foreach ($jsmodules as $module => $backtraces) {
9611 if (strpos($module, 'yui') === 0) {
9612 $yuicount += 1;
9613 } else {
9614 $othercount += 1;
9616 $details .= "<div class='yui-module'><p>$module</p>";
9617 foreach ($backtraces as $backtrace) {
9618 $details .= "<div class='backtrace'>$backtrace</div>";
9620 $details .= '</div>';
9622 $info['html'] .= "<span class='includedyuimodules'>Included YUI modules: $yuicount</span> ";
9623 $info['txt'] .= "includedyuimodules: $yuicount ";
9624 $info['html'] .= "<span class='includedjsmodules'>Other JavaScript modules: $othercount</span> ";
9625 $info['txt'] .= "includedjsmodules: $othercount ";
9626 // Slightly odd to output the details in a display: none div. The point
9627 // Is that it takes a lot of space, and if you care you can reveal it
9628 // using firebug.
9629 $info['html'] .= '<div id="yui-module-debug" class="notifytiny">'.$details.'</div>';
9632 if (!empty($PERF->logwrites)) {
9633 $info['logwrites'] = $PERF->logwrites;
9634 $info['html'] .= '<span class="logwrites">Log DB writes '.$info['logwrites'].'</span> ';
9635 $info['txt'] .= 'logwrites: '.$info['logwrites'].' ';
9638 $info['dbqueries'] = $DB->perf_get_reads().'/'.($DB->perf_get_writes() - $PERF->logwrites);
9639 $info['html'] .= '<span class="dbqueries">DB reads/writes: '.$info['dbqueries'].'</span> ';
9640 $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' ';
9642 if (!empty($PERF->profiling) && $PERF->profiling) {
9643 require_once($CFG->dirroot .'/lib/profilerlib.php');
9644 $info['html'] .= '<span class="profilinginfo">'.Profiler::get_profiling(array('-R')).'</span>';
9647 if (function_exists('posix_times')) {
9648 $ptimes = posix_times();
9649 if (is_array($ptimes)) {
9650 foreach ($ptimes as $key => $val) {
9651 $info[$key] = $ptimes[$key] - $PERF->startposixtimes[$key];
9653 $info['html'] .= "<span class=\"posixtimes\">ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</span> ";
9654 $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] ";
9658 // Grab the load average for the last minute
9659 // /proc will only work under some linux configurations
9660 // while uptime is there under MacOSX/Darwin and other unices
9661 if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) {
9662 list($server_load) = explode(' ', $loadavg[0]);
9663 unset($loadavg);
9664 } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) {
9665 if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) {
9666 $server_load = $matches[1];
9667 } else {
9668 trigger_error('Could not parse uptime output!');
9671 if (!empty($server_load)) {
9672 $info['serverload'] = $server_load;
9673 $info['html'] .= '<span class="serverload">Load average: '.$info['serverload'].'</span> ';
9674 $info['txt'] .= "serverload: {$info['serverload']} ";
9677 // Display size of session if session started
9678 if (session_id()) {
9679 $info['sessionsize'] = display_size(strlen(session_encode()));
9680 $info['html'] .= '<span class="sessionsize">Session: ' . $info['sessionsize'] . '</span> ';
9681 $info['txt'] .= "Session: {$info['sessionsize']} ";
9684 /* if (isset($rcache->hits) && isset($rcache->misses)) {
9685 $info['rcachehits'] = $rcache->hits;
9686 $info['rcachemisses'] = $rcache->misses;
9687 $info['html'] .= '<span class="rcache">Record cache hit/miss ratio : '.
9688 "{$rcache->hits}/{$rcache->misses}</span> ";
9689 $info['txt'] .= 'rcache: '.
9690 "{$rcache->hits}/{$rcache->misses} ";
9692 $info['html'] = '<div class="performanceinfo siteinfo">'.$info['html'].'</div>';
9693 return $info;
9697 * @todo Document this function linux people
9699 function apd_get_profiling() {
9700 return shell_exec('pprofp -u ' . ini_get('apd.dumpdir') . '/pprof.' . getmypid() . '.*');
9704 * Delete directory or only it's content
9706 * @param string $dir directory path
9707 * @param bool $content_only
9708 * @return bool success, true also if dir does not exist
9710 function remove_dir($dir, $content_only=false) {
9711 if (!file_exists($dir)) {
9712 // nothing to do
9713 return true;
9715 $handle = opendir($dir);
9716 $result = true;
9717 while (false!==($item = readdir($handle))) {
9718 if($item != '.' && $item != '..') {
9719 if(is_dir($dir.'/'.$item)) {
9720 $result = remove_dir($dir.'/'.$item) && $result;
9721 }else{
9722 $result = unlink($dir.'/'.$item) && $result;
9726 closedir($handle);
9727 if ($content_only) {
9728 return $result;
9730 return rmdir($dir); // if anything left the result will be false, no need for && $result
9734 * Detect if an object or a class contains a given property
9735 * will take an actual object or the name of a class
9737 * @param mix $obj Name of class or real object to test
9738 * @param string $property name of property to find
9739 * @return bool true if property exists
9741 function object_property_exists( $obj, $property ) {
9742 if (is_string( $obj )) {
9743 $properties = get_class_vars( $obj );
9745 else {
9746 $properties = get_object_vars( $obj );
9748 return array_key_exists( $property, $properties );
9753 * Detect a custom script replacement in the data directory that will
9754 * replace an existing moodle script
9756 * @param string $urlpath path to the original script
9757 * @return string|bool full path name if a custom script exists, false if no custom script exists
9759 function custom_script_path($urlpath='') {
9760 global $CFG;
9762 // set default $urlpath, if necessary
9763 if (empty($urlpath)) {
9764 $urlpath = qualified_me(); // e.g. http://www.this-server.com/moodle/this-script.php
9767 // $urlpath is invalid if it is empty or does not start with the Moodle wwwroot
9768 if (empty($urlpath) or (strpos($urlpath, $CFG->wwwroot) === false )) {
9769 return false;
9772 // replace wwwroot with the path to the customscripts folder and clean path
9773 $scriptpath = $CFG->customscripts . clean_param(substr($urlpath, strlen($CFG->wwwroot)), PARAM_PATH);
9775 // remove the query string, if any
9776 if (($strpos = strpos($scriptpath, '?')) !== false) {
9777 $scriptpath = substr($scriptpath, 0, $strpos);
9780 // remove trailing slashes, if any
9781 $scriptpath = rtrim($scriptpath, '/\\');
9783 // append index.php, if necessary
9784 if (is_dir($scriptpath)) {
9785 $scriptpath .= '/index.php';
9788 // check the custom script exists
9789 if (file_exists($scriptpath)) {
9790 return $scriptpath;
9791 } else {
9792 return false;
9797 * Returns whether or not the user object is a remote MNET user. This function
9798 * is in moodlelib because it does not rely on loading any of the MNET code.
9800 * @global object
9801 * @param object $user A valid user object
9802 * @return bool True if the user is from a remote Moodle.
9804 function is_mnet_remote_user($user) {
9805 global $CFG;
9807 if (!isset($CFG->mnet_localhost_id)) {
9808 include_once $CFG->dirroot . '/mnet/lib.php';
9809 $env = new mnet_environment();
9810 $env->init();
9811 unset($env);
9814 return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id);
9818 * This function will search for browser prefereed languages, setting Moodle
9819 * to use the best one available if $SESSION->lang is undefined
9821 * @global object
9822 * @global object
9823 * @global object
9825 function setup_lang_from_browser() {
9827 global $CFG, $SESSION, $USER;
9829 if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) {
9830 // Lang is defined in session or user profile, nothing to do
9831 return;
9834 if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do
9835 return;
9838 /// Extract and clean langs from headers
9839 $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
9840 $rawlangs = str_replace('-', '_', $rawlangs); // we are using underscores
9841 $rawlangs = explode(',', $rawlangs); // Convert to array
9842 $langs = array();
9844 $order = 1.0;
9845 foreach ($rawlangs as $lang) {
9846 if (strpos($lang, ';') === false) {
9847 $langs[(string)$order] = $lang;
9848 $order = $order-0.01;
9849 } else {
9850 $parts = explode(';', $lang);
9851 $pos = strpos($parts[1], '=');
9852 $langs[substr($parts[1], $pos+1)] = $parts[0];
9855 krsort($langs, SORT_NUMERIC);
9857 /// Look for such langs under standard locations
9858 foreach ($langs as $lang) {
9859 $lang = strtolower(clean_param($lang, PARAM_SAFEDIR)); // clean it properly for include
9860 if (get_string_manager()->translation_exists($lang, false)) {
9861 $SESSION->lang = $lang; /// Lang exists, set it in session
9862 break; /// We have finished. Go out
9865 return;
9869 * check if $url matches anything in proxybypass list
9871 * any errors just result in the proxy being used (least bad)
9873 * @global object
9874 * @param string $url url to check
9875 * @return boolean true if we should bypass the proxy
9877 function is_proxybypass( $url ) {
9878 global $CFG;
9880 // sanity check
9881 if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) {
9882 return false;
9885 // get the host part out of the url
9886 if (!$host = parse_url( $url, PHP_URL_HOST )) {
9887 return false;
9890 // get the possible bypass hosts into an array
9891 $matches = explode( ',', $CFG->proxybypass );
9893 // check for a match
9894 // (IPs need to match the left hand side and hosts the right of the url,
9895 // but we can recklessly check both as there can't be a false +ve)
9896 $bypass = false;
9897 foreach ($matches as $match) {
9898 $match = trim($match);
9900 // try for IP match (Left side)
9901 $lhs = substr($host,0,strlen($match));
9902 if (strcasecmp($match,$lhs)==0) {
9903 return true;
9906 // try for host match (Right side)
9907 $rhs = substr($host,-strlen($match));
9908 if (strcasecmp($match,$rhs)==0) {
9909 return true;
9913 // nothing matched.
9914 return false;
9918 ////////////////////////////////////////////////////////////////////////////////
9921 * Check if the passed navigation is of the new style
9923 * @param mixed $navigation
9924 * @return bool true for yes false for no
9926 function is_newnav($navigation) {
9927 if (is_array($navigation) && !empty($navigation['newnav'])) {
9928 return true;
9929 } else {
9930 return false;
9935 * Checks whether the given variable name is defined as a variable within the given object.
9937 * This will NOT work with stdClass objects, which have no class variables.
9939 * @param string $var The variable name
9940 * @param object $object The object to check
9941 * @return boolean
9943 function in_object_vars($var, $object) {
9944 $class_vars = get_class_vars(get_class($object));
9945 $class_vars = array_keys($class_vars);
9946 return in_array($var, $class_vars);
9950 * Returns an array without repeated objects.
9951 * This function is similar to array_unique, but for arrays that have objects as values
9953 * @param array $array
9954 * @param bool $keep_key_assoc
9955 * @return array
9957 function object_array_unique($array, $keep_key_assoc = true) {
9958 $duplicate_keys = array();
9959 $tmp = array();
9961 foreach ($array as $key=>$val) {
9962 // convert objects to arrays, in_array() does not support objects
9963 if (is_object($val)) {
9964 $val = (array)$val;
9967 if (!in_array($val, $tmp)) {
9968 $tmp[] = $val;
9969 } else {
9970 $duplicate_keys[] = $key;
9974 foreach ($duplicate_keys as $key) {
9975 unset($array[$key]);
9978 return $keep_key_assoc ? $array : array_values($array);
9982 * Is a userid the primary administrator?
9984 * @param int $userid int id of user to check
9985 * @return boolean
9987 function is_primary_admin($userid){
9988 $primaryadmin = get_admin();
9990 if($userid == $primaryadmin->id){
9991 return true;
9992 }else{
9993 return false;
9998 * Returns the site identifier
10000 * @global object
10001 * @return string $CFG->siteidentifier, first making sure it is properly initialised.
10003 function get_site_identifier() {
10004 global $CFG;
10005 // Check to see if it is missing. If so, initialise it.
10006 if (empty($CFG->siteidentifier)) {
10007 set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']);
10009 // Return it.
10010 return $CFG->siteidentifier;
10014 * Check whether the given password has no more than the specified
10015 * number of consecutive identical characters.
10017 * @param string $password password to be checked against the password policy
10018 * @param integer $maxchars maximum number of consecutive identical characters
10020 function check_consecutive_identical_characters($password, $maxchars) {
10022 if ($maxchars < 1) {
10023 return true; // 0 is to disable this check
10025 if (strlen($password) <= $maxchars) {
10026 return true; // too short to fail this test
10029 $previouschar = '';
10030 $consecutivecount = 1;
10031 foreach (str_split($password) as $char) {
10032 if ($char != $previouschar) {
10033 $consecutivecount = 1;
10035 else {
10036 $consecutivecount++;
10037 if ($consecutivecount > $maxchars) {
10038 return false; // check failed already
10042 $previouschar = $char;
10045 return true;
10049 * helper function to do partial function binding
10050 * so we can use it for preg_replace_callback, for example
10051 * this works with php functions, user functions, static methods and class methods
10052 * it returns you a callback that you can pass on like so:
10054 * $callback = partial('somefunction', $arg1, $arg2);
10055 * or
10056 * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
10057 * or even
10058 * $obj = new someclass();
10059 * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2);
10061 * and then the arguments that are passed through at calltime are appended to the argument list.
10063 * @param mixed $function a php callback
10064 * $param mixed $arg1.. $argv arguments to partially bind with
10066 * @return callback
10068 function partial() {
10069 if (!class_exists('partial')) {
10070 class partial{
10071 var $values = array();
10072 var $func;
10074 function __construct($func, $args) {
10075 $this->values = $args;
10076 $this->func = $func;
10079 function method() {
10080 $args = func_get_args();
10081 return call_user_func_array($this->func, array_merge($this->values, $args));
10085 $args = func_get_args();
10086 $func = array_shift($args);
10087 $p = new partial($func, $args);
10088 return array($p, 'method');
10092 * helper function to load up and initialise the mnet environment
10093 * this must be called before you use mnet functions.
10095 * @return mnet_environment the equivalent of old $MNET global
10097 function get_mnet_environment() {
10098 global $CFG;
10099 require_once($CFG->dirroot . '/mnet/lib.php');
10100 static $instance = null;
10101 if (empty($instance)) {
10102 $instance = new mnet_environment();
10103 $instance->init();
10105 return $instance;
10109 * during xmlrpc server code execution, any code wishing to access
10110 * information about the remote peer must use this to get it.
10112 * @return mnet_remote_client the equivalent of old $MNET_REMOTE_CLIENT global
10114 function get_mnet_remote_client() {
10115 if (!defined('MNET_SERVER')) {
10116 debugging(get_string('notinxmlrpcserver', 'mnet'));
10117 return false;
10119 global $MNET_REMOTE_CLIENT;
10120 if (isset($MNET_REMOTE_CLIENT)) {
10121 return $MNET_REMOTE_CLIENT;
10123 return false;
10127 * during the xmlrpc server code execution, this will be called
10128 * to setup the object returned by {@see get_mnet_remote_client}
10130 * @param mnet_remote_client $client the client to set up
10132 function set_mnet_remote_client($client) {
10133 if (!defined('MNET_SERVER')) {
10134 throw new moodle_exception('notinxmlrpcserver', 'mnet');
10136 global $MNET_REMOTE_CLIENT;
10137 $MNET_REMOTE_CLIENT = $client;
10141 * return the jump url for a given remote user
10142 * this is used for rewriting forum post links in emails, etc
10144 * @param stdclass $user the user to get the idp url for
10146 function mnet_get_idp_jump_url($user) {
10147 global $CFG;
10149 static $mnetjumps = array();
10150 if (!array_key_exists($user->mnethostid, $mnetjumps)) {
10151 $idp = mnet_get_peer_host($user->mnethostid);
10152 $idpjumppath = mnet_get_app_jumppath($idp->applicationid);
10153 $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl=';
10155 return $mnetjumps[$user->mnethostid];
10159 * Gets the homepage to use for the current user
10161 * @return int One of HOMEPAGE_*
10163 function get_home_page() {
10164 global $CFG;
10166 if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) {
10167 if ($CFG->defaulthomepage == HOMEPAGE_MY) {
10168 return HOMEPAGE_MY;
10169 } else {
10170 return (int)get_user_preferences('user_home_page_preference', HOMEPAGE_MY);
10173 return HOMEPAGE_SITE;