[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * moodlelib.php - Moodle main library 19 * 20 * Main library file of miscellaneous general-purpose Moodle functions. 21 * Other main libraries: 22 * - weblib.php - functions that produce web output 23 * - datalib.php - functions that access the database 24 * 25 * @package core 26 * @subpackage lib 27 * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com 28 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 29 */ 30 31 defined('MOODLE_INTERNAL') || die(); 32 33 // CONSTANTS (Encased in phpdoc proper comments). 34 35 // Date and time constants. 36 /** 37 * Time constant - the number of seconds in a year 38 */ 39 define('YEARSECS', 31536000); 40 41 /** 42 * Time constant - the number of seconds in a week 43 */ 44 define('WEEKSECS', 604800); 45 46 /** 47 * Time constant - the number of seconds in a day 48 */ 49 define('DAYSECS', 86400); 50 51 /** 52 * Time constant - the number of seconds in an hour 53 */ 54 define('HOURSECS', 3600); 55 56 /** 57 * Time constant - the number of seconds in a minute 58 */ 59 define('MINSECS', 60); 60 61 /** 62 * Time constant - the number of minutes in a day 63 */ 64 define('DAYMINS', 1440); 65 66 /** 67 * Time constant - the number of minutes in an hour 68 */ 69 define('HOURMINS', 60); 70 71 // Parameter constants - every call to optional_param(), required_param() 72 // or clean_param() should have a specified type of parameter. 73 74 /** 75 * PARAM_ALPHA - contains only english ascii letters a-zA-Z. 76 */ 77 define('PARAM_ALPHA', 'alpha'); 78 79 /** 80 * PARAM_ALPHAEXT the same contents as PARAM_ALPHA plus the chars in quotes: "_-" allowed 81 * NOTE: originally this allowed "/" too, please use PARAM_SAFEPATH if "/" needed 82 */ 83 define('PARAM_ALPHAEXT', 'alphaext'); 84 85 /** 86 * PARAM_ALPHANUM - expected numbers and letters only. 87 */ 88 define('PARAM_ALPHANUM', 'alphanum'); 89 90 /** 91 * PARAM_ALPHANUMEXT - expected numbers, letters only and _-. 92 */ 93 define('PARAM_ALPHANUMEXT', 'alphanumext'); 94 95 /** 96 * PARAM_AUTH - actually checks to make sure the string is a valid auth plugin 97 */ 98 define('PARAM_AUTH', 'auth'); 99 100 /** 101 * PARAM_BASE64 - Base 64 encoded format 102 */ 103 define('PARAM_BASE64', 'base64'); 104 105 /** 106 * PARAM_BOOL - converts input into 0 or 1, use for switches in forms and urls. 107 */ 108 define('PARAM_BOOL', 'bool'); 109 110 /** 111 * PARAM_CAPABILITY - A capability name, like 'moodle/role:manage'. Actually 112 * checked against the list of capabilities in the database. 113 */ 114 define('PARAM_CAPABILITY', 'capability'); 115 116 /** 117 * PARAM_CLEANHTML - cleans submitted HTML code. Note that you almost never want 118 * to use this. The normal mode of operation is to use PARAM_RAW when recieving 119 * the input (required/optional_param or formslib) and then sanitse the HTML 120 * using format_text on output. This is for the rare cases when you want to 121 * sanitise the HTML on input. This cleaning may also fix xhtml strictness. 122 */ 123 define('PARAM_CLEANHTML', 'cleanhtml'); 124 125 /** 126 * PARAM_EMAIL - an email address following the RFC 127 */ 128 define('PARAM_EMAIL', 'email'); 129 130 /** 131 * PARAM_FILE - safe file name, all dangerous chars are stripped, protects against XSS, SQL injections and directory traversals 132 */ 133 define('PARAM_FILE', 'file'); 134 135 /** 136 * PARAM_FLOAT - a real/floating point number. 137 * 138 * Note that you should not use PARAM_FLOAT for numbers typed in by the user. 139 * It does not work for languages that use , as a decimal separator. 140 * Instead, do something like 141 * $rawvalue = required_param('name', PARAM_RAW); 142 * // ... other code including require_login, which sets current lang ... 143 * $realvalue = unformat_float($rawvalue); 144 * // ... then use $realvalue 145 */ 146 define('PARAM_FLOAT', 'float'); 147 148 /** 149 * PARAM_HOST - expected fully qualified domain name (FQDN) or an IPv4 dotted quad (IP address) 150 */ 151 define('PARAM_HOST', 'host'); 152 153 /** 154 * PARAM_INT - integers only, use when expecting only numbers. 155 */ 156 define('PARAM_INT', 'int'); 157 158 /** 159 * PARAM_LANG - checks to see if the string is a valid installed language in the current site. 160 */ 161 define('PARAM_LANG', 'lang'); 162 163 /** 164 * PARAM_LOCALURL - expected properly formatted URL as well as one that refers to the local server itself. (NOT orthogonal to the 165 * others! Implies PARAM_URL!) 166 */ 167 define('PARAM_LOCALURL', 'localurl'); 168 169 /** 170 * PARAM_NOTAGS - all html tags are stripped from the text. Do not abuse this type. 171 */ 172 define('PARAM_NOTAGS', 'notags'); 173 174 /** 175 * PARAM_PATH - safe relative path name, all dangerous chars are stripped, protects against XSS, SQL injections and directory 176 * traversals note: the leading slash is not removed, window drive letter is not allowed 177 */ 178 define('PARAM_PATH', 'path'); 179 180 /** 181 * PARAM_PEM - Privacy Enhanced Mail format 182 */ 183 define('PARAM_PEM', 'pem'); 184 185 /** 186 * PARAM_PERMISSION - A permission, one of CAP_INHERIT, CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT. 187 */ 188 define('PARAM_PERMISSION', 'permission'); 189 190 /** 191 * PARAM_RAW specifies a parameter that is not cleaned/processed in any way except the discarding of the invalid utf-8 characters 192 */ 193 define('PARAM_RAW', 'raw'); 194 195 /** 196 * PARAM_RAW_TRIMMED like PARAM_RAW but leading and trailing whitespace is stripped. 197 */ 198 define('PARAM_RAW_TRIMMED', 'raw_trimmed'); 199 200 /** 201 * PARAM_SAFEDIR - safe directory name, suitable for include() and require() 202 */ 203 define('PARAM_SAFEDIR', 'safedir'); 204 205 /** 206 * PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths, etc. 207 */ 208 define('PARAM_SAFEPATH', 'safepath'); 209 210 /** 211 * PARAM_SEQUENCE - expects a sequence of numbers like 8 to 1,5,6,4,6,8,9. Numbers and comma only. 212 */ 213 define('PARAM_SEQUENCE', 'sequence'); 214 215 /** 216 * PARAM_TAG - one tag (interests, blogs, etc.) - mostly international characters and space, <> not supported 217 */ 218 define('PARAM_TAG', 'tag'); 219 220 /** 221 * PARAM_TAGLIST - list of tags separated by commas (interests, blogs, etc.) 222 */ 223 define('PARAM_TAGLIST', 'taglist'); 224 225 /** 226 * PARAM_TEXT - general plain text compatible with multilang filter, no other html tags. Please note '<', or '>' are allowed here. 227 */ 228 define('PARAM_TEXT', 'text'); 229 230 /** 231 * PARAM_THEME - Checks to see if the string is a valid theme name in the current site 232 */ 233 define('PARAM_THEME', 'theme'); 234 235 /** 236 * PARAM_URL - expected properly formatted URL. Please note that domain part is required, http://localhost/ is not accepted but 237 * http://localhost.localdomain/ is ok. 238 */ 239 define('PARAM_URL', 'url'); 240 241 /** 242 * PARAM_USERNAME - Clean username to only contains allowed characters. This is to be used ONLY when manually creating user 243 * accounts, do NOT use when syncing with external systems!! 244 */ 245 define('PARAM_USERNAME', 'username'); 246 247 /** 248 * PARAM_STRINGID - used to check if the given string is valid string identifier for get_string() 249 */ 250 define('PARAM_STRINGID', 'stringid'); 251 252 // DEPRECATED PARAM TYPES OR ALIASES - DO NOT USE FOR NEW CODE. 253 /** 254 * PARAM_CLEAN - obsoleted, please use a more specific type of parameter. 255 * It was one of the first types, that is why it is abused so much ;-) 256 * @deprecated since 2.0 257 */ 258 define('PARAM_CLEAN', 'clean'); 259 260 /** 261 * PARAM_INTEGER - deprecated alias for PARAM_INT 262 * @deprecated since 2.0 263 */ 264 define('PARAM_INTEGER', 'int'); 265 266 /** 267 * PARAM_NUMBER - deprecated alias of PARAM_FLOAT 268 * @deprecated since 2.0 269 */ 270 define('PARAM_NUMBER', 'float'); 271 272 /** 273 * PARAM_ACTION - deprecated alias for PARAM_ALPHANUMEXT, use for various actions in forms and urls 274 * NOTE: originally alias for PARAM_APLHA 275 * @deprecated since 2.0 276 */ 277 define('PARAM_ACTION', 'alphanumext'); 278 279 /** 280 * PARAM_FORMAT - deprecated alias for PARAM_ALPHANUMEXT, use for names of plugins, formats, etc. 281 * NOTE: originally alias for PARAM_APLHA 282 * @deprecated since 2.0 283 */ 284 define('PARAM_FORMAT', 'alphanumext'); 285 286 /** 287 * PARAM_MULTILANG - deprecated alias of PARAM_TEXT. 288 * @deprecated since 2.0 289 */ 290 define('PARAM_MULTILANG', 'text'); 291 292 /** 293 * PARAM_TIMEZONE - expected timezone. Timezone can be int +-(0-13) or float +-(0.5-12.5) or 294 * string separated by '/' and can have '-' &/ '_' (eg. America/North_Dakota/New_Salem 295 * America/Port-au-Prince) 296 */ 297 define('PARAM_TIMEZONE', 'timezone'); 298 299 /** 300 * PARAM_CLEANFILE - deprecated alias of PARAM_FILE; originally was removing regional chars too 301 */ 302 define('PARAM_CLEANFILE', 'file'); 303 304 /** 305 * PARAM_COMPONENT is used for full component names (aka frankenstyle) such as 'mod_forum', 'core_rating', 'auth_ldap'. 306 * Short legacy subsystem names and module names are accepted too ex: 'forum', 'rating', 'user'. 307 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter. 308 * NOTE: numbers and underscores are strongly discouraged in plugin names! 309 */ 310 define('PARAM_COMPONENT', 'component'); 311 312 /** 313 * PARAM_AREA is a name of area used when addressing files, comments, ratings, etc. 314 * It is usually used together with context id and component. 315 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter. 316 */ 317 define('PARAM_AREA', 'area'); 318 319 /** 320 * PARAM_PLUGIN is used for plugin names such as 'forum', 'glossary', 'ldap', 'radius', 'paypal', 'completionstatus'. 321 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter. 322 * NOTE: numbers and underscores are strongly discouraged in plugin names! Underscores are forbidden in module names. 323 */ 324 define('PARAM_PLUGIN', 'plugin'); 325 326 327 // Web Services. 328 329 /** 330 * VALUE_REQUIRED - if the parameter is not supplied, there is an error 331 */ 332 define('VALUE_REQUIRED', 1); 333 334 /** 335 * VALUE_OPTIONAL - if the parameter is not supplied, then the param has no value 336 */ 337 define('VALUE_OPTIONAL', 2); 338 339 /** 340 * VALUE_DEFAULT - if the parameter is not supplied, then the default value is used 341 */ 342 define('VALUE_DEFAULT', 0); 343 344 /** 345 * NULL_NOT_ALLOWED - the parameter can not be set to null in the database 346 */ 347 define('NULL_NOT_ALLOWED', false); 348 349 /** 350 * NULL_ALLOWED - the parameter can be set to null in the database 351 */ 352 define('NULL_ALLOWED', true); 353 354 // Page types. 355 356 /** 357 * PAGE_COURSE_VIEW is a definition of a page type. For more information on the page class see moodle/lib/pagelib.php. 358 */ 359 define('PAGE_COURSE_VIEW', 'course-view'); 360 361 /** Get remote addr constant */ 362 define('GETREMOTEADDR_SKIP_HTTP_CLIENT_IP', '1'); 363 /** Get remote addr constant */ 364 define('GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR', '2'); 365 366 // Blog access level constant declaration. 367 define ('BLOG_USER_LEVEL', 1); 368 define ('BLOG_GROUP_LEVEL', 2); 369 define ('BLOG_COURSE_LEVEL', 3); 370 define ('BLOG_SITE_LEVEL', 4); 371 define ('BLOG_GLOBAL_LEVEL', 5); 372 373 374 // Tag constants. 375 /** 376 * To prevent problems with multibytes strings,Flag updating in nav not working on the review page. this should not exceed the 377 * length of "varchar(255) / 3 (bytes / utf-8 character) = 85". 378 * TODO: this is not correct, varchar(255) are 255 unicode chars ;-) 379 * 380 * @todo define(TAG_MAX_LENGTH) this is not correct, varchar(255) are 255 unicode chars ;-) 381 */ 382 define('TAG_MAX_LENGTH', 50); 383 384 // Password policy constants. 385 define ('PASSWORD_LOWER', 'abcdefghijklmnopqrstuvwxyz'); 386 define ('PASSWORD_UPPER', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'); 387 define ('PASSWORD_DIGITS', '0123456789'); 388 define ('PASSWORD_NONALPHANUM', '.,;:!?_-+/*@#&$'); 389 390 // Feature constants. 391 // Used for plugin_supports() to report features that are, or are not, supported by a module. 392 393 /** True if module can provide a grade */ 394 define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade'); 395 /** True if module supports outcomes */ 396 define('FEATURE_GRADE_OUTCOMES', 'outcomes'); 397 /** True if module supports advanced grading methods */ 398 define('FEATURE_ADVANCED_GRADING', 'grade_advanced_grading'); 399 /** True if module controls the grade visibility over the gradebook */ 400 define('FEATURE_CONTROLS_GRADE_VISIBILITY', 'controlsgradevisbility'); 401 /** True if module supports plagiarism plugins */ 402 define('FEATURE_PLAGIARISM', 'plagiarism'); 403 404 /** True if module has code to track whether somebody viewed it */ 405 define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views'); 406 /** True if module has custom completion rules */ 407 define('FEATURE_COMPLETION_HAS_RULES', 'completion_has_rules'); 408 409 /** True if module has no 'view' page (like label) */ 410 define('FEATURE_NO_VIEW_LINK', 'viewlink'); 411 /** True (which is default) if the module wants support for setting the ID number for grade calculation purposes. */ 412 define('FEATURE_IDNUMBER', 'idnumber'); 413 /** True if module supports groups */ 414 define('FEATURE_GROUPS', 'groups'); 415 /** True if module supports groupings */ 416 define('FEATURE_GROUPINGS', 'groupings'); 417 /** 418 * True if module supports groupmembersonly (which no longer exists) 419 * @deprecated Since Moodle 2.8 420 */ 421 define('FEATURE_GROUPMEMBERSONLY', 'groupmembersonly'); 422 423 /** Type of module */ 424 define('FEATURE_MOD_ARCHETYPE', 'mod_archetype'); 425 /** True if module supports intro editor */ 426 define('FEATURE_MOD_INTRO', 'mod_intro'); 427 /** True if module has default completion */ 428 define('FEATURE_MODEDIT_DEFAULT_COMPLETION', 'modedit_default_completion'); 429 430 define('FEATURE_COMMENT', 'comment'); 431 432 define('FEATURE_RATE', 'rate'); 433 /** True if module supports backup/restore of moodle2 format */ 434 define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2'); 435 436 /** True if module can show description on course main page */ 437 define('FEATURE_SHOW_DESCRIPTION', 'showdescription'); 438 439 /** True if module uses the question bank */ 440 define('FEATURE_USES_QUESTIONS', 'usesquestions'); 441 442 /** Unspecified module archetype */ 443 define('MOD_ARCHETYPE_OTHER', 0); 444 /** Resource-like type module */ 445 define('MOD_ARCHETYPE_RESOURCE', 1); 446 /** Assignment module archetype */ 447 define('MOD_ARCHETYPE_ASSIGNMENT', 2); 448 /** System (not user-addable) module archetype */ 449 define('MOD_ARCHETYPE_SYSTEM', 3); 450 451 /** 452 * Return this from modname_get_types callback to use default display in activity chooser. 453 * Deprecated, will be removed in 3.5, TODO MDL-53697. 454 * @deprecated since Moodle 3.1 455 */ 456 define('MOD_SUBTYPE_NO_CHILDREN', 'modsubtypenochildren'); 457 458 /** 459 * Security token used for allowing access 460 * from external application such as web services. 461 * Scripts do not use any session, performance is relatively 462 * low because we need to load access info in each request. 463 * Scripts are executed in parallel. 464 */ 465 define('EXTERNAL_TOKEN_PERMANENT', 0); 466 467 /** 468 * Security token used for allowing access 469 * of embedded applications, the code is executed in the 470 * active user session. Token is invalidated after user logs out. 471 * Scripts are executed serially - normal session locking is used. 472 */ 473 define('EXTERNAL_TOKEN_EMBEDDED', 1); 474 475 /** 476 * The home page should be the site home 477 */ 478 define('HOMEPAGE_SITE', 0); 479 /** 480 * The home page should be the users my page 481 */ 482 define('HOMEPAGE_MY', 1); 483 /** 484 * The home page can be chosen by the user 485 */ 486 define('HOMEPAGE_USER', 2); 487 488 /** 489 * Hub directory url (should be moodle.org) 490 */ 491 define('HUB_HUBDIRECTORYURL', "http://hubdirectory.moodle.org"); 492 493 494 /** 495 * Moodle.org url (should be moodle.org) 496 */ 497 define('HUB_MOODLEORGHUBURL', "http://hub.moodle.org"); 498 499 /** 500 * Moodle mobile app service name 501 */ 502 define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app'); 503 504 /** 505 * Indicates the user has the capabilities required to ignore activity and course file size restrictions 506 */ 507 define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1); 508 509 /** 510 * Course display settings: display all sections on one page. 511 */ 512 define('COURSE_DISPLAY_SINGLEPAGE', 0); 513 /** 514 * Course display settings: split pages into a page per section. 515 */ 516 define('COURSE_DISPLAY_MULTIPAGE', 1); 517 518 /** 519 * Authentication constant: String used in password field when password is not stored. 520 */ 521 define('AUTH_PASSWORD_NOT_CACHED', 'not cached'); 522 523 // PARAMETER HANDLING. 524 525 /** 526 * Returns a particular value for the named variable, taken from 527 * POST or GET. If the parameter doesn't exist then an error is 528 * thrown because we require this variable. 529 * 530 * This function should be used to initialise all required values 531 * in a script that are based on parameters. Usually it will be 532 * used like this: 533 * $id = required_param('id', PARAM_INT); 534 * 535 * Please note the $type parameter is now required and the value can not be array. 536 * 537 * @param string $parname the name of the page parameter we want 538 * @param string $type expected type of parameter 539 * @return mixed 540 * @throws coding_exception 541 */ 542 function required_param($parname, $type) { 543 if (func_num_args() != 2 or empty($parname) or empty($type)) { 544 throw new coding_exception('required_param() requires $parname and $type to be specified (parameter: '.$parname.')'); 545 } 546 // POST has precedence. 547 if (isset($_POST[$parname])) { 548 $param = $_POST[$parname]; 549 } else if (isset($_GET[$parname])) { 550 $param = $_GET[$parname]; 551 } else { 552 print_error('missingparam', '', '', $parname); 553 } 554 555 if (is_array($param)) { 556 debugging('Invalid array parameter detected in required_param(): '.$parname); 557 // TODO: switch to fatal error in Moodle 2.3. 558 return required_param_array($parname, $type); 559 } 560 561 return clean_param($param, $type); 562 } 563 564 /** 565 * Returns a particular array value for the named variable, taken from 566 * POST or GET. If the parameter doesn't exist then an error is 567 * thrown because we require this variable. 568 * 569 * This function should be used to initialise all required values 570 * in a script that are based on parameters. Usually it will be 571 * used like this: 572 * $ids = required_param_array('ids', PARAM_INT); 573 * 574 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported 575 * 576 * @param string $parname the name of the page parameter we want 577 * @param string $type expected type of parameter 578 * @return array 579 * @throws coding_exception 580 */ 581 function required_param_array($parname, $type) { 582 if (func_num_args() != 2 or empty($parname) or empty($type)) { 583 throw new coding_exception('required_param_array() requires $parname and $type to be specified (parameter: '.$parname.')'); 584 } 585 // POST has precedence. 586 if (isset($_POST[$parname])) { 587 $param = $_POST[$parname]; 588 } else if (isset($_GET[$parname])) { 589 $param = $_GET[$parname]; 590 } else { 591 print_error('missingparam', '', '', $parname); 592 } 593 if (!is_array($param)) { 594 print_error('missingparam', '', '', $parname); 595 } 596 597 $result = array(); 598 foreach ($param as $key => $value) { 599 if (!preg_match('/^[a-z0-9_-]+$/i', $key)) { 600 debugging('Invalid key name in required_param_array() detected: '.$key.', parameter: '.$parname); 601 continue; 602 } 603 $result[$key] = clean_param($value, $type); 604 } 605 606 return $result; 607 } 608 609 /** 610 * Returns a particular value for the named variable, taken from 611 * POST or GET, otherwise returning a given default. 612 * 613 * This function should be used to initialise all optional values 614 * in a script that are based on parameters. Usually it will be 615 * used like this: 616 * $name = optional_param('name', 'Fred', PARAM_TEXT); 617 * 618 * Please note the $type parameter is now required and the value can not be array. 619 * 620 * @param string $parname the name of the page parameter we want 621 * @param mixed $default the default value to return if nothing is found 622 * @param string $type expected type of parameter 623 * @return mixed 624 * @throws coding_exception 625 */ 626 function optional_param($parname, $default, $type) { 627 if (func_num_args() != 3 or empty($parname) or empty($type)) { 628 throw new coding_exception('optional_param requires $parname, $default + $type to be specified (parameter: '.$parname.')'); 629 } 630 631 // POST has precedence. 632 if (isset($_POST[$parname])) { 633 $param = $_POST[$parname]; 634 } else if (isset($_GET[$parname])) { 635 $param = $_GET[$parname]; 636 } else { 637 return $default; 638 } 639 640 if (is_array($param)) { 641 debugging('Invalid array parameter detected in required_param(): '.$parname); 642 // TODO: switch to $default in Moodle 2.3. 643 return optional_param_array($parname, $default, $type); 644 } 645 646 return clean_param($param, $type); 647 } 648 649 /** 650 * Returns a particular array value for the named variable, taken from 651 * POST or GET, otherwise returning a given default. 652 * 653 * This function should be used to initialise all optional values 654 * in a script that are based on parameters. Usually it will be 655 * used like this: 656 * $ids = optional_param('id', array(), PARAM_INT); 657 * 658 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported 659 * 660 * @param string $parname the name of the page parameter we want 661 * @param mixed $default the default value to return if nothing is found 662 * @param string $type expected type of parameter 663 * @return array 664 * @throws coding_exception 665 */ 666 function optional_param_array($parname, $default, $type) { 667 if (func_num_args() != 3 or empty($parname) or empty($type)) { 668 throw new coding_exception('optional_param_array requires $parname, $default + $type to be specified (parameter: '.$parname.')'); 669 } 670 671 // POST has precedence. 672 if (isset($_POST[$parname])) { 673 $param = $_POST[$parname]; 674 } else if (isset($_GET[$parname])) { 675 $param = $_GET[$parname]; 676 } else { 677 return $default; 678 } 679 if (!is_array($param)) { 680 debugging('optional_param_array() expects array parameters only: '.$parname); 681 return $default; 682 } 683 684 $result = array(); 685 foreach ($param as $key => $value) { 686 if (!preg_match('/^[a-z0-9_-]+$/i', $key)) { 687 debugging('Invalid key name in optional_param_array() detected: '.$key.', parameter: '.$parname); 688 continue; 689 } 690 $result[$key] = clean_param($value, $type); 691 } 692 693 return $result; 694 } 695 696 /** 697 * Strict validation of parameter values, the values are only converted 698 * to requested PHP type. Internally it is using clean_param, the values 699 * before and after cleaning must be equal - otherwise 700 * an invalid_parameter_exception is thrown. 701 * Objects and classes are not accepted. 702 * 703 * @param mixed $param 704 * @param string $type PARAM_ constant 705 * @param bool $allownull are nulls valid value? 706 * @param string $debuginfo optional debug information 707 * @return mixed the $param value converted to PHP type 708 * @throws invalid_parameter_exception if $param is not of given type 709 */ 710 function validate_param($param, $type, $allownull=NULL_NOT_ALLOWED, $debuginfo='') { 711 if (is_null($param)) { 712 if ($allownull == NULL_ALLOWED) { 713 return null; 714 } else { 715 throw new invalid_parameter_exception($debuginfo); 716 } 717 } 718 if (is_array($param) or is_object($param)) { 719 throw new invalid_parameter_exception($debuginfo); 720 } 721 722 $cleaned = clean_param($param, $type); 723 724 if ($type == PARAM_FLOAT) { 725 // Do not detect precision loss here. 726 if (is_float($param) or is_int($param)) { 727 // These always fit. 728 } else if (!is_numeric($param) or !preg_match('/^[\+-]?[0-9]*\.?[0-9]*(e[-+]?[0-9]+)?$/i', (string)$param)) { 729 throw new invalid_parameter_exception($debuginfo); 730 } 731 } else if ((string)$param !== (string)$cleaned) { 732 // Conversion to string is usually lossless. 733 throw new invalid_parameter_exception($debuginfo); 734 } 735 736 return $cleaned; 737 } 738 739 /** 740 * Makes sure array contains only the allowed types, this function does not validate array key names! 741 * 742 * <code> 743 * $options = clean_param($options, PARAM_INT); 744 * </code> 745 * 746 * @param array $param the variable array we are cleaning 747 * @param string $type expected format of param after cleaning. 748 * @param bool $recursive clean recursive arrays 749 * @return array 750 * @throws coding_exception 751 */ 752 function clean_param_array(array $param = null, $type, $recursive = false) { 753 // Convert null to empty array. 754 $param = (array)$param; 755 foreach ($param as $key => $value) { 756 if (is_array($value)) { 757 if ($recursive) { 758 $param[$key] = clean_param_array($value, $type, true); 759 } else { 760 throw new coding_exception('clean_param_array can not process multidimensional arrays when $recursive is false.'); 761 } 762 } else { 763 $param[$key] = clean_param($value, $type); 764 } 765 } 766 return $param; 767 } 768 769 /** 770 * Used by {@link optional_param()} and {@link required_param()} to 771 * clean the variables and/or cast to specific types, based on 772 * an options field. 773 * <code> 774 * $course->format = clean_param($course->format, PARAM_ALPHA); 775 * $selectedgradeitem = clean_param($selectedgradeitem, PARAM_INT); 776 * </code> 777 * 778 * @param mixed $param the variable we are cleaning 779 * @param string $type expected format of param after cleaning. 780 * @return mixed 781 * @throws coding_exception 782 */ 783 function clean_param($param, $type) { 784 global $CFG; 785 786 if (is_array($param)) { 787 throw new coding_exception('clean_param() can not process arrays, please use clean_param_array() instead.'); 788 } else if (is_object($param)) { 789 if (method_exists($param, '__toString')) { 790 $param = $param->__toString(); 791 } else { 792 throw new coding_exception('clean_param() can not process objects, please use clean_param_array() instead.'); 793 } 794 } 795 796 switch ($type) { 797 case PARAM_RAW: 798 // No cleaning at all. 799 $param = fix_utf8($param); 800 return $param; 801 802 case PARAM_RAW_TRIMMED: 803 // No cleaning, but strip leading and trailing whitespace. 804 $param = fix_utf8($param); 805 return trim($param); 806 807 case PARAM_CLEAN: 808 // General HTML cleaning, try to use more specific type if possible this is deprecated! 809 // Please use more specific type instead. 810 if (is_numeric($param)) { 811 return $param; 812 } 813 $param = fix_utf8($param); 814 // Sweep for scripts, etc. 815 return clean_text($param); 816 817 case PARAM_CLEANHTML: 818 // Clean html fragment. 819 $param = fix_utf8($param); 820 // Sweep for scripts, etc. 821 $param = clean_text($param, FORMAT_HTML); 822 return trim($param); 823 824 case PARAM_INT: 825 // Convert to integer. 826 return (int)$param; 827 828 case PARAM_FLOAT: 829 // Convert to float. 830 return (float)$param; 831 832 case PARAM_ALPHA: 833 // Remove everything not `a-z`. 834 return preg_replace('/[^a-zA-Z]/i', '', $param); 835 836 case PARAM_ALPHAEXT: 837 // Remove everything not `a-zA-Z_-` (originally allowed "/" too). 838 return preg_replace('/[^a-zA-Z_-]/i', '', $param); 839 840 case PARAM_ALPHANUM: 841 // Remove everything not `a-zA-Z0-9`. 842 return preg_replace('/[^A-Za-z0-9]/i', '', $param); 843 844 case PARAM_ALPHANUMEXT: 845 // Remove everything not `a-zA-Z0-9_-`. 846 return preg_replace('/[^A-Za-z0-9_-]/i', '', $param); 847 848 case PARAM_SEQUENCE: 849 // Remove everything not `0-9,`. 850 return preg_replace('/[^0-9,]/i', '', $param); 851 852 case PARAM_BOOL: 853 // Convert to 1 or 0. 854 $tempstr = strtolower($param); 855 if ($tempstr === 'on' or $tempstr === 'yes' or $tempstr === 'true') { 856 $param = 1; 857 } else if ($tempstr === 'off' or $tempstr === 'no' or $tempstr === 'false') { 858 $param = 0; 859 } else { 860 $param = empty($param) ? 0 : 1; 861 } 862 return $param; 863 864 case PARAM_NOTAGS: 865 // Strip all tags. 866 $param = fix_utf8($param); 867 return strip_tags($param); 868 869 case PARAM_TEXT: 870 // Leave only tags needed for multilang. 871 $param = fix_utf8($param); 872 // If the multilang syntax is not correct we strip all tags because it would break xhtml strict which is required 873 // for accessibility standards please note this cleaning does not strip unbalanced '>' for BC compatibility reasons. 874 do { 875 if (strpos($param, '</lang>') !== false) { 876 // Old and future mutilang syntax. 877 $param = strip_tags($param, '<lang>'); 878 if (!preg_match_all('/<.*>/suU', $param, $matches)) { 879 break; 880 } 881 $open = false; 882 foreach ($matches[0] as $match) { 883 if ($match === '</lang>') { 884 if ($open) { 885 $open = false; 886 continue; 887 } else { 888 break 2; 889 } 890 } 891 if (!preg_match('/^<lang lang="[a-zA-Z0-9_-]+"\s*>$/u', $match)) { 892 break 2; 893 } else { 894 $open = true; 895 } 896 } 897 if ($open) { 898 break; 899 } 900 return $param; 901 902 } else if (strpos($param, '</span>') !== false) { 903 // Current problematic multilang syntax. 904 $param = strip_tags($param, '<span>'); 905 if (!preg_match_all('/<.*>/suU', $param, $matches)) { 906 break; 907 } 908 $open = false; 909 foreach ($matches[0] as $match) { 910 if ($match === '</span>') { 911 if ($open) { 912 $open = false; 913 continue; 914 } else { 915 break 2; 916 } 917 } 918 if (!preg_match('/^<span(\s+lang="[a-zA-Z0-9_-]+"|\s+class="multilang"){2}\s*>$/u', $match)) { 919 break 2; 920 } else { 921 $open = true; 922 } 923 } 924 if ($open) { 925 break; 926 } 927 return $param; 928 } 929 } while (false); 930 // Easy, just strip all tags, if we ever want to fix orphaned '&' we have to do that in format_string(). 931 return strip_tags($param); 932 933 case PARAM_COMPONENT: 934 // We do not want any guessing here, either the name is correct or not 935 // please note only normalised component names are accepted. 936 if (!preg_match('/^[a-z]+(_[a-z][a-z0-9_]*)?[a-z0-9]+$/', $param)) { 937 return ''; 938 } 939 if (strpos($param, '__') !== false) { 940 return ''; 941 } 942 if (strpos($param, 'mod_') === 0) { 943 // Module names must not contain underscores because we need to differentiate them from invalid plugin types. 944 if (substr_count($param, '_') != 1) { 945 return ''; 946 } 947 } 948 return $param; 949 950 case PARAM_PLUGIN: 951 case PARAM_AREA: 952 // We do not want any guessing here, either the name is correct or not. 953 if (!is_valid_plugin_name($param)) { 954 return ''; 955 } 956 return $param; 957 958 case PARAM_SAFEDIR: 959 // Remove everything not a-zA-Z0-9_- . 960 return preg_replace('/[^a-zA-Z0-9_-]/i', '', $param); 961 962 case PARAM_SAFEPATH: 963 // Remove everything not a-zA-Z0-9/_- . 964 return preg_replace('/[^a-zA-Z0-9\/_-]/i', '', $param); 965 966 case PARAM_FILE: 967 // Strip all suspicious characters from filename. 968 $param = fix_utf8($param); 969 $param = preg_replace('~[[:cntrl:]]|[&<>"`\|\':\\\\/]~u', '', $param); 970 if ($param === '.' || $param === '..') { 971 $param = ''; 972 } 973 return $param; 974 975 case PARAM_PATH: 976 // Strip all suspicious characters from file path. 977 $param = fix_utf8($param); 978 $param = str_replace('\\', '/', $param); 979 980 // Explode the path and clean each element using the PARAM_FILE rules. 981 $breadcrumb = explode('/', $param); 982 foreach ($breadcrumb as $key => $crumb) { 983 if ($crumb === '.' && $key === 0) { 984 // Special condition to allow for relative current path such as ./currentdirfile.txt. 985 } else { 986 $crumb = clean_param($crumb, PARAM_FILE); 987 } 988 $breadcrumb[$key] = $crumb; 989 } 990 $param = implode('/', $breadcrumb); 991 992 // Remove multiple current path (./././) and multiple slashes (///). 993 $param = preg_replace('~//+~', '/', $param); 994 $param = preg_replace('~/(\./)+~', '/', $param); 995 return $param; 996 997 case PARAM_HOST: 998 // Allow FQDN or IPv4 dotted quad. 999 $param = preg_replace('/[^\.\d\w-]/', '', $param ); 1000 // Match ipv4 dotted quad. 1001 if (preg_match('/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/', $param, $match)) { 1002 // Confirm values are ok. 1003 if ( $match[0] > 255 1004 || $match[1] > 255 1005 || $match[3] > 255 1006 || $match[4] > 255 ) { 1007 // Hmmm, what kind of dotted quad is this? 1008 $param = ''; 1009 } 1010 } else if ( preg_match('/^[\w\d\.-]+$/', $param) // Dots, hyphens, numbers. 1011 && !preg_match('/^[\.-]/', $param) // No leading dots/hyphens. 1012 && !preg_match('/[\.-]$/', $param) // No trailing dots/hyphens. 1013 ) { 1014 // All is ok - $param is respected. 1015 } else { 1016 // All is not ok... 1017 $param=''; 1018 } 1019 return $param; 1020 1021 case PARAM_URL: // Allow safe ftp, http, mailto urls. 1022 $param = fix_utf8($param); 1023 include_once($CFG->dirroot . '/lib/validateurlsyntax.php'); 1024 if (!empty($param) && validateUrlSyntax($param, 's?H?S?F?E?u-P-a?I?p?f?q?r?')) { 1025 // All is ok, param is respected. 1026 } else { 1027 // Not really ok. 1028 $param =''; 1029 } 1030 return $param; 1031 1032 case PARAM_LOCALURL: 1033 // Allow http absolute, root relative and relative URLs within wwwroot. 1034 $param = clean_param($param, PARAM_URL); 1035 if (!empty($param)) { 1036 1037 // Simulate the HTTPS version of the site. 1038 $httpswwwroot = str_replace('http://', 'https://', $CFG->wwwroot); 1039 1040 if ($param === $CFG->wwwroot) { 1041 // Exact match; 1042 } else if (!empty($CFG->loginhttps) && $param === $httpswwwroot) { 1043 // Exact match; 1044 } else if (preg_match(':^/:', $param)) { 1045 // Root-relative, ok! 1046 } else if (preg_match('/^' . preg_quote($CFG->wwwroot . '/', '/') . '/i', $param)) { 1047 // Absolute, and matches our wwwroot. 1048 } else if (!empty($CFG->loginhttps) && preg_match('/^' . preg_quote($httpswwwroot . '/', '/') . '/i', $param)) { 1049 // Absolute, and matches our httpswwwroot. 1050 } else { 1051 // Relative - let's make sure there are no tricks. 1052 if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?')) { 1053 // Looks ok. 1054 } else { 1055 $param = ''; 1056 } 1057 } 1058 } 1059 return $param; 1060 1061 case PARAM_PEM: 1062 $param = trim($param); 1063 // PEM formatted strings may contain letters/numbers and the symbols: 1064 // forward slash: / 1065 // plus sign: + 1066 // equal sign: = 1067 // , surrounded by BEGIN and END CERTIFICATE prefix and suffixes. 1068 if (preg_match('/^-----BEGIN CERTIFICATE-----([\s\w\/\+=]+)-----END CERTIFICATE-----$/', trim($param), $matches)) { 1069 list($wholething, $body) = $matches; 1070 unset($wholething, $matches); 1071 $b64 = clean_param($body, PARAM_BASE64); 1072 if (!empty($b64)) { 1073 return "-----BEGIN CERTIFICATE-----\n$b64\n-----END CERTIFICATE-----\n"; 1074 } else { 1075 return ''; 1076 } 1077 } 1078 return ''; 1079 1080 case PARAM_BASE64: 1081 if (!empty($param)) { 1082 // PEM formatted strings may contain letters/numbers and the symbols 1083 // forward slash: / 1084 // plus sign: + 1085 // equal sign: =. 1086 if (0 >= preg_match('/^([\s\w\/\+=]+)$/', trim($param))) { 1087 return ''; 1088 } 1089 $lines = preg_split('/[\s]+/', $param, -1, PREG_SPLIT_NO_EMPTY); 1090 // Each line of base64 encoded data must be 64 characters in length, except for the last line which may be less 1091 // than (or equal to) 64 characters long. 1092 for ($i=0, $j=count($lines); $i < $j; $i++) { 1093 if ($i + 1 == $j) { 1094 if (64 < strlen($lines[$i])) { 1095 return ''; 1096 } 1097 continue; 1098 } 1099 1100 if (64 != strlen($lines[$i])) { 1101 return ''; 1102 } 1103 } 1104 return implode("\n", $lines); 1105 } else { 1106 return ''; 1107 } 1108 1109 case PARAM_TAG: 1110 $param = fix_utf8($param); 1111 // Please note it is not safe to use the tag name directly anywhere, 1112 // it must be processed with s(), urlencode() before embedding anywhere. 1113 // Remove some nasties. 1114 $param = preg_replace('~[[:cntrl:]]|[<>`]~u', '', $param); 1115 // Convert many whitespace chars into one. 1116 $param = preg_replace('/\s+/u', ' ', $param); 1117 $param = core_text::substr(trim($param), 0, TAG_MAX_LENGTH); 1118 return $param; 1119 1120 case PARAM_TAGLIST: 1121 $param = fix_utf8($param); 1122 $tags = explode(',', $param); 1123 $result = array(); 1124 foreach ($tags as $tag) { 1125 $res = clean_param($tag, PARAM_TAG); 1126 if ($res !== '') { 1127 $result[] = $res; 1128 } 1129 } 1130 if ($result) { 1131 return implode(',', $result); 1132 } else { 1133 return ''; 1134 } 1135 1136 case PARAM_CAPABILITY: 1137 if (get_capability_info($param)) { 1138 return $param; 1139 } else { 1140 return ''; 1141 } 1142 1143 case PARAM_PERMISSION: 1144 $param = (int)$param; 1145 if (in_array($param, array(CAP_INHERIT, CAP_ALLOW, CAP_PREVENT, CAP_PROHIBIT))) { 1146 return $param; 1147 } else { 1148 return CAP_INHERIT; 1149 } 1150 1151 case PARAM_AUTH: 1152 $param = clean_param($param, PARAM_PLUGIN); 1153 if (empty($param)) { 1154 return ''; 1155 } else if (exists_auth_plugin($param)) { 1156 return $param; 1157 } else { 1158 return ''; 1159 } 1160 1161 case PARAM_LANG: 1162 $param = clean_param($param, PARAM_SAFEDIR); 1163 if (get_string_manager()->translation_exists($param)) { 1164 return $param; 1165 } else { 1166 // Specified language is not installed or param malformed. 1167 return ''; 1168 } 1169 1170 case PARAM_THEME: 1171 $param = clean_param($param, PARAM_PLUGIN); 1172 if (empty($param)) { 1173 return ''; 1174 } else if (file_exists("$CFG->dirroot/theme/$param/config.php")) { 1175 return $param; 1176 } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$param/config.php")) { 1177 return $param; 1178 } else { 1179 // Specified theme is not installed. 1180 return ''; 1181 } 1182 1183 case PARAM_USERNAME: 1184 $param = fix_utf8($param); 1185 $param = trim($param); 1186 // Convert uppercase to lowercase MDL-16919. 1187 $param = core_text::strtolower($param); 1188 if (empty($CFG->extendedusernamechars)) { 1189 $param = str_replace(" " , "", $param); 1190 // Regular expression, eliminate all chars EXCEPT: 1191 // alphanum, dash (-), underscore (_), at sign (@) and period (.) characters. 1192 $param = preg_replace('/[^-\.@_a-z0-9]/', '', $param); 1193 } 1194 return $param; 1195 1196 case PARAM_EMAIL: 1197 $param = fix_utf8($param); 1198 if (validate_email($param)) { 1199 return $param; 1200 } else { 1201 return ''; 1202 } 1203 1204 case PARAM_STRINGID: 1205 if (preg_match('|^[a-zA-Z][a-zA-Z0-9\.:/_-]*$|', $param)) { 1206 return $param; 1207 } else { 1208 return ''; 1209 } 1210 1211 case PARAM_TIMEZONE: 1212 // Can be int, float(with .5 or .0) or string seperated by '/' and can have '-_'. 1213 $param = fix_utf8($param); 1214 $timezonepattern = '/^(([+-]?(0?[0-9](\.[5|0])?|1[0-3](\.0)?|1[0-2]\.5))|(99)|[[:alnum:]]+(\/?[[:alpha:]_-])+)$/'; 1215 if (preg_match($timezonepattern, $param)) { 1216 return $param; 1217 } else { 1218 return ''; 1219 } 1220 1221 default: 1222 // Doh! throw error, switched parameters in optional_param or another serious problem. 1223 print_error("unknownparamtype", '', '', $type); 1224 } 1225 } 1226 1227 /** 1228 * Makes sure the data is using valid utf8, invalid characters are discarded. 1229 * 1230 * Note: this function is not intended for full objects with methods and private properties. 1231 * 1232 * @param mixed $value 1233 * @return mixed with proper utf-8 encoding 1234 */ 1235 function fix_utf8($value) { 1236 if (is_null($value) or $value === '') { 1237 return $value; 1238 1239 } else if (is_string($value)) { 1240 if ((string)(int)$value === $value) { 1241 // Shortcut. 1242 return $value; 1243 } 1244 // No null bytes expected in our data, so let's remove it. 1245 $value = str_replace("\0", '', $value); 1246 1247 // Note: this duplicates min_fix_utf8() intentionally. 1248 static $buggyiconv = null; 1249 if ($buggyiconv === null) { 1250 $buggyiconv = (!function_exists('iconv') or @iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€'); 1251 } 1252 1253 if ($buggyiconv) { 1254 if (function_exists('mb_convert_encoding')) { 1255 $subst = mb_substitute_character(); 1256 mb_substitute_character(''); 1257 $result = mb_convert_encoding($value, 'utf-8', 'utf-8'); 1258 mb_substitute_character($subst); 1259 1260 } else { 1261 // Warn admins on admin/index.php page. 1262 $result = $value; 1263 } 1264 1265 } else { 1266 $result = @iconv('UTF-8', 'UTF-8//IGNORE', $value); 1267 } 1268 1269 return $result; 1270 1271 } else if (is_array($value)) { 1272 foreach ($value as $k => $v) { 1273 $value[$k] = fix_utf8($v); 1274 } 1275 return $value; 1276 1277 } else if (is_object($value)) { 1278 // Do not modify original. 1279 $value = clone($value); 1280 foreach ($value as $k => $v) { 1281 $value->$k = fix_utf8($v); 1282 } 1283 return $value; 1284 1285 } else { 1286 // This is some other type, no utf-8 here. 1287 return $value; 1288 } 1289 } 1290 1291 /** 1292 * Return true if given value is integer or string with integer value 1293 * 1294 * @param mixed $value String or Int 1295 * @return bool true if number, false if not 1296 */ 1297 function is_number($value) { 1298 if (is_int($value)) { 1299 return true; 1300 } else if (is_string($value)) { 1301 return ((string)(int)$value) === $value; 1302 } else { 1303 return false; 1304 } 1305 } 1306 1307 /** 1308 * Returns host part from url. 1309 * 1310 * @param string $url full url 1311 * @return string host, null if not found 1312 */ 1313 function get_host_from_url($url) { 1314 preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches); 1315 if ($matches) { 1316 return $matches[1]; 1317 } 1318 return null; 1319 } 1320 1321 /** 1322 * Tests whether anything was returned by text editor 1323 * 1324 * This function is useful for testing whether something you got back from 1325 * the HTML editor actually contains anything. Sometimes the HTML editor 1326 * appear to be empty, but actually you get back a <br> tag or something. 1327 * 1328 * @param string $string a string containing HTML. 1329 * @return boolean does the string contain any actual content - that is text, 1330 * images, objects, etc. 1331 */ 1332 function html_is_blank($string) { 1333 return trim(strip_tags($string, '<img><object><applet><input><select><textarea><hr>')) == ''; 1334 } 1335 1336 /** 1337 * Set a key in global configuration 1338 * 1339 * Set a key/value pair in both this session's {@link $CFG} global variable 1340 * and in the 'config' database table for future sessions. 1341 * 1342 * Can also be used to update keys for plugin-scoped configs in config_plugin table. 1343 * In that case it doesn't affect $CFG. 1344 * 1345 * A NULL value will delete the entry. 1346 * 1347 * NOTE: this function is called from lib/db/upgrade.php 1348 * 1349 * @param string $name the key to set 1350 * @param string $value the value to set (without magic quotes) 1351 * @param string $plugin (optional) the plugin scope, default null 1352 * @return bool true or exception 1353 */ 1354 function set_config($name, $value, $plugin=null) { 1355 global $CFG, $DB; 1356 1357 if (empty($plugin)) { 1358 if (!array_key_exists($name, $CFG->config_php_settings)) { 1359 // So it's defined for this invocation at least. 1360 if (is_null($value)) { 1361 unset($CFG->$name); 1362 } else { 1363 // Settings from db are always strings. 1364 $CFG->$name = (string)$value; 1365 } 1366 } 1367 1368 if ($DB->get_field('config', 'name', array('name' => $name))) { 1369 if ($value === null) { 1370 $DB->delete_records('config', array('name' => $name)); 1371 } else { 1372 $DB->set_field('config', 'value', $value, array('name' => $name)); 1373 } 1374 } else { 1375 if ($value !== null) { 1376 $config = new stdClass(); 1377 $config->name = $name; 1378 $config->value = $value; 1379 $DB->insert_record('config', $config, false); 1380 } 1381 } 1382 if ($name === 'siteidentifier') { 1383 cache_helper::update_site_identifier($value); 1384 } 1385 cache_helper::invalidate_by_definition('core', 'config', array(), 'core'); 1386 } else { 1387 // Plugin scope. 1388 if ($id = $DB->get_field('config_plugins', 'id', array('name' => $name, 'plugin' => $plugin))) { 1389 if ($value===null) { 1390 $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin)); 1391 } else { 1392 $DB->set_field('config_plugins', 'value', $value, array('id' => $id)); 1393 } 1394 } else { 1395 if ($value !== null) { 1396 $config = new stdClass(); 1397 $config->plugin = $plugin; 1398 $config->name = $name; 1399 $config->value = $value; 1400 $DB->insert_record('config_plugins', $config, false); 1401 } 1402 } 1403 cache_helper::invalidate_by_definition('core', 'config', array(), $plugin); 1404 } 1405 1406 return true; 1407 } 1408 1409 /** 1410 * Get configuration values from the global config table 1411 * or the config_plugins table. 1412 * 1413 * If called with one parameter, it will load all the config 1414 * variables for one plugin, and return them as an object. 1415 * 1416 * If called with 2 parameters it will return a string single 1417 * value or false if the value is not found. 1418 * 1419 * NOTE: this function is called from lib/db/upgrade.php 1420 * 1421 * @static string|false $siteidentifier The site identifier is not cached. We use this static cache so 1422 * that we need only fetch it once per request. 1423 * @param string $plugin full component name 1424 * @param string $name default null 1425 * @return mixed hash-like object or single value, return false no config found 1426 * @throws dml_exception 1427 */ 1428 function get_config($plugin, $name = null) { 1429 global $CFG, $DB; 1430 1431 static $siteidentifier = null; 1432 1433 if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) { 1434 $forced =& $CFG->config_php_settings; 1435 $iscore = true; 1436 $plugin = 'core'; 1437 } else { 1438 if (array_key_exists($plugin, $CFG->forced_plugin_settings)) { 1439 $forced =& $CFG->forced_plugin_settings[$plugin]; 1440 } else { 1441 $forced = array(); 1442 } 1443 $iscore = false; 1444 } 1445 1446 if ($siteidentifier === null) { 1447 try { 1448 // This may fail during installation. 1449 // If you have a look at {@link initialise_cfg()} you will see that this is how we detect the need to 1450 // install the database. 1451 $siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier')); 1452 } catch (dml_exception $ex) { 1453 // Set siteidentifier to false. We don't want to trip this continually. 1454 $siteidentifier = false; 1455 throw $ex; 1456 } 1457 } 1458 1459 if (!empty($name)) { 1460 if (array_key_exists($name, $forced)) { 1461 return (string)$forced[$name]; 1462 } else if ($name === 'siteidentifier' && $plugin == 'core') { 1463 return $siteidentifier; 1464 } 1465 } 1466 1467 $cache = cache::make('core', 'config'); 1468 $result = $cache->get($plugin); 1469 if ($result === false) { 1470 // The user is after a recordset. 1471 if (!$iscore) { 1472 $result = $DB->get_records_menu('config_plugins', array('plugin' => $plugin), '', 'name,value'); 1473 } else { 1474 // This part is not really used any more, but anyway... 1475 $result = $DB->get_records_menu('config', array(), '', 'name,value');; 1476 } 1477 $cache->set($plugin, $result); 1478 } 1479 1480 if (!empty($name)) { 1481 if (array_key_exists($name, $result)) { 1482 return $result[$name]; 1483 } 1484 return false; 1485 } 1486 1487 if ($plugin === 'core') { 1488 $result['siteidentifier'] = $siteidentifier; 1489 } 1490 1491 foreach ($forced as $key => $value) { 1492 if (is_null($value) or is_array($value) or is_object($value)) { 1493 // We do not want any extra mess here, just real settings that could be saved in db. 1494 unset($result[$key]); 1495 } else { 1496 // Convert to string as if it went through the DB. 1497 $result[$key] = (string)$value; 1498 } 1499 } 1500 1501 return (object)$result; 1502 } 1503 1504 /** 1505 * Removes a key from global configuration. 1506 * 1507 * NOTE: this function is called from lib/db/upgrade.php 1508 * 1509 * @param string $name the key to set 1510 * @param string $plugin (optional) the plugin scope 1511 * @return boolean whether the operation succeeded. 1512 */ 1513 function unset_config($name, $plugin=null) { 1514 global $CFG, $DB; 1515 1516 if (empty($plugin)) { 1517 unset($CFG->$name); 1518 $DB->delete_records('config', array('name' => $name)); 1519 cache_helper::invalidate_by_definition('core', 'config', array(), 'core'); 1520 } else { 1521 $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin)); 1522 cache_helper::invalidate_by_definition('core', 'config', array(), $plugin); 1523 } 1524 1525 return true; 1526 } 1527 1528 /** 1529 * Remove all the config variables for a given plugin. 1530 * 1531 * NOTE: this function is called from lib/db/upgrade.php 1532 * 1533 * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice'; 1534 * @return boolean whether the operation succeeded. 1535 */ 1536 function unset_all_config_for_plugin($plugin) { 1537 global $DB; 1538 // Delete from the obvious config_plugins first. 1539 $DB->delete_records('config_plugins', array('plugin' => $plugin)); 1540 // Next delete any suspect settings from config. 1541 $like = $DB->sql_like('name', '?', true, true, false, '|'); 1542 $params = array($DB->sql_like_escape($plugin.'_', '|') . '%'); 1543 $DB->delete_records_select('config', $like, $params); 1544 // Finally clear both the plugin cache and the core cache (suspect settings now removed from core). 1545 cache_helper::invalidate_by_definition('core', 'config', array(), array('core', $plugin)); 1546 1547 return true; 1548 } 1549 1550 /** 1551 * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability. 1552 * 1553 * All users are verified if they still have the necessary capability. 1554 * 1555 * @param string $value the value of the config setting. 1556 * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor. 1557 * @param bool $includeadmins include administrators. 1558 * @return array of user objects. 1559 */ 1560 function get_users_from_config($value, $capability, $includeadmins = true) { 1561 if (empty($value) or $value === '$@NONE@$') { 1562 return array(); 1563 } 1564 1565 // We have to make sure that users still have the necessary capability, 1566 // it should be faster to fetch them all first and then test if they are present 1567 // instead of validating them one-by-one. 1568 $users = get_users_by_capability(context_system::instance(), $capability); 1569 if ($includeadmins) { 1570 $admins = get_admins(); 1571 foreach ($admins as $admin) { 1572 $users[$admin->id] = $admin; 1573 } 1574 } 1575 1576 if ($value === '$@ALL@$') { 1577 return $users; 1578 } 1579 1580 $result = array(); // Result in correct order. 1581 $allowed = explode(',', $value); 1582 foreach ($allowed as $uid) { 1583 if (isset($users[$uid])) { 1584 $user = $users[$uid]; 1585 $result[$user->id] = $user; 1586 } 1587 } 1588 1589 return $result; 1590 } 1591 1592 1593 /** 1594 * Invalidates browser caches and cached data in temp. 1595 * 1596 * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at 1597 * {@link phpunit_util::reset_dataroot()} 1598 * 1599 * @return void 1600 */ 1601 function purge_all_caches() { 1602 global $CFG, $DB; 1603 1604 reset_text_filters_cache(); 1605 js_reset_all_caches(); 1606 theme_reset_all_caches(); 1607 get_string_manager()->reset_caches(); 1608 core_text::reset_caches(); 1609 if (class_exists('core_plugin_manager')) { 1610 core_plugin_manager::reset_caches(); 1611 } 1612 1613 // Bump up cacherev field for all courses. 1614 try { 1615 increment_revision_number('course', 'cacherev', ''); 1616 } catch (moodle_exception $e) { 1617 // Ignore exception since this function is also called before upgrade script when field course.cacherev does not exist yet. 1618 } 1619 1620 $DB->reset_caches(); 1621 cache_helper::purge_all(); 1622 1623 // Purge all other caches: rss, simplepie, etc. 1624 remove_dir($CFG->cachedir.'', true); 1625 1626 // Make sure cache dir is writable, throws exception if not. 1627 make_cache_directory(''); 1628 1629 // This is the only place where we purge local caches, we are only adding files there. 1630 // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes. 1631 remove_dir($CFG->localcachedir, true); 1632 set_config('localcachedirpurged', time()); 1633 make_localcache_directory('', true); 1634 \core\task\manager::clear_static_caches(); 1635 } 1636 1637 /** 1638 * Get volatile flags 1639 * 1640 * @param string $type 1641 * @param int $changedsince default null 1642 * @return array records array 1643 */ 1644 function get_cache_flags($type, $changedsince = null) { 1645 global $DB; 1646 1647 $params = array('type' => $type, 'expiry' => time()); 1648 $sqlwhere = "flagtype = :type AND expiry >= :expiry"; 1649 if ($changedsince !== null) { 1650 $params['changedsince'] = $changedsince; 1651 $sqlwhere .= " AND timemodified > :changedsince"; 1652 } 1653 $cf = array(); 1654 if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) { 1655 foreach ($flags as $flag) { 1656 $cf[$flag->name] = $flag->value; 1657 } 1658 } 1659 return $cf; 1660 } 1661 1662 /** 1663 * Get volatile flags 1664 * 1665 * @param string $type 1666 * @param string $name 1667 * @param int $changedsince default null 1668 * @return string|false The cache flag value or false 1669 */ 1670 function get_cache_flag($type, $name, $changedsince=null) { 1671 global $DB; 1672 1673 $params = array('type' => $type, 'name' => $name, 'expiry' => time()); 1674 1675 $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry"; 1676 if ($changedsince !== null) { 1677 $params['changedsince'] = $changedsince; 1678 $sqlwhere .= " AND timemodified > :changedsince"; 1679 } 1680 1681 return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params); 1682 } 1683 1684 /** 1685 * Set a volatile flag 1686 * 1687 * @param string $type the "type" namespace for the key 1688 * @param string $name the key to set 1689 * @param string $value the value to set (without magic quotes) - null will remove the flag 1690 * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs 1691 * @return bool Always returns true 1692 */ 1693 function set_cache_flag($type, $name, $value, $expiry = null) { 1694 global $DB; 1695 1696 $timemodified = time(); 1697 if ($expiry === null || $expiry < $timemodified) { 1698 $expiry = $timemodified + 24 * 60 * 60; 1699 } else { 1700 $expiry = (int)$expiry; 1701 } 1702 1703 if ($value === null) { 1704 unset_cache_flag($type, $name); 1705 return true; 1706 } 1707 1708 if ($f = $DB->get_record('cache_flags', array('name' => $name, 'flagtype' => $type), '*', IGNORE_MULTIPLE)) { 1709 // This is a potential problem in DEBUG_DEVELOPER. 1710 if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) { 1711 return true; // No need to update. 1712 } 1713 $f->value = $value; 1714 $f->expiry = $expiry; 1715 $f->timemodified = $timemodified; 1716 $DB->update_record('cache_flags', $f); 1717 } else { 1718 $f = new stdClass(); 1719 $f->flagtype = $type; 1720 $f->name = $name; 1721 $f->value = $value; 1722 $f->expiry = $expiry; 1723 $f->timemodified = $timemodified; 1724 $DB->insert_record('cache_flags', $f); 1725 } 1726 return true; 1727 } 1728 1729 /** 1730 * Removes a single volatile flag 1731 * 1732 * @param string $type the "type" namespace for the key 1733 * @param string $name the key to set 1734 * @return bool 1735 */ 1736 function unset_cache_flag($type, $name) { 1737 global $DB; 1738 $DB->delete_records('cache_flags', array('name' => $name, 'flagtype' => $type)); 1739 return true; 1740 } 1741 1742 /** 1743 * Garbage-collect volatile flags 1744 * 1745 * @return bool Always returns true 1746 */ 1747 function gc_cache_flags() { 1748 global $DB; 1749 $DB->delete_records_select('cache_flags', 'expiry < ?', array(time())); 1750 return true; 1751 } 1752 1753 // USER PREFERENCE API. 1754 1755 /** 1756 * Refresh user preference cache. This is used most often for $USER 1757 * object that is stored in session, but it also helps with performance in cron script. 1758 * 1759 * Preferences for each user are loaded on first use on every page, then again after the timeout expires. 1760 * 1761 * @package core 1762 * @category preference 1763 * @access public 1764 * @param stdClass $user User object. Preferences are preloaded into 'preference' property 1765 * @param int $cachelifetime Cache life time on the current page (in seconds) 1766 * @throws coding_exception 1767 * @return null 1768 */ 1769 function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120) { 1770 global $DB; 1771 // Static cache, we need to check on each page load, not only every 2 minutes. 1772 static $loadedusers = array(); 1773 1774 if (!isset($user->id)) { 1775 throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field'); 1776 } 1777 1778 if (empty($user->id) or isguestuser($user->id)) { 1779 // No permanent storage for not-logged-in users and guest. 1780 if (!isset($user->preference)) { 1781 $user->preference = array(); 1782 } 1783 return; 1784 } 1785 1786 $timenow = time(); 1787 1788 if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) { 1789 // Already loaded at least once on this page. Are we up to date? 1790 if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) { 1791 // No need to reload - we are on the same page and we loaded prefs just a moment ago. 1792 return; 1793 1794 } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) { 1795 // No change since the lastcheck on this page. 1796 $user->preference['_lastloaded'] = $timenow; 1797 return; 1798 } 1799 } 1800 1801 // OK, so we have to reload all preferences. 1802 $loadedusers[$user->id] = true; 1803 $user->preference = $DB->get_records_menu('user_preferences', array('userid' => $user->id), '', 'name,value'); // All values. 1804 $user->preference['_lastloaded'] = $timenow; 1805 } 1806 1807 /** 1808 * Called from set/unset_user_preferences, so that the prefs can be correctly reloaded in different sessions. 1809 * 1810 * NOTE: internal function, do not call from other code. 1811 * 1812 * @package core 1813 * @access private 1814 * @param integer $userid the user whose prefs were changed. 1815 */ 1816 function mark_user_preferences_changed($userid) { 1817 global $CFG; 1818 1819 if (empty($userid) or isguestuser($userid)) { 1820 // No cache flags for guest and not-logged-in users. 1821 return; 1822 } 1823 1824 set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout); 1825 } 1826 1827 /** 1828 * Sets a preference for the specified user. 1829 * 1830 * If a $user object is submitted it's 'preference' property is used for the preferences cache. 1831 * 1832 * @package core 1833 * @category preference 1834 * @access public 1835 * @param string $name The key to set as preference for the specified user 1836 * @param string $value The value to set for the $name key in the specified user's 1837 * record, null means delete current value. 1838 * @param stdClass|int|null $user A moodle user object or id, null means current user 1839 * @throws coding_exception 1840 * @return bool Always true or exception 1841 */ 1842 function set_user_preference($name, $value, $user = null) { 1843 global $USER, $DB; 1844 1845 if (empty($name) or is_numeric($name) or $name === '_lastloaded') { 1846 throw new coding_exception('Invalid preference name in set_user_preference() call'); 1847 } 1848 1849 if (is_null($value)) { 1850 // Null means delete current. 1851 return unset_user_preference($name, $user); 1852 } else if (is_object($value)) { 1853 throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed'); 1854 } else if (is_array($value)) { 1855 throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed'); 1856 } 1857 // Value column maximum length is 1333 characters. 1858 $value = (string)$value; 1859 if (core_text::strlen($value) > 1333) { 1860 throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column'); 1861 } 1862 1863 if (is_null($user)) { 1864 $user = $USER; 1865 } else if (isset($user->id)) { 1866 // It is a valid object. 1867 } else if (is_numeric($user)) { 1868 $user = (object)array('id' => (int)$user); 1869 } else { 1870 throw new coding_exception('Invalid $user parameter in set_user_preference() call'); 1871 } 1872 1873 check_user_preferences_loaded($user); 1874 1875 if (empty($user->id) or isguestuser($user->id)) { 1876 // No permanent storage for not-logged-in users and guest. 1877 $user->preference[$name] = $value; 1878 return true; 1879 } 1880 1881 if ($preference = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => $name))) { 1882 if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) { 1883 // Preference already set to this value. 1884 return true; 1885 } 1886 $DB->set_field('user_preferences', 'value', $value, array('id' => $preference->id)); 1887 1888 } else { 1889 $preference = new stdClass(); 1890 $preference->userid = $user->id; 1891 $preference->name = $name; 1892 $preference->value = $value; 1893 $DB->insert_record('user_preferences', $preference); 1894 } 1895 1896 // Update value in cache. 1897 $user->preference[$name] = $value; 1898 1899 // Set reload flag for other sessions. 1900 mark_user_preferences_changed($user->id); 1901 1902 return true; 1903 } 1904 1905 /** 1906 * Sets a whole array of preferences for the current user 1907 * 1908 * If a $user object is submitted it's 'preference' property is used for the preferences cache. 1909 * 1910 * @package core 1911 * @category preference 1912 * @access public 1913 * @param array $prefarray An array of key/value pairs to be set 1914 * @param stdClass|int|null $user A moodle user object or id, null means current user 1915 * @return bool Always true or exception 1916 */ 1917 function set_user_preferences(array $prefarray, $user = null) { 1918 foreach ($prefarray as $name => $value) { 1919 set_user_preference($name, $value, $user); 1920 } 1921 return true; 1922 } 1923 1924 /** 1925 * Unsets a preference completely by deleting it from the database 1926 * 1927 * If a $user object is submitted it's 'preference' property is used for the preferences cache. 1928 * 1929 * @package core 1930 * @category preference 1931 * @access public 1932 * @param string $name The key to unset as preference for the specified user 1933 * @param stdClass|int|null $user A moodle user object or id, null means current user 1934 * @throws coding_exception 1935 * @return bool Always true or exception 1936 */ 1937 function unset_user_preference($name, $user = null) { 1938 global $USER, $DB; 1939 1940 if (empty($name) or is_numeric($name) or $name === '_lastloaded') { 1941 throw new coding_exception('Invalid preference name in unset_user_preference() call'); 1942 } 1943 1944 if (is_null($user)) { 1945 $user = $USER; 1946 } else if (isset($user->id)) { 1947 // It is a valid object. 1948 } else if (is_numeric($user)) { 1949 $user = (object)array('id' => (int)$user); 1950 } else { 1951 throw new coding_exception('Invalid $user parameter in unset_user_preference() call'); 1952 } 1953 1954 check_user_preferences_loaded($user); 1955 1956 if (empty($user->id) or isguestuser($user->id)) { 1957 // No permanent storage for not-logged-in user and guest. 1958 unset($user->preference[$name]); 1959 return true; 1960 } 1961 1962 // Delete from DB. 1963 $DB->delete_records('user_preferences', array('userid' => $user->id, 'name' => $name)); 1964 1965 // Delete the preference from cache. 1966 unset($user->preference[$name]); 1967 1968 // Set reload flag for other sessions. 1969 mark_user_preferences_changed($user->id); 1970 1971 return true; 1972 } 1973 1974 /** 1975 * Used to fetch user preference(s) 1976 * 1977 * If no arguments are supplied this function will return 1978 * all of the current user preferences as an array. 1979 * 1980 * If a name is specified then this function 1981 * attempts to return that particular preference value. If 1982 * none is found, then the optional value $default is returned, 1983 * otherwise null. 1984 * 1985 * If a $user object is submitted it's 'preference' property is used for the preferences cache. 1986 * 1987 * @package core 1988 * @category preference 1989 * @access public 1990 * @param string $name Name of the key to use in finding a preference value 1991 * @param mixed|null $default Value to be returned if the $name key is not set in the user preferences 1992 * @param stdClass|int|null $user A moodle user object or id, null means current user 1993 * @throws coding_exception 1994 * @return string|mixed|null A string containing the value of a single preference. An 1995 * array with all of the preferences or null 1996 */ 1997 function get_user_preferences($name = null, $default = null, $user = null) { 1998 global $USER; 1999 2000 if (is_null($name)) { 2001 // All prefs. 2002 } else if (is_numeric($name) or $name === '_lastloaded') { 2003 throw new coding_exception('Invalid preference name in get_user_preferences() call'); 2004 } 2005 2006 if (is_null($user)) { 2007 $user = $USER; 2008 } else if (isset($user->id)) { 2009 // Is a valid object. 2010 } else if (is_numeric($user)) { 2011 $user = (object)array('id' => (int)$user); 2012 } else { 2013 throw new coding_exception('Invalid $user parameter in get_user_preferences() call'); 2014 } 2015 2016 check_user_preferences_loaded($user); 2017 2018 if (empty($name)) { 2019 // All values. 2020 return $user->preference; 2021 } else if (isset($user->preference[$name])) { 2022 // The single string value. 2023 return $user->preference[$name]; 2024 } else { 2025 // Default value (null if not specified). 2026 return $default; 2027 } 2028 } 2029 2030 // FUNCTIONS FOR HANDLING TIME. 2031 2032 /** 2033 * Given Gregorian date parts in user time produce a GMT timestamp. 2034 * 2035 * @package core 2036 * @category time 2037 * @param int $year The year part to create timestamp of 2038 * @param int $month The month part to create timestamp of 2039 * @param int $day The day part to create timestamp of 2040 * @param int $hour The hour part to create timestamp of 2041 * @param int $minute The minute part to create timestamp of 2042 * @param int $second The second part to create timestamp of 2043 * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset. 2044 * if 99 then default user's timezone is used {@link http://docs.moodle.org/dev/Time_API#Timezone} 2045 * @param bool $applydst Toggle Daylight Saving Time, default true, will be 2046 * applied only if timezone is 99 or string. 2047 * @return int GMT timestamp 2048 */ 2049 function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) { 2050 $date = new DateTime('now', core_date::get_user_timezone_object($timezone)); 2051 $date->setDate((int)$year, (int)$month, (int)$day); 2052 $date->setTime((int)$hour, (int)$minute, (int)$second); 2053 2054 $time = $date->getTimestamp(); 2055 2056 // Moodle BC DST stuff. 2057 if (!$applydst) { 2058 $time += dst_offset_on($time, $timezone); 2059 } 2060 2061 return $time; 2062 2063 } 2064 2065 /** 2066 * Format a date/time (seconds) as weeks, days, hours etc as needed 2067 * 2068 * Given an amount of time in seconds, returns string 2069 * formatted nicely as weeks, days, hours etc as needed 2070 * 2071 * @package core 2072 * @category time 2073 * @uses MINSECS 2074 * @uses HOURSECS 2075 * @uses DAYSECS 2076 * @uses YEARSECS 2077 * @param int $totalsecs Time in seconds 2078 * @param stdClass $str Should be a time object 2079 * @return string A nicely formatted date/time string 2080 */ 2081 function format_time($totalsecs, $str = null) { 2082 2083 $totalsecs = abs($totalsecs); 2084 2085 if (!$str) { 2086 // Create the str structure the slow way. 2087 $str = new stdClass(); 2088 $str->day = get_string('day'); 2089 $str->days = get_string('days'); 2090 $str->hour = get_string('hour'); 2091 $str->hours = get_string('hours'); 2092 $str->min = get_string('min'); 2093 $str->mins = get_string('mins'); 2094 $str->sec = get_string('sec'); 2095 $str->secs = get_string('secs'); 2096 $str->year = get_string('year'); 2097 $str->years = get_string('years'); 2098 } 2099 2100 $years = floor($totalsecs/YEARSECS); 2101 $remainder = $totalsecs - ($years*YEARSECS); 2102 $days = floor($remainder/DAYSECS); 2103 $remainder = $totalsecs - ($days*DAYSECS); 2104 $hours = floor($remainder/HOURSECS); 2105 $remainder = $remainder - ($hours*HOURSECS); 2106 $mins = floor($remainder/MINSECS); 2107 $secs = $remainder - ($mins*MINSECS); 2108 2109 $ss = ($secs == 1) ? $str->sec : $str->secs; 2110 $sm = ($mins == 1) ? $str->min : $str->mins; 2111 $sh = ($hours == 1) ? $str->hour : $str->hours; 2112 $sd = ($days == 1) ? $str->day : $str->days; 2113 $sy = ($years == 1) ? $str->year : $str->years; 2114 2115 $oyears = ''; 2116 $odays = ''; 2117 $ohours = ''; 2118 $omins = ''; 2119 $osecs = ''; 2120 2121 if ($years) { 2122 $oyears = $years .' '. $sy; 2123 } 2124 if ($days) { 2125 $odays = $days .' '. $sd; 2126 } 2127 if ($hours) { 2128 $ohours = $hours .' '. $sh; 2129 } 2130 if ($mins) { 2131 $omins = $mins .' '. $sm; 2132 } 2133 if ($secs) { 2134 $osecs = $secs .' '. $ss; 2135 } 2136 2137 if ($years) { 2138 return trim($oyears .' '. $odays); 2139 } 2140 if ($days) { 2141 return trim($odays .' '. $ohours); 2142 } 2143 if ($hours) { 2144 return trim($ohours .' '. $omins); 2145 } 2146 if ($mins) { 2147 return trim($omins .' '. $osecs); 2148 } 2149 if ($secs) { 2150 return $osecs; 2151 } 2152 return get_string('now'); 2153 } 2154 2155 /** 2156 * Returns a formatted string that represents a date in user time. 2157 * 2158 * @package core 2159 * @category time 2160 * @param int $date the timestamp in UTC, as obtained from the database. 2161 * @param string $format strftime format. You should probably get this using 2162 * get_string('strftime...', 'langconfig'); 2163 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and 2164 * not 99 then daylight saving will not be added. 2165 * {@link http://docs.moodle.org/dev/Time_API#Timezone} 2166 * @param bool $fixday If true (default) then the leading zero from %d is removed. 2167 * If false then the leading zero is maintained. 2168 * @param bool $fixhour If true (default) then the leading zero from %I is removed. 2169 * @return string the formatted date/time. 2170 */ 2171 function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) { 2172 $calendartype = \core_calendar\type_factory::get_calendar_instance(); 2173 return $calendartype->timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour); 2174 } 2175 2176 /** 2177 * Returns a formatted date ensuring it is UTF-8. 2178 * 2179 * If we are running under Windows convert to Windows encoding and then back to UTF-8 2180 * (because it's impossible to specify UTF-8 to fetch locale info in Win32). 2181 * 2182 * @param int $date the timestamp - since Moodle 2.9 this is a real UTC timestamp 2183 * @param string $format strftime format. 2184 * @param int|float|string $tz the user timezone 2185 * @return string the formatted date/time. 2186 * @since Moodle 2.3.3 2187 */ 2188 function date_format_string($date, $format, $tz = 99) { 2189 global $CFG; 2190 2191 $localewincharset = null; 2192 // Get the calendar type user is using. 2193 if ($CFG->ostype == 'WINDOWS') { 2194 $calendartype = \core_calendar\type_factory::get_calendar_instance(); 2195 $localewincharset = $calendartype->locale_win_charset(); 2196 } 2197 2198 if ($localewincharset) { 2199 $format = core_text::convert($format, 'utf-8', $localewincharset); 2200 } 2201 2202 date_default_timezone_set(core_date::get_user_timezone($tz)); 2203 $datestring = strftime($format, $date); 2204 core_date::set_default_server_timezone(); 2205 2206 if ($localewincharset) { 2207 $datestring = core_text::convert($datestring, $localewincharset, 'utf-8'); 2208 } 2209 2210 return $datestring; 2211 } 2212 2213 /** 2214 * Given a $time timestamp in GMT (seconds since epoch), 2215 * returns an array that represents the Gregorian date in user time 2216 * 2217 * @package core 2218 * @category time 2219 * @param int $time Timestamp in GMT 2220 * @param float|int|string $timezone user timezone 2221 * @return array An array that represents the date in user time 2222 */ 2223 function usergetdate($time, $timezone=99) { 2224 date_default_timezone_set(core_date::get_user_timezone($timezone)); 2225 $result = getdate($time); 2226 core_date::set_default_server_timezone(); 2227 2228 return $result; 2229 } 2230 2231 /** 2232 * Given a GMT timestamp (seconds since epoch), offsets it by 2233 * the timezone. eg 3pm in India is 3pm GMT - 7 * 3600 seconds 2234 * 2235 * NOTE: this function does not include DST properly, 2236 * you should use the PHP date stuff instead! 2237 * 2238 * @package core 2239 * @category time 2240 * @param int $date Timestamp in GMT 2241 * @param float|int|string $timezone user timezone 2242 * @return int 2243 */ 2244 function usertime($date, $timezone=99) { 2245 $userdate = new DateTime('@' . $date); 2246 $userdate->setTimezone(core_date::get_user_timezone_object($timezone)); 2247 $dst = dst_offset_on($date, $timezone); 2248 2249 return $date - $userdate->getOffset() + $dst; 2250 } 2251 2252 /** 2253 * Given a time, return the GMT timestamp of the most recent midnight 2254 * for the current user. 2255 * 2256 * @package core 2257 * @category time 2258 * @param int $date Timestamp in GMT 2259 * @param float|int|string $timezone user timezone 2260 * @return int Returns a GMT timestamp 2261 */ 2262 function usergetmidnight($date, $timezone=99) { 2263 2264 $userdate = usergetdate($date, $timezone); 2265 2266 // Time of midnight of this user's day, in GMT. 2267 return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone); 2268 2269 } 2270 2271 /** 2272 * Returns a string that prints the user's timezone 2273 * 2274 * @package core 2275 * @category time 2276 * @param float|int|string $timezone user timezone 2277 * @return string 2278 */ 2279 function usertimezone($timezone=99) { 2280 $tz = core_date::get_user_timezone($timezone); 2281 return core_date::get_localised_timezone($tz); 2282 } 2283 2284 /** 2285 * Returns a float or a string which denotes the user's timezone 2286 * 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) 2287 * means that for this timezone there are also DST rules to be taken into account 2288 * Checks various settings and picks the most dominant of those which have a value 2289 * 2290 * @package core 2291 * @category time 2292 * @param float|int|string $tz timezone to calculate GMT time offset before 2293 * calculating user timezone, 99 is default user timezone 2294 * {@link http://docs.moodle.org/dev/Time_API#Timezone} 2295 * @return float|string 2296 */ 2297 function get_user_timezone($tz = 99) { 2298 global $USER, $CFG; 2299 2300 $timezones = array( 2301 $tz, 2302 isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99, 2303 isset($USER->timezone) ? $USER->timezone : 99, 2304 isset($CFG->timezone) ? $CFG->timezone : 99, 2305 ); 2306 2307 $tz = 99; 2308 2309 // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array. 2310 while (((empty($tz) && !is_numeric($tz)) || $tz == 99) && $next = each($timezones)) { 2311 $tz = $next['value']; 2312 } 2313 return is_numeric($tz) ? (float) $tz : $tz; 2314 } 2315 2316 /** 2317 * Calculates the Daylight Saving Offset for a given date/time (timestamp) 2318 * - Note: Daylight saving only works for string timezones and not for float. 2319 * 2320 * @package core 2321 * @category time 2322 * @param int $time must NOT be compensated at all, it has to be a pure timestamp 2323 * @param int|float|string $strtimezone user timezone 2324 * @return int 2325 */ 2326 function dst_offset_on($time, $strtimezone = null) { 2327 $tz = core_date::get_user_timezone($strtimezone); 2328 $date = new DateTime('@' . $time); 2329 $date->setTimezone(new DateTimeZone($tz)); 2330 if ($date->format('I') == '1') { 2331 if ($tz === 'Australia/Lord_Howe') { 2332 return 1800; 2333 } 2334 return 3600; 2335 } 2336 return 0; 2337 } 2338 2339 /** 2340 * Calculates when the day appears in specific month 2341 * 2342 * @package core 2343 * @category time 2344 * @param int $startday starting day of the month 2345 * @param int $weekday The day when week starts (normally taken from user preferences) 2346 * @param int $month The month whose day is sought 2347 * @param int $year The year of the month whose day is sought 2348 * @return int 2349 */ 2350 function find_day_in_month($startday, $weekday, $month, $year) { 2351 $calendartype = \core_calendar\type_factory::get_calendar_instance(); 2352 2353 $daysinmonth = days_in_month($month, $year); 2354 $daysinweek = count($calendartype->get_weekdays()); 2355 2356 if ($weekday == -1) { 2357 // Don't care about weekday, so return: 2358 // abs($startday) if $startday != -1 2359 // $daysinmonth otherwise. 2360 return ($startday == -1) ? $daysinmonth : abs($startday); 2361 } 2362 2363 // From now on we 're looking for a specific weekday. 2364 // Give "end of month" its actual value, since we know it. 2365 if ($startday == -1) { 2366 $startday = -1 * $daysinmonth; 2367 } 2368 2369 // Starting from day $startday, the sign is the direction. 2370 if ($startday < 1) { 2371 $startday = abs($startday); 2372 $lastmonthweekday = dayofweek($daysinmonth, $month, $year); 2373 2374 // This is the last such weekday of the month. 2375 $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday; 2376 if ($lastinmonth > $daysinmonth) { 2377 $lastinmonth -= $daysinweek; 2378 } 2379 2380 // Find the first such weekday <= $startday. 2381 while ($lastinmonth > $startday) { 2382 $lastinmonth -= $daysinweek; 2383 } 2384 2385 return $lastinmonth; 2386 } else { 2387 $indexweekday = dayofweek($startday, $month, $year); 2388 2389 $diff = $weekday - $indexweekday; 2390 if ($diff < 0) { 2391 $diff += $daysinweek; 2392 } 2393 2394 // This is the first such weekday of the month equal to or after $startday. 2395 $firstfromindex = $startday + $diff; 2396 2397 return $firstfromindex; 2398 } 2399 } 2400 2401 /** 2402 * Calculate the number of days in a given month 2403 * 2404 * @package core 2405 * @category time 2406 * @param int $month The month whose day count is sought 2407 * @param int $year The year of the month whose day count is sought 2408 * @return int 2409 */ 2410 function days_in_month($month, $year) { 2411 $calendartype = \core_calendar\type_factory::get_calendar_instance(); 2412 return $calendartype->get_num_days_in_month($year, $month); 2413 } 2414 2415 /** 2416 * Calculate the position in the week of a specific calendar day 2417 * 2418 * @package core 2419 * @category time 2420 * @param int $day The day of the date whose position in the week is sought 2421 * @param int $month The month of the date whose position in the week is sought 2422 * @param int $year The year of the date whose position in the week is sought 2423 * @return int 2424 */ 2425 function dayofweek($day, $month, $year) { 2426 $calendartype = \core_calendar\type_factory::get_calendar_instance(); 2427 return $calendartype->get_weekday($year, $month, $day); 2428 } 2429 2430 // USER AUTHENTICATION AND LOGIN. 2431 2432 /** 2433 * Returns full login url. 2434 * 2435 * @return string login url 2436 */ 2437 function get_login_url() { 2438 global $CFG; 2439 2440 $url = "$CFG->wwwroot/login/index.php"; 2441 2442 if (!empty($CFG->loginhttps)) { 2443 $url = str_replace('http:', 'https:', $url); 2444 } 2445 2446 return $url; 2447 } 2448 2449 /** 2450 * This function checks that the current user is logged in and has the 2451 * required privileges 2452 * 2453 * This function checks that the current user is logged in, and optionally 2454 * whether they are allowed to be in a particular course and view a particular 2455 * course module. 2456 * If they are not logged in, then it redirects them to the site login unless 2457 * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which 2458 * case they are automatically logged in as guests. 2459 * If $courseid is given and the user is not enrolled in that course then the 2460 * user is redirected to the course enrolment page. 2461 * If $cm is given and the course module is hidden and the user is not a teacher 2462 * in the course then the user is redirected to the course home page. 2463 * 2464 * When $cm parameter specified, this function sets page layout to 'module'. 2465 * You need to change it manually later if some other layout needed. 2466 * 2467 * @package core_access 2468 * @category access 2469 * 2470 * @param mixed $courseorid id of the course or course object 2471 * @param bool $autologinguest default true 2472 * @param object $cm course module object 2473 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to 2474 * true. Used to avoid (=false) some scripts (file.php...) to set that variable, 2475 * in order to keep redirects working properly. MDL-14495 2476 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions 2477 * @return mixed Void, exit, and die depending on path 2478 * @throws coding_exception 2479 * @throws require_login_exception 2480 */ 2481 function require_login($courseorid = null, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) { 2482 global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT; 2483 2484 // Must not redirect when byteserving already started. 2485 if (!empty($_SERVER['HTTP_RANGE'])) { 2486 $preventredirect = true; 2487 } 2488 2489 if (AJAX_SCRIPT) { 2490 // We cannot redirect for AJAX scripts either. 2491 $preventredirect = true; 2492 } 2493 2494 // Setup global $COURSE, themes, language and locale. 2495 if (!empty($courseorid)) { 2496 if (is_object($courseorid)) { 2497 $course = $courseorid; 2498 } else if ($courseorid == SITEID) { 2499 $course = clone($SITE); 2500 } else { 2501 $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST); 2502 } 2503 if ($cm) { 2504 if ($cm->course != $course->id) { 2505 throw new coding_exception('course and cm parameters in require_login() call do not match!!'); 2506 } 2507 // Make sure we have a $cm from get_fast_modinfo as this contains activity access details. 2508 if (!($cm instanceof cm_info)) { 2509 // Note: nearly all pages call get_fast_modinfo anyway and it does not make any 2510 // db queries so this is not really a performance concern, however it is obviously 2511 // better if you use get_fast_modinfo to get the cm before calling this. 2512 $modinfo = get_fast_modinfo($course); 2513 $cm = $modinfo->get_cm($cm->id); 2514 } 2515 } 2516 } else { 2517 // Do not touch global $COURSE via $PAGE->set_course(), 2518 // the reasons is we need to be able to call require_login() at any time!! 2519 $course = $SITE; 2520 if ($cm) { 2521 throw new coding_exception('cm parameter in require_login() requires valid course parameter!'); 2522 } 2523 } 2524 2525 // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false. 2526 // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future 2527 // risk leading the user back to the AJAX request URL. 2528 if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) { 2529 $setwantsurltome = false; 2530 } 2531 2532 // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour. 2533 if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !empty($CFG->dbsessions)) { 2534 if ($preventredirect) { 2535 throw new require_login_session_timeout_exception(); 2536 } else { 2537 if ($setwantsurltome) { 2538 $SESSION->wantsurl = qualified_me(); 2539 } 2540 redirect(get_login_url()); 2541 } 2542 } 2543 2544 // If the user is not even logged in yet then make sure they are. 2545 if (!isloggedin()) { 2546 if ($autologinguest and !empty($CFG->guestloginbutton) and !empty($CFG->autologinguests)) { 2547 if (!$guest = get_complete_user_data('id', $CFG->siteguest)) { 2548 // Misconfigured site guest, just redirect to login page. 2549 redirect(get_login_url()); 2550 exit; // Never reached. 2551 } 2552 $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang; 2553 complete_user_login($guest); 2554 $USER->autologinguest = true; 2555 $SESSION->lang = $lang; 2556 } else { 2557 // NOTE: $USER->site check was obsoleted by session test cookie, $USER->confirmed test is in login/index.php. 2558 if ($preventredirect) { 2559 throw new require_login_exception('You are not logged in'); 2560 } 2561 2562 if ($setwantsurltome) { 2563 $SESSION->wantsurl = qualified_me(); 2564 } 2565 2566 $referer = get_local_referer(false); 2567 if (!empty($referer)) { 2568 $SESSION->fromurl = $referer; 2569 } 2570 2571 // Give auth plugins an opportunity to authenticate or redirect to an external login page 2572 $authsequence = get_enabled_auth_plugins(true); // auths, in sequence 2573 foreach($authsequence as $authname) { 2574 $authplugin = get_auth_plugin($authname); 2575 $authplugin->pre_loginpage_hook(); 2576 if (isloggedin()) { 2577 break; 2578 } 2579 } 2580 2581 // If we're still not logged in then go to the login page 2582 if (!isloggedin()) { 2583 redirect(get_login_url()); 2584 exit; // Never reached. 2585 } 2586 } 2587 } 2588 2589 // Loginas as redirection if needed. 2590 if ($course->id != SITEID and \core\session\manager::is_loggedinas()) { 2591 if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) { 2592 if ($USER->loginascontext->instanceid != $course->id) { 2593 print_error('loginasonecourse', '', $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid); 2594 } 2595 } 2596 } 2597 2598 // Check whether the user should be changing password (but only if it is REALLY them). 2599 if (get_user_preferences('auth_forcepasswordchange') && !\core\session\manager::is_loggedinas()) { 2600 $userauth = get_auth_plugin($USER->auth); 2601 if ($userauth->can_change_password() and !$preventredirect) { 2602 if ($setwantsurltome) { 2603 $SESSION->wantsurl = qualified_me(); 2604 } 2605 if ($changeurl = $userauth->change_password_url()) { 2606 // Use plugin custom url. 2607 redirect($changeurl); 2608 } else { 2609 // Use moodle internal method. 2610 if (empty($CFG->loginhttps)) { 2611 redirect($CFG->wwwroot .'/login/change_password.php'); 2612 } else { 2613 $wwwroot = str_replace('http:', 'https:', $CFG->wwwroot); 2614 redirect($wwwroot .'/login/change_password.php'); 2615 } 2616 } 2617 } else { 2618 print_error('nopasswordchangeforced', 'auth'); 2619 } 2620 } 2621 2622 // Check that the user account is properly set up. 2623 if (user_not_fully_set_up($USER)) { 2624 if ($preventredirect) { 2625 throw new require_login_exception('User not fully set-up'); 2626 } 2627 if ($setwantsurltome) { 2628 $SESSION->wantsurl = qualified_me(); 2629 } 2630 redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&course='. SITEID); 2631 } 2632 2633 // Make sure the USER has a sesskey set up. Used for CSRF protection. 2634 sesskey(); 2635 2636 // Do not bother admins with any formalities. 2637 if (is_siteadmin()) { 2638 // Set the global $COURSE. 2639 if ($cm) { 2640 $PAGE->set_cm($cm, $course); 2641 $PAGE->set_pagelayout('incourse'); 2642 } else if (!empty($courseorid)) { 2643 $PAGE->set_course($course); 2644 } 2645 // Set accesstime or the user will appear offline which messes up messaging. 2646 user_accesstime_log($course->id); 2647 return; 2648 } 2649 2650 // Check that the user has agreed to a site policy if there is one - do not test in case of admins. 2651 if (!$USER->policyagreed and !is_siteadmin()) { 2652 if (!empty($CFG->sitepolicy) and !isguestuser()) { 2653 if ($preventredirect) { 2654 throw new require_login_exception('Policy not agreed'); 2655 } 2656 if ($setwantsurltome) { 2657 $SESSION->wantsurl = qualified_me(); 2658 } 2659 redirect($CFG->wwwroot .'/user/policy.php'); 2660 } else if (!empty($CFG->sitepolicyguest) and isguestuser()) { 2661 if ($preventredirect) { 2662 throw new require_login_exception('Policy not agreed'); 2663 } 2664 if ($setwantsurltome) { 2665 $SESSION->wantsurl = qualified_me(); 2666 } 2667 redirect($CFG->wwwroot .'/user/policy.php'); 2668 } 2669 } 2670 2671 // Fetch the system context, the course context, and prefetch its child contexts. 2672 $sysctx = context_system::instance(); 2673 $coursecontext = context_course::instance($course->id, MUST_EXIST); 2674 if ($cm) { 2675 $cmcontext = context_module::instance($cm->id, MUST_EXIST); 2676 } else { 2677 $cmcontext = null; 2678 } 2679 2680 // If the site is currently under maintenance, then print a message. 2681 if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:config', $sysctx)) { 2682 if ($preventredirect) { 2683 throw new require_login_exception('Maintenance in progress'); 2684 } 2685 2686 print_maintenance_message(); 2687 } 2688 2689 // Make sure the course itself is not hidden. 2690 if ($course->id == SITEID) { 2691 // Frontpage can not be hidden. 2692 } else { 2693 if (is_role_switched($course->id)) { 2694 // When switching roles ignore the hidden flag - user had to be in course to do the switch. 2695 } else { 2696 if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) { 2697 // Originally there was also test of parent category visibility, BUT is was very slow in complex queries 2698 // involving "my courses" now it is also possible to simply hide all courses user is not enrolled in :-). 2699 if ($preventredirect) { 2700 throw new require_login_exception('Course is hidden'); 2701 } 2702 $PAGE->set_context(null); 2703 // We need to override the navigation URL as the course won't have been added to the navigation and thus 2704 // the navigation will mess up when trying to find it. 2705 navigation_node::override_active_url(new moodle_url('/')); 2706 notice(get_string('coursehidden'), $CFG->wwwroot .'/'); 2707 } 2708 } 2709 } 2710 2711 // Is the user enrolled? 2712 if ($course->id == SITEID) { 2713 // Everybody is enrolled on the frontpage. 2714 } else { 2715 if (\core\session\manager::is_loggedinas()) { 2716 // Make sure the REAL person can access this course first. 2717 $realuser = \core\session\manager::get_realuser(); 2718 if (!is_enrolled($coursecontext, $realuser->id, '', true) and 2719 !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) { 2720 if ($preventredirect) { 2721 throw new require_login_exception('Invalid course login-as access'); 2722 } 2723 $PAGE->set_context(null); 2724 echo $OUTPUT->header(); 2725 notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/'); 2726 } 2727 } 2728 2729 $access = false; 2730 2731 if (is_role_switched($course->id)) { 2732 // Ok, user had to be inside this course before the switch. 2733 $access = true; 2734 2735 } else if (is_viewing($coursecontext, $USER)) { 2736 // Ok, no need to mess with enrol. 2737 $access = true; 2738 2739 } else { 2740 if (isset($USER->enrol['enrolled'][$course->id])) { 2741 if ($USER->enrol['enrolled'][$course->id] > time()) { 2742 $access = true; 2743 if (isset($USER->enrol['tempguest'][$course->id])) { 2744 unset($USER->enrol['tempguest'][$course->id]); 2745 remove_temp_course_roles($coursecontext); 2746 } 2747 } else { 2748 // Expired. 2749 unset($USER->enrol['enrolled'][$course->id]); 2750 } 2751 } 2752 if (isset($USER->enrol['tempguest'][$course->id])) { 2753 if ($USER->enrol['tempguest'][$course->id] == 0) { 2754 $access = true; 2755 } else if ($USER->enrol['tempguest'][$course->id] > time()) { 2756 $access = true; 2757 } else { 2758 // Expired. 2759 unset($USER->enrol['tempguest'][$course->id]); 2760 remove_temp_course_roles($coursecontext); 2761 } 2762 } 2763 2764 if (!$access) { 2765 // Cache not ok. 2766 $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id); 2767 if ($until !== false) { 2768 // Active participants may always access, a timestamp in the future, 0 (always) or false. 2769 if ($until == 0) { 2770 $until = ENROL_MAX_TIMESTAMP; 2771 } 2772 $USER->enrol['enrolled'][$course->id] = $until; 2773 $access = true; 2774 2775 } else { 2776 $params = array('courseid' => $course->id, 'status' => ENROL_INSTANCE_ENABLED); 2777 $instances = $DB->get_records('enrol', $params, 'sortorder, id ASC'); 2778 $enrols = enrol_get_plugins(true); 2779 // First ask all enabled enrol instances in course if they want to auto enrol user. 2780 foreach ($instances as $instance) { 2781 if (!isset($enrols[$instance->enrol])) { 2782 continue; 2783 } 2784 // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false. 2785 $until = $enrols[$instance->enrol]->try_autoenrol($instance); 2786 if ($until !== false) { 2787 if ($until == 0) { 2788 $until = ENROL_MAX_TIMESTAMP; 2789 } 2790 $USER->enrol['enrolled'][$course->id] = $until; 2791 $access = true; 2792 break; 2793 } 2794 } 2795 // If not enrolled yet try to gain temporary guest access. 2796 if (!$access) { 2797 foreach ($instances as $instance) { 2798 if (!isset($enrols[$instance->enrol])) { 2799 continue; 2800 } 2801 // Get a duration for the guest access, a timestamp in the future or false. 2802 $until = $enrols[$instance->enrol]->try_guestaccess($instance); 2803 if ($until !== false and $until > time()) { 2804 $USER->enrol['tempguest'][$course->id] = $until; 2805 $access = true; 2806 break; 2807 } 2808 } 2809 } 2810 } 2811 } 2812 } 2813 2814 if (!$access) { 2815 if ($preventredirect) { 2816 throw new require_login_exception('Not enrolled'); 2817 } 2818 if ($setwantsurltome) { 2819 $SESSION->wantsurl = qualified_me(); 2820 } 2821 redirect($CFG->wwwroot .'/enrol/index.php?id='. $course->id); 2822 } 2823 } 2824 2825 // Check visibility of activity to current user; includes visible flag, conditional availability, etc. 2826 if ($cm && !$cm->uservisible) { 2827 if ($preventredirect) { 2828 throw new require_login_exception('Activity is hidden'); 2829 } 2830 if ($course->id != SITEID) { 2831 $url = new moodle_url('/course/view.php', array('id' => $course->id)); 2832 } else { 2833 $url = new moodle_url('/'); 2834 } 2835 redirect($url, get_string('activityiscurrentlyhidden')); 2836 } 2837 2838 // Set the global $COURSE. 2839 if ($cm) { 2840 $PAGE->set_cm($cm, $course); 2841 $PAGE->set_pagelayout('incourse'); 2842 } else if (!empty($courseorid)) { 2843 $PAGE->set_course($course); 2844 } 2845 2846 // Finally access granted, update lastaccess times. 2847 user_accesstime_log($course->id); 2848 } 2849 2850 2851 /** 2852 * This function just makes sure a user is logged out. 2853 * 2854 * @package core_access 2855 * @category access 2856 */ 2857 function require_logout() { 2858 global $USER, $DB; 2859 2860 if (!isloggedin()) { 2861 // This should not happen often, no need for hooks or events here. 2862 \core\session\manager::terminate_current(); 2863 return; 2864 } 2865 2866 // Execute hooks before action. 2867 $authplugins = array(); 2868 $authsequence = get_enabled_auth_plugins(); 2869 foreach ($authsequence as $authname) { 2870 $authplugins[$authname] = get_auth_plugin($authname); 2871 $authplugins[$authname]->prelogout_hook(); 2872 } 2873 2874 // Store info that gets removed during logout. 2875 $sid = session_id(); 2876 $event = \core\event\user_loggedout::create( 2877 array( 2878 'userid' => $USER->id, 2879 'objectid' => $USER->id, 2880 'other' => array('sessionid' => $sid), 2881 ) 2882 ); 2883 if ($session = $DB->get_record('sessions', array('sid'=>$sid))) { 2884 $event->add_record_snapshot('sessions', $session); 2885 } 2886 2887 // Clone of $USER object to be used by auth plugins. 2888 $user = fullclone($USER); 2889 2890 // Delete session record and drop $_SESSION content. 2891 \core\session\manager::terminate_current(); 2892 2893 // Trigger event AFTER action. 2894 $event->trigger(); 2895 2896 // Hook to execute auth plugins redirection after event trigger. 2897 foreach ($authplugins as $authplugin) { 2898 $authplugin->postlogout_hook($user); 2899 } 2900 } 2901 2902 /** 2903 * Weaker version of require_login() 2904 * 2905 * This is a weaker version of {@link require_login()} which only requires login 2906 * when called from within a course rather than the site page, unless 2907 * the forcelogin option is turned on. 2908 * @see require_login() 2909 * 2910 * @package core_access 2911 * @category access 2912 * 2913 * @param mixed $courseorid The course object or id in question 2914 * @param bool $autologinguest Allow autologin guests if that is wanted 2915 * @param object $cm Course activity module if known 2916 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to 2917 * true. Used to avoid (=false) some scripts (file.php...) to set that variable, 2918 * in order to keep redirects working properly. MDL-14495 2919 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions 2920 * @return void 2921 * @throws coding_exception 2922 */ 2923 function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) { 2924 global $CFG, $PAGE, $SITE; 2925 $issite = ((is_object($courseorid) and $courseorid->id == SITEID) 2926 or (!is_object($courseorid) and $courseorid == SITEID)); 2927 if ($issite && !empty($cm) && !($cm instanceof cm_info)) { 2928 // Note: nearly all pages call get_fast_modinfo anyway and it does not make any 2929 // db queries so this is not really a performance concern, however it is obviously 2930 // better if you use get_fast_modinfo to get the cm before calling this. 2931 if (is_object($courseorid)) { 2932 $course = $courseorid; 2933 } else { 2934 $course = clone($SITE); 2935 } 2936 $modinfo = get_fast_modinfo($course); 2937 $cm = $modinfo->get_cm($cm->id); 2938 } 2939 if (!empty($CFG->forcelogin)) { 2940 // Login required for both SITE and courses. 2941 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); 2942 2943 } else if ($issite && !empty($cm) and !$cm->uservisible) { 2944 // Always login for hidden activities. 2945 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); 2946 2947 } else if ($issite) { 2948 // Login for SITE not required. 2949 // We still need to instatiate PAGE vars properly so that things that rely on it like navigation function correctly. 2950 if (!empty($courseorid)) { 2951 if (is_object($courseorid)) { 2952 $course = $courseorid; 2953 } else { 2954 $course = clone $SITE; 2955 } 2956 if ($cm) { 2957 if ($cm->course != $course->id) { 2958 throw new coding_exception('course and cm parameters in require_course_login() call do not match!!'); 2959 } 2960 $PAGE->set_cm($cm, $course); 2961 $PAGE->set_pagelayout('incourse'); 2962 } else { 2963 $PAGE->set_course($course); 2964 } 2965 } else { 2966 // If $PAGE->course, and hence $PAGE->context, have not already been set up properly, set them up now. 2967 $PAGE->set_course($PAGE->course); 2968 } 2969 user_accesstime_log(SITEID); 2970 return; 2971 2972 } else { 2973 // Course login always required. 2974 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); 2975 } 2976 } 2977 2978 /** 2979 * Require key login. Function terminates with error if key not found or incorrect. 2980 * 2981 * @uses NO_MOODLE_COOKIES 2982 * @uses PARAM_ALPHANUM 2983 * @param string $script unique script identifier 2984 * @param int $instance optional instance id 2985 * @return int Instance ID 2986 */ 2987 function require_user_key_login($script, $instance=null) { 2988 global $DB; 2989 2990 if (!NO_MOODLE_COOKIES) { 2991 print_error('sessioncookiesdisable'); 2992 } 2993 2994 // Extra safety. 2995 \core\session\manager::write_close(); 2996 2997 $keyvalue = required_param('key', PARAM_ALPHANUM); 2998 2999 if (!$key = $DB->get_record('user_private_key', array('script' => $script, 'value' => $keyvalue, 'instance' => $instance))) { 3000 print_error('invalidkey'); 3001 } 3002 3003 if (!empty($key->validuntil) and $key->validuntil < time()) { 3004 print_error('expiredkey'); 3005 } 3006 3007 if ($key->iprestriction) { 3008 $remoteaddr = getremoteaddr(null); 3009 if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) { 3010 print_error('ipmismatch'); 3011 } 3012 } 3013 3014 if (!$user = $DB->get_record('user', array('id' => $key->userid))) { 3015 print_error('invaliduserid'); 3016 } 3017 3018 // Emulate normal session. 3019 enrol_check_plugins($user); 3020 \core\session\manager::set_user($user); 3021 3022 // Note we are not using normal login. 3023 if (!defined('USER_KEY_LOGIN')) { 3024 define('USER_KEY_LOGIN', true); 3025 } 3026 3027 // Return instance id - it might be empty. 3028 return $key->instance; 3029 } 3030 3031 /** 3032 * Creates a new private user access key. 3033 * 3034 * @param string $script unique target identifier 3035 * @param int $userid 3036 * @param int $instance optional instance id 3037 * @param string $iprestriction optional ip restricted access 3038 * @param int $validuntil key valid only until given data 3039 * @return string access key value 3040 */ 3041 function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) { 3042 global $DB; 3043 3044 $key = new stdClass(); 3045 $key->script = $script; 3046 $key->userid = $userid; 3047 $key->instance = $instance; 3048 $key->iprestriction = $iprestriction; 3049 $key->validuntil = $validuntil; 3050 $key->timecreated = time(); 3051 3052 // Something long and unique. 3053 $key->value = md5($userid.'_'.time().random_string(40)); 3054 while ($DB->record_exists('user_private_key', array('value' => $key->value))) { 3055 // Must be unique. 3056 $key->value = md5($userid.'_'.time().random_string(40)); 3057 } 3058 $DB->insert_record('user_private_key', $key); 3059 return $key->value; 3060 } 3061 3062 /** 3063 * Delete the user's new private user access keys for a particular script. 3064 * 3065 * @param string $script unique target identifier 3066 * @param int $userid 3067 * @return void 3068 */ 3069 function delete_user_key($script, $userid) { 3070 global $DB; 3071 $DB->delete_records('user_private_key', array('script' => $script, 'userid' => $userid)); 3072 } 3073 3074 /** 3075 * Gets a private user access key (and creates one if one doesn't exist). 3076 * 3077 * @param string $script unique target identifier 3078 * @param int $userid 3079 * @param int $instance optional instance id 3080 * @param string $iprestriction optional ip restricted access 3081 * @param int $validuntil key valid only until given date 3082 * @return string access key value 3083 */ 3084 function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) { 3085 global $DB; 3086 3087 if ($key = $DB->get_record('user_private_key', array('script' => $script, 'userid' => $userid, 3088 'instance' => $instance, 'iprestriction' => $iprestriction, 3089 'validuntil' => $validuntil))) { 3090 return $key->value; 3091 } else { 3092 return create_user_key($script, $userid, $instance, $iprestriction, $validuntil); 3093 } 3094 } 3095 3096 3097 /** 3098 * Modify the user table by setting the currently logged in user's last login to now. 3099 * 3100 * @return bool Always returns true 3101 */ 3102 function update_user_login_times() { 3103 global $USER, $DB; 3104 3105 if (isguestuser()) { 3106 // Do not update guest access times/ips for performance. 3107 return true; 3108 } 3109 3110 $now = time(); 3111 3112 $user = new stdClass(); 3113 $user->id = $USER->id; 3114 3115 // Make sure all users that logged in have some firstaccess. 3116 if ($USER->firstaccess == 0) { 3117 $USER->firstaccess = $user->firstaccess = $now; 3118 } 3119 3120 // Store the previous current as lastlogin. 3121 $USER->lastlogin = $user->lastlogin = $USER->currentlogin; 3122 3123 $USER->currentlogin = $user->currentlogin = $now; 3124 3125 // Function user_accesstime_log() may not update immediately, better do it here. 3126 $USER->lastaccess = $user->lastaccess = $now; 3127 $USER->lastip = $user->lastip = getremoteaddr(); 3128 3129 // Note: do not call user_update_user() here because this is part of the login process, 3130 // the login event means that these fields were updated. 3131 $DB->update_record('user', $user); 3132 return true; 3133 } 3134 3135 /** 3136 * Determines if a user has completed setting up their account. 3137 * 3138 * @param stdClass $user A {@link $USER} object to test for the existence of a valid name and email 3139 * @return bool 3140 */ 3141 function user_not_fully_set_up($user) { 3142 if (isguestuser($user)) { 3143 return false; 3144 } 3145 return (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user)); 3146 } 3147 3148 /** 3149 * Check whether the user has exceeded the bounce threshold 3150 * 3151 * @param stdClass $user A {@link $USER} object 3152 * @return bool true => User has exceeded bounce threshold 3153 */ 3154 function over_bounce_threshold($user) { 3155 global $CFG, $DB; 3156 3157 if (empty($CFG->handlebounces)) { 3158 return false; 3159 } 3160 3161 if (empty($user->id)) { 3162 // No real (DB) user, nothing to do here. 3163 return false; 3164 } 3165 3166 // Set sensible defaults. 3167 if (empty($CFG->minbounces)) { 3168 $CFG->minbounces = 10; 3169 } 3170 if (empty($CFG->bounceratio)) { 3171 $CFG->bounceratio = .20; 3172 } 3173 $bouncecount = 0; 3174 $sendcount = 0; 3175 if ($bounce = $DB->get_record('user_preferences', array ('userid' => $user->id, 'name' => 'email_bounce_count'))) { 3176 $bouncecount = $bounce->value; 3177 } 3178 if ($send = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) { 3179 $sendcount = $send->value; 3180 } 3181 return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio); 3182 } 3183 3184 /** 3185 * Used to increment or reset email sent count 3186 * 3187 * @param stdClass $user object containing an id 3188 * @param bool $reset will reset the count to 0 3189 * @return void 3190 */ 3191 function set_send_count($user, $reset=false) { 3192 global $DB; 3193 3194 if (empty($user->id)) { 3195 // No real (DB) user, nothing to do here. 3196 return; 3197 } 3198 3199 if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) { 3200 $pref->value = (!empty($reset)) ? 0 : $pref->value+1; 3201 $DB->update_record('user_preferences', $pref); 3202 } else if (!empty($reset)) { 3203 // If it's not there and we're resetting, don't bother. Make a new one. 3204 $pref = new stdClass(); 3205 $pref->name = 'email_send_count'; 3206 $pref->value = 1; 3207 $pref->userid = $user->id; 3208 $DB->insert_record('user_preferences', $pref, false); 3209 } 3210 } 3211 3212 /** 3213 * Increment or reset user's email bounce count 3214 * 3215 * @param stdClass $user object containing an id 3216 * @param bool $reset will reset the count to 0 3217 */ 3218 function set_bounce_count($user, $reset=false) { 3219 global $DB; 3220 3221 if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_bounce_count'))) { 3222 $pref->value = (!empty($reset)) ? 0 : $pref->value+1; 3223 $DB->update_record('user_preferences', $pref); 3224 } else if (!empty($reset)) { 3225 // If it's not there and we're resetting, don't bother. Make a new one. 3226 $pref = new stdClass(); 3227 $pref->name = 'email_bounce_count'; 3228 $pref->value = 1; 3229 $pref->userid = $user->id; 3230 $DB->insert_record('user_preferences', $pref, false); 3231 } 3232 } 3233 3234 /** 3235 * Determines if the logged in user is currently moving an activity 3236 * 3237 * @param int $courseid The id of the course being tested 3238 * @return bool 3239 */ 3240 function ismoving($courseid) { 3241 global $USER; 3242 3243 if (!empty($USER->activitycopy)) { 3244 return ($USER->activitycopycourse == $courseid); 3245 } 3246 return false; 3247 } 3248 3249 /** 3250 * Returns a persons full name 3251 * 3252 * Given an object containing all of the users name values, this function returns a string with the full name of the person. 3253 * The result may depend on system settings or language. 'override' will force both names to be used even if system settings 3254 * specify one. 3255 * 3256 * @param stdClass $user A {@link $USER} object to get full name of. 3257 * @param bool $override If true then the name will be firstname followed by lastname rather than adhering to fullnamedisplay. 3258 * @return string 3259 */ 3260 function fullname($user, $override=false) { 3261 global $CFG, $SESSION; 3262 3263 if (!isset($user->firstname) and !isset($user->lastname)) { 3264 return ''; 3265 } 3266 3267 // Get all of the name fields. 3268 $allnames = get_all_user_name_fields(); 3269 if ($CFG->debugdeveloper) { 3270 foreach ($allnames as $allname) { 3271 if (!property_exists($user, $allname)) { 3272 // If all the user name fields are not set in the user object, then notify the programmer that it needs to be fixed. 3273 debugging('You need to update your sql to include additional name fields in the user object.', DEBUG_DEVELOPER); 3274 // Message has been sent, no point in sending the message multiple times. 3275 break; 3276 } 3277 } 3278 } 3279 3280 if (!$override) { 3281 if (!empty($CFG->forcefirstname)) { 3282 $user->firstname = $CFG->forcefirstname; 3283 } 3284 if (!empty($CFG->forcelastname)) { 3285 $user->lastname = $CFG->forcelastname; 3286 } 3287 } 3288 3289 if (!empty($SESSION->fullnamedisplay)) { 3290 $CFG->fullnamedisplay = $SESSION->fullnamedisplay; 3291 } 3292 3293 $template = null; 3294 // If the fullnamedisplay setting is available, set the template to that. 3295 if (isset($CFG->fullnamedisplay)) { 3296 $template = $CFG->fullnamedisplay; 3297 } 3298 // If the template is empty, or set to language, return the language string. 3299 if ((empty($template) || $template == 'language') && !$override) { 3300 return get_string('fullnamedisplay', null, $user); 3301 } 3302 3303 // Check to see if we are displaying according to the alternative full name format. 3304 if ($override) { 3305 if (empty($CFG->alternativefullnameformat) || $CFG->alternativefullnameformat == 'language') { 3306 // Default to show just the user names according to the fullnamedisplay string. 3307 return get_string('fullnamedisplay', null, $user); 3308 } else { 3309 // If the override is true, then change the template to use the complete name. 3310 $template = $CFG->alternativefullnameformat; 3311 } 3312 } 3313 3314 $requirednames = array(); 3315 // With each name, see if it is in the display name template, and add it to the required names array if it is. 3316 foreach ($allnames as $allname) { 3317 if (strpos($template, $allname) !== false) { 3318 $requirednames[] = $allname; 3319 } 3320 } 3321 3322 $displayname = $template; 3323 // Switch in the actual data into the template. 3324 foreach ($requirednames as $altname) { 3325 if (isset($user->$altname)) { 3326 // Using empty() on the below if statement causes breakages. 3327 if ((string)$user->$altname == '') { 3328 $displayname = str_replace($altname, 'EMPTY', $displayname); 3329 } else { 3330 $displayname = str_replace($altname, $user->$altname, $displayname); 3331 } 3332 } else { 3333 $displayname = str_replace($altname, 'EMPTY', $displayname); 3334 } 3335 } 3336 // Tidy up any misc. characters (Not perfect, but gets most characters). 3337 // Don't remove the "u" at the end of the first expression unless you want garbled characters when combining hiragana or 3338 // katakana and parenthesis. 3339 $patterns = array(); 3340 // This regular expression replacement is to fix problems such as 'James () Kirk' Where 'Tiberius' (middlename) has not been 3341 // filled in by a user. 3342 // The special characters are Japanese brackets that are common enough to make allowances for them (not covered by :punct:). 3343 $patterns[] = '/[[:punct:]「」]*EMPTY[[:punct:]「」]*/u'; 3344 // This regular expression is to remove any double spaces in the display name. 3345 $patterns[] = '/\s{2,}/u'; 3346 foreach ($patterns as $pattern) { 3347 $displayname = preg_replace($pattern, ' ', $displayname); 3348 } 3349 3350 // Trimming $displayname will help the next check to ensure that we don't have a display name with spaces. 3351 $displayname = trim($displayname); 3352 if (empty($displayname)) { 3353 // Going with just the first name if no alternate fields are filled out. May be changed later depending on what 3354 // people in general feel is a good setting to fall back on. 3355 $displayname = $user->firstname; 3356 } 3357 return $displayname; 3358 } 3359 3360 /** 3361 * A centralised location for the all name fields. Returns an array / sql string snippet. 3362 * 3363 * @param bool $returnsql True for an sql select field snippet. 3364 * @param string $tableprefix table query prefix to use in front of each field. 3365 * @param string $prefix prefix added to the name fields e.g. authorfirstname. 3366 * @param string $fieldprefix sql field prefix e.g. id AS userid. 3367 * @param bool $order moves firstname and lastname to the top of the array / start of the string. 3368 * @return array|string All name fields. 3369 */ 3370 function get_all_user_name_fields($returnsql = false, $tableprefix = null, $prefix = null, $fieldprefix = null, $order = false) { 3371 // This array is provided in this order because when called by fullname() (above) if firstname is before 3372 // firstnamephonetic str_replace() will change the wrong placeholder. 3373 $alternatenames = array('firstnamephonetic' => 'firstnamephonetic', 3374 'lastnamephonetic' => 'lastnamephonetic', 3375 'middlename' => 'middlename', 3376 'alternatename' => 'alternatename', 3377 'firstname' => 'firstname', 3378 'lastname' => 'lastname'); 3379 3380 // Let's add a prefix to the array of user name fields if provided. 3381 if ($prefix) { 3382 foreach ($alternatenames as $key => $altname) { 3383 $alternatenames[$key] = $prefix . $altname; 3384 } 3385 } 3386 3387 // If we want the end result to have firstname and lastname at the front / top of the result. 3388 if ($order) { 3389 // Move the last two elements (firstname, lastname) off the array and put them at the top. 3390 for ($i = 0; $i < 2; $i++) { 3391 // Get the last element. 3392 $lastelement = end($alternatenames); 3393 // Remove it from the array. 3394 unset($alternatenames[$lastelement]); 3395 // Put the element back on the top of the array. 3396 $alternatenames = array_merge(array($lastelement => $lastelement), $alternatenames); 3397 } 3398 } 3399 3400 // Create an sql field snippet if requested. 3401 if ($returnsql) { 3402 if ($tableprefix) { 3403 if ($fieldprefix) { 3404 foreach ($alternatenames as $key => $altname) { 3405 $alternatenames[$key] = $tableprefix . '.' . $altname . ' AS ' . $fieldprefix . $altname; 3406 } 3407 } else { 3408 foreach ($alternatenames as $key => $altname) { 3409 $alternatenames[$key] = $tableprefix . '.' . $altname; 3410 } 3411 } 3412 } 3413 $alternatenames = implode(',', $alternatenames); 3414 } 3415 return $alternatenames; 3416 } 3417 3418 /** 3419 * Reduces lines of duplicated code for getting user name fields. 3420 * 3421 * See also {@link user_picture::unalias()} 3422 * 3423 * @param object $addtoobject Object to add user name fields to. 3424 * @param object $secondobject Object that contains user name field information. 3425 * @param string $prefix prefix to be added to all fields (including $additionalfields) e.g. authorfirstname. 3426 * @param array $additionalfields Additional fields to be matched with data in the second object. 3427 * The key can be set to the user table field name. 3428 * @return object User name fields. 3429 */ 3430 function username_load_fields_from_object($addtoobject, $secondobject, $prefix = null, $additionalfields = null) { 3431 $fields = get_all_user_name_fields(false, null, $prefix); 3432 if ($additionalfields) { 3433 // Additional fields can specify their own 'alias' such as 'id' => 'userid'. This checks to see if 3434 // the key is a number and then sets the key to the array value. 3435 foreach ($additionalfields as $key => $value) { 3436 if (is_numeric($key)) { 3437 $additionalfields[$value] = $prefix . $value; 3438 unset($additionalfields[$key]); 3439 } else { 3440 $additionalfields[$key] = $prefix . $value; 3441 } 3442 } 3443 $fields = array_merge($fields, $additionalfields); 3444 } 3445 foreach ($fields as $key => $field) { 3446 // Important that we have all of the user name fields present in the object that we are sending back. 3447 $addtoobject->$key = ''; 3448 if (isset($secondobject->$field)) { 3449 $addtoobject->$key = $secondobject->$field; 3450 } 3451 } 3452 return $addtoobject; 3453 } 3454 3455 /** 3456 * Returns an array of values in order of occurance in a provided string. 3457 * The key in the result is the character postion in the string. 3458 * 3459 * @param array $values Values to be found in the string format 3460 * @param string $stringformat The string which may contain values being searched for. 3461 * @return array An array of values in order according to placement in the string format. 3462 */ 3463 function order_in_string($values, $stringformat) { 3464 $valuearray = array(); 3465 foreach ($values as $value) { 3466 $pattern = "/$value\b/"; 3467 // Using preg_match as strpos() may match values that are similar e.g. firstname and firstnamephonetic. 3468 if (preg_match($pattern, $stringformat)) { 3469 $replacement = "thing"; 3470 // Replace the value with something more unique to ensure we get the right position when using strpos(). 3471 $newformat = preg_replace($pattern, $replacement, $stringformat); 3472 $position = strpos($newformat, $replacement); 3473 $valuearray[$position] = $value; 3474 } 3475 } 3476 ksort($valuearray); 3477 return $valuearray; 3478 } 3479 3480 /** 3481 * Checks if current user is shown any extra fields when listing users. 3482 * 3483 * @param object $context Context 3484 * @param array $already Array of fields that we're going to show anyway 3485 * so don't bother listing them 3486 * @return array Array of field names from user table, not including anything 3487 * listed in $already 3488 */ 3489 function get_extra_user_fields($context, $already = array()) { 3490 global $CFG; 3491 3492 // Only users with permission get the extra fields. 3493 if (!has_capability('moodle/site:viewuseridentity', $context)) { 3494 return array(); 3495 } 3496 3497 // Split showuseridentity on comma. 3498 if (empty($CFG->showuseridentity)) { 3499 // Explode gives wrong result with empty string. 3500 $extra = array(); 3501 } else { 3502 $extra = explode(',', $CFG->showuseridentity); 3503 } 3504 $renumber = false; 3505 foreach ($extra as $key => $field) { 3506 if (in_array($field, $already)) { 3507 unset($extra[$key]); 3508 $renumber = true; 3509 } 3510 } 3511 if ($renumber) { 3512 // For consistency, if entries are removed from array, renumber it 3513 // so they are numbered as you would expect. 3514 $extra = array_merge($extra); 3515 } 3516 return $extra; 3517 } 3518 3519 /** 3520 * If the current user is to be shown extra user fields when listing or 3521 * selecting users, returns a string suitable for including in an SQL select 3522 * clause to retrieve those fields. 3523 * 3524 * @param context $context Context 3525 * @param string $alias Alias of user table, e.g. 'u' (default none) 3526 * @param string $prefix Prefix for field names using AS, e.g. 'u_' (default none) 3527 * @param array $already Array of fields that we're going to include anyway so don't list them (default none) 3528 * @return string Partial SQL select clause, beginning with comma, for example ',u.idnumber,u.department' unless it is blank 3529 */ 3530 function get_extra_user_fields_sql($context, $alias='', $prefix='', $already = array()) { 3531 $fields = get_extra_user_fields($context, $already); 3532 $result = ''; 3533 // Add punctuation for alias. 3534 if ($alias !== '') { 3535 $alias .= '.'; 3536 } 3537 foreach ($fields as $field) { 3538 $result .= ', ' . $alias . $field; 3539 if ($prefix) { 3540 $result .= ' AS ' . $prefix . $field; 3541 } 3542 } 3543 return $result; 3544 } 3545 3546 /** 3547 * Returns the display name of a field in the user table. Works for most fields that are commonly displayed to users. 3548 * @param string $field Field name, e.g. 'phone1' 3549 * @return string Text description taken from language file, e.g. 'Phone number' 3550 */ 3551 function get_user_field_name($field) { 3552 // Some fields have language strings which are not the same as field name. 3553 switch ($field) { 3554 case 'url' : { 3555 return get_string('webpage'); 3556 } 3557 case 'icq' : { 3558 return get_string('icqnumber'); 3559 } 3560 case 'skype' : { 3561 return get_string('skypeid'); 3562 } 3563 case 'aim' : { 3564 return get_string('aimid'); 3565 } 3566 case 'yahoo' : { 3567 return get_string('yahooid'); 3568 } 3569 case 'msn' : { 3570 return get_string('msnid'); 3571 } 3572 } 3573 // Otherwise just use the same lang string. 3574 return get_string($field); 3575 } 3576 3577 /** 3578 * Returns whether a given authentication plugin exists. 3579 * 3580 * @param string $auth Form of authentication to check for. Defaults to the global setting in {@link $CFG}. 3581 * @return boolean Whether the plugin is available. 3582 */ 3583 function exists_auth_plugin($auth) { 3584 global $CFG; 3585 3586 if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) { 3587 return is_readable("{$CFG->dirroot}/auth/$auth/auth.php"); 3588 } 3589 return false; 3590 } 3591 3592 /** 3593 * Checks if a given plugin is in the list of enabled authentication plugins. 3594 * 3595 * @param string $auth Authentication plugin. 3596 * @return boolean Whether the plugin is enabled. 3597 */ 3598 function is_enabled_auth($auth) { 3599 if (empty($auth)) { 3600 return false; 3601 } 3602 3603 $enabled = get_enabled_auth_plugins(); 3604 3605 return in_array($auth, $enabled); 3606 } 3607 3608 /** 3609 * Returns an authentication plugin instance. 3610 * 3611 * @param string $auth name of authentication plugin 3612 * @return auth_plugin_base An instance of the required authentication plugin. 3613 */ 3614 function get_auth_plugin($auth) { 3615 global $CFG; 3616 3617 // Check the plugin exists first. 3618 if (! exists_auth_plugin($auth)) { 3619 print_error('authpluginnotfound', 'debug', '', $auth); 3620 } 3621 3622 // Return auth plugin instance. 3623 require_once("{$CFG->dirroot}/auth/$auth/auth.php"); 3624 $class = "auth_plugin_$auth"; 3625 return new $class; 3626 } 3627 3628 /** 3629 * Returns array of active auth plugins. 3630 * 3631 * @param bool $fix fix $CFG->auth if needed 3632 * @return array 3633 */ 3634 function get_enabled_auth_plugins($fix=false) { 3635 global $CFG; 3636 3637 $default = array('manual', 'nologin'); 3638 3639 if (empty($CFG->auth)) { 3640 $auths = array(); 3641 } else { 3642 $auths = explode(',', $CFG->auth); 3643 } 3644 3645 if ($fix) { 3646 $auths = array_unique($auths); 3647 foreach ($auths as $k => $authname) { 3648 if (!exists_auth_plugin($authname) or in_array($authname, $default)) { 3649 unset($auths[$k]); 3650 } 3651 } 3652 $newconfig = implode(',', $auths); 3653 if (!isset($CFG->auth) or $newconfig != $CFG->auth) { 3654 set_config('auth', $newconfig); 3655 } 3656 } 3657 3658 return (array_merge($default, $auths)); 3659 } 3660 3661 /** 3662 * Returns true if an internal authentication method is being used. 3663 * if method not specified then, global default is assumed 3664 * 3665 * @param string $auth Form of authentication required 3666 * @return bool 3667 */ 3668 function is_internal_auth($auth) { 3669 // Throws error if bad $auth. 3670 $authplugin = get_auth_plugin($auth); 3671 return $authplugin->is_internal(); 3672 } 3673 3674 /** 3675 * Returns true if the user is a 'restored' one. 3676 * 3677 * Used in the login process to inform the user and allow him/her to reset the password 3678 * 3679 * @param string $username username to be checked 3680 * @return bool 3681 */ 3682 function is_restored_user($username) { 3683 global $CFG, $DB; 3684 3685 return $DB->record_exists('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'password' => 'restored')); 3686 } 3687 3688 /** 3689 * Returns an array of user fields 3690 * 3691 * @return array User field/column names 3692 */ 3693 function get_user_fieldnames() { 3694 global $DB; 3695 3696 $fieldarray = $DB->get_columns('user'); 3697 unset($fieldarray['id']); 3698 $fieldarray = array_keys($fieldarray); 3699 3700 return $fieldarray; 3701 } 3702 3703 /** 3704 * Creates a bare-bones user record 3705 * 3706 * @todo Outline auth types and provide code example 3707 * 3708 * @param string $username New user's username to add to record 3709 * @param string $password New user's password to add to record 3710 * @param string $auth Form of authentication required 3711 * @return stdClass A complete user object 3712 */ 3713 function create_user_record($username, $password, $auth = 'manual') { 3714 global $CFG, $DB; 3715 require_once($CFG->dirroot.'/user/profile/lib.php'); 3716 require_once($CFG->dirroot.'/user/lib.php'); 3717 3718 // Just in case check text case. 3719 $username = trim(core_text::strtolower($username)); 3720 3721 $authplugin = get_auth_plugin($auth); 3722 $customfields = $authplugin->get_custom_user_profile_fields(); 3723 $newuser = new stdClass(); 3724 if ($newinfo = $authplugin->get_userinfo($username)) { 3725 $newinfo = truncate_userinfo($newinfo); 3726 foreach ($newinfo as $key => $value) { 3727 if (in_array($key, $authplugin->userfields) || (in_array($key, $customfields))) { 3728 $newuser->$key = $value; 3729 } 3730 } 3731 } 3732 3733 if (!empty($newuser->email)) { 3734 if (email_is_not_allowed($newuser->email)) { 3735 unset($newuser->email); 3736 } 3737 } 3738 3739 if (!isset($newuser->city)) { 3740 $newuser->city = ''; 3741 } 3742 3743 $newuser->auth = $auth; 3744 $newuser->username = $username; 3745 3746 // Fix for MDL-8480 3747 // user CFG lang for user if $newuser->lang is empty 3748 // or $user->lang is not an installed language. 3749 if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) { 3750 $newuser->lang = $CFG->lang; 3751 } 3752 $newuser->confirmed = 1; 3753 $newuser->lastip = getremoteaddr(); 3754 $newuser->timecreated = time(); 3755 $newuser->timemodified = $newuser->timecreated; 3756 $newuser->mnethostid = $CFG->mnet_localhost_id; 3757 3758 $newuser->id = user_create_user($newuser, false, false); 3759 3760 // Save user profile data. 3761 profile_save_data($newuser); 3762 3763 $user = get_complete_user_data('id', $newuser->id); 3764 if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})) { 3765 set_user_preference('auth_forcepasswordchange', 1, $user); 3766 } 3767 // Set the password. 3768 update_internal_user_password($user, $password); 3769 3770 // Trigger event. 3771 \core\event\user_created::create_from_userid($newuser->id)->trigger(); 3772 3773 return $user; 3774 } 3775 3776 /** 3777 * Will update a local user record from an external source (MNET users can not be updated using this method!). 3778 * 3779 * @param string $username user's username to update the record 3780 * @return stdClass A complete user object 3781 */ 3782 function update_user_record($username) { 3783 global $DB, $CFG; 3784 // Just in case check text case. 3785 $username = trim(core_text::strtolower($username)); 3786 3787 $oldinfo = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id), '*', MUST_EXIST); 3788 return update_user_record_by_id($oldinfo->id); 3789 } 3790 3791 /** 3792 * Will update a local user record from an external source (MNET users can not be updated using this method!). 3793 * 3794 * @param int $id user id 3795 * @return stdClass A complete user object 3796 */ 3797 function update_user_record_by_id($id) { 3798 global $DB, $CFG; 3799 require_once($CFG->dirroot."/user/profile/lib.php"); 3800 require_once($CFG->dirroot.'/user/lib.php'); 3801 3802 $params = array('mnethostid' => $CFG->mnet_localhost_id, 'id' => $id, 'deleted' => 0); 3803 $oldinfo = $DB->get_record('user', $params, '*', MUST_EXIST); 3804 3805 $newuser = array(); 3806 $userauth = get_auth_plugin($oldinfo->auth); 3807 3808 if ($newinfo = $userauth->get_userinfo($oldinfo->username)) { 3809 $newinfo = truncate_userinfo($newinfo); 3810 $customfields = $userauth->get_custom_user_profile_fields(); 3811 3812 foreach ($newinfo as $key => $value) { 3813 $iscustom = in_array($key, $customfields); 3814 if (!$iscustom) { 3815 $key = strtolower($key); 3816 } 3817 if ((!property_exists($oldinfo, $key) && !$iscustom) or $key === 'username' or $key === 'id' 3818 or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') { 3819 // Unknown or must not be changed. 3820 continue; 3821 } 3822 $confval = $userauth->config->{'field_updatelocal_' . $key}; 3823 $lockval = $userauth->config->{'field_lock_' . $key}; 3824 if (empty($confval) || empty($lockval)) { 3825 continue; 3826 } 3827 if ($confval === 'onlogin') { 3828 // MDL-4207 Don't overwrite modified user profile values with 3829 // empty LDAP values when 'unlocked if empty' is set. The purpose 3830 // of the setting 'unlocked if empty' is to allow the user to fill 3831 // in a value for the selected field _if LDAP is giving 3832 // nothing_ for this field. Thus it makes sense to let this value 3833 // stand in until LDAP is giving a value for this field. 3834 if (!(empty($value) && $lockval === 'unlockedifempty')) { 3835 if ($iscustom || (in_array($key, $userauth->userfields) && 3836 ((string)$oldinfo->$key !== (string)$value))) { 3837 $newuser[$key] = (string)$value; 3838 } 3839 } 3840 } 3841 } 3842 if ($newuser) { 3843 $newuser['id'] = $oldinfo->id; 3844 $newuser['timemodified'] = time(); 3845 user_update_user((object) $newuser, false, false); 3846 3847 // Save user profile data. 3848 profile_save_data((object) $newuser); 3849 3850 // Trigger event. 3851 \core\event\user_updated::create_from_userid($newuser['id'])->trigger(); 3852 } 3853 } 3854 3855 return get_complete_user_data('id', $oldinfo->id); 3856 } 3857 3858 /** 3859 * Will truncate userinfo as it comes from auth_get_userinfo (from external auth) which may have large fields. 3860 * 3861 * @param array $info Array of user properties to truncate if needed 3862 * @return array The now truncated information that was passed in 3863 */ 3864 function truncate_userinfo(array $info) { 3865 // Define the limits. 3866 $limit = array( 3867 'username' => 100, 3868 'idnumber' => 255, 3869 'firstname' => 100, 3870 'lastname' => 100, 3871 'email' => 100, 3872 'icq' => 15, 3873 'phone1' => 20, 3874 'phone2' => 20, 3875 'institution' => 255, 3876 'department' => 255, 3877 'address' => 255, 3878 'city' => 120, 3879 'country' => 2, 3880 'url' => 255, 3881 ); 3882 3883 // Apply where needed. 3884 foreach (array_keys($info) as $key) { 3885 if (!empty($limit[$key])) { 3886 $info[$key] = trim(core_text::substr($info[$key], 0, $limit[$key])); 3887 } 3888 } 3889 3890 return $info; 3891 } 3892 3893 /** 3894 * Marks user deleted in internal user database and notifies the auth plugin. 3895 * Also unenrols user from all roles and does other cleanup. 3896 * 3897 * Any plugin that needs to purge user data should register the 'user_deleted' event. 3898 * 3899 * @param stdClass $user full user object before delete 3900 * @return boolean success 3901 * @throws coding_exception if invalid $user parameter detected 3902 */ 3903 function delete_user(stdClass $user) { 3904 global $CFG, $DB; 3905 require_once($CFG->libdir.'/grouplib.php'); 3906 require_once($CFG->libdir.'/gradelib.php'); 3907 require_once($CFG->dirroot.'/message/lib.php'); 3908 require_once($CFG->dirroot.'/user/lib.php'); 3909 3910 // Make sure nobody sends bogus record type as parameter. 3911 if (!property_exists($user, 'id') or !property_exists($user, 'username')) { 3912 throw new coding_exception('Invalid $user parameter in delete_user() detected'); 3913 } 3914 3915 // Better not trust the parameter and fetch the latest info this will be very expensive anyway. 3916 if (!$user = $DB->get_record('user', array('id' => $user->id))) { 3917 debugging('Attempt to delete unknown user account.'); 3918 return false; 3919 } 3920 3921 // There must be always exactly one guest record, originally the guest account was identified by username only, 3922 // now we use $CFG->siteguest for performance reasons. 3923 if ($user->username === 'guest' or isguestuser($user)) { 3924 debugging('Guest user account can not be deleted.'); 3925 return false; 3926 } 3927 3928 // Admin can be theoretically from different auth plugin, but we want to prevent deletion of internal accoutns only, 3929 // if anything goes wrong ppl may force somebody to be admin via config.php setting $CFG->siteadmins. 3930 if ($user->auth === 'manual' and is_siteadmin($user)) { 3931 debugging('Local administrator accounts can not be deleted.'); 3932 return false; 3933 } 3934 3935 // Allow plugins to use this user object before we completely delete it. 3936 if ($pluginsfunction = get_plugins_with_function('pre_user_delete')) { 3937 foreach ($pluginsfunction as $plugintype => $plugins) { 3938 foreach ($plugins as $pluginfunction) { 3939 $pluginfunction($user); 3940 } 3941 } 3942 } 3943 3944 // Keep user record before updating it, as we have to pass this to user_deleted event. 3945 $olduser = clone $user; 3946 3947 // Keep a copy of user context, we need it for event. 3948 $usercontext = context_user::instance($user->id); 3949 3950 // Delete all grades - backup is kept in grade_grades_history table. 3951 grade_user_delete($user->id); 3952 3953 // Move unread messages from this user to read. 3954 message_move_userfrom_unread2read($user->id); 3955 3956 // TODO: remove from cohorts using standard API here. 3957 3958 // Remove user tags. 3959 core_tag_tag::remove_all_item_tags('core', 'user', $user->id); 3960 3961 // Unconditionally unenrol from all courses. 3962 enrol_user_delete($user); 3963 3964 // Unenrol from all roles in all contexts. 3965 // This might be slow but it is really needed - modules might do some extra cleanup! 3966 role_unassign_all(array('userid' => $user->id)); 3967 3968 // Now do a brute force cleanup. 3969 3970 // Remove from all cohorts. 3971 $DB->delete_records('cohort_members', array('userid' => $user->id)); 3972 3973 // Remove from all groups. 3974 $DB->delete_records('groups_members', array('userid' => $user->id)); 3975 3976 // Brute force unenrol from all courses. 3977 $DB->delete_records('user_enrolments', array('userid' => $user->id)); 3978 3979 // Purge user preferences. 3980 $DB->delete_records('user_preferences', array('userid' => $user->id)); 3981 3982 // Purge user extra profile info. 3983 $DB->delete_records('user_info_data', array('userid' => $user->id)); 3984 3985 // Purge log of previous password hashes. 3986 $DB->delete_records('user_password_history', array('userid' => $user->id)); 3987 3988 // Last course access not necessary either. 3989 $DB->delete_records('user_lastaccess', array('userid' => $user->id)); 3990 // Remove all user tokens. 3991 $DB->delete_records('external_tokens', array('userid' => $user->id)); 3992 3993 // Unauthorise the user for all services. 3994 $DB->delete_records('external_services_users', array('userid' => $user->id)); 3995 3996 // Remove users private keys. 3997 $DB->delete_records('user_private_key', array('userid' => $user->id)); 3998 3999 // Remove users customised pages. 4000 $DB->delete_records('my_pages', array('userid' => $user->id, 'private' => 1)); 4001 4002 // Force logout - may fail if file based sessions used, sorry. 4003 \core\session\manager::kill_user_sessions($user->id); 4004 4005 // Generate username from email address, or a fake email. 4006 $delemail = !empty($user->email) ? $user->email : $user->username . '.' . $user->id . '@unknownemail.invalid'; 4007 $delname = clean_param($delemail . "." . time(), PARAM_USERNAME); 4008 4009 // Workaround for bulk deletes of users with the same email address. 4010 while ($DB->record_exists('user', array('username' => $delname))) { // No need to use mnethostid here. 4011 $delname++; 4012 } 4013 4014 // Mark internal user record as "deleted". 4015 $updateuser = new stdClass(); 4016 $updateuser->id = $user->id; 4017 $updateuser->deleted = 1; 4018 $updateuser->username = $delname; // Remember it just in case. 4019 $updateuser->email = md5($user->username);// Store hash of username, useful importing/restoring users. 4020 $updateuser->idnumber = ''; // Clear this field to free it up. 4021 $updateuser->picture = 0; 4022 $updateuser->timemodified = time(); 4023 4024 // Don't trigger update event, as user is being deleted. 4025 user_update_user($updateuser, false, false); 4026 4027 // Now do a final accesslib cleanup - removes all role assignments in user context and context itself. 4028 context_helper::delete_instance(CONTEXT_USER, $user->id); 4029 4030 // Any plugin that needs to cleanup should register this event. 4031 // Trigger event. 4032 $event = \core\event\user_deleted::create( 4033 array( 4034 'objectid' => $user->id, 4035 'relateduserid' => $user->id, 4036 'context' => $usercontext, 4037 'other' => array( 4038 'username' => $user->username, 4039 'email' => $user->email, 4040 'idnumber' => $user->idnumber, 4041 'picture' => $user->picture, 4042 'mnethostid' => $user->mnethostid 4043 ) 4044 ) 4045 ); 4046 $event->add_record_snapshot('user', $olduser); 4047 $event->trigger(); 4048 4049 // We will update the user's timemodified, as it will be passed to the user_deleted event, which 4050 // should know about this updated property persisted to the user's table. 4051 $user->timemodified = $updateuser->timemodified; 4052 4053 // Notify auth plugin - do not block the delete even when plugin fails. 4054 $authplugin = get_auth_plugin($user->auth); 4055 $authplugin->user_delete($user); 4056 4057 return true; 4058 } 4059 4060 /** 4061 * Retrieve the guest user object. 4062 * 4063 * @return stdClass A {@link $USER} object 4064 */ 4065 function guest_user() { 4066 global $CFG, $DB; 4067 4068 if ($newuser = $DB->get_record('user', array('id' => $CFG->siteguest))) { 4069 $newuser->confirmed = 1; 4070 $newuser->lang = $CFG->lang; 4071 $newuser->lastip = getremoteaddr(); 4072 } 4073 4074 return $newuser; 4075 } 4076 4077 /** 4078 * Authenticates a user against the chosen authentication mechanism 4079 * 4080 * Given a username and password, this function looks them 4081 * up using the currently selected authentication mechanism, 4082 * and if the authentication is successful, it returns a 4083 * valid $user object from the 'user' table. 4084 * 4085 * Uses auth_ functions from the currently active auth module 4086 * 4087 * After authenticate_user_login() returns success, you will need to 4088 * log that the user has logged in, and call complete_user_login() to set 4089 * the session up. 4090 * 4091 * Note: this function works only with non-mnet accounts! 4092 * 4093 * @param string $username User's username (or also email if $CFG->authloginviaemail enabled) 4094 * @param string $password User's password 4095 * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO 4096 * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists) 4097 * @return stdClass|false A {@link $USER} object or false if error 4098 */ 4099 function authenticate_user_login($username, $password, $ignorelockout=false, &$failurereason=null) { 4100 global $CFG, $DB; 4101 require_once("$CFG->libdir/authlib.php"); 4102 4103 if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) { 4104 // we have found the user 4105 4106 } else if (!empty($CFG->authloginviaemail)) { 4107 if ($email = clean_param($username, PARAM_EMAIL)) { 4108 $select = "mnethostid = :mnethostid AND LOWER(email) = LOWER(:email) AND deleted = 0"; 4109 $params = array('mnethostid' => $CFG->mnet_localhost_id, 'email' => $email); 4110 $users = $DB->get_records_select('user', $select, $params, 'id', 'id', 0, 2); 4111 if (count($users) === 1) { 4112 // Use email for login only if unique. 4113 $user = reset($users); 4114 $user = get_complete_user_data('id', $user->id); 4115 $username = $user->username; 4116 } 4117 unset($users); 4118 } 4119 } 4120 4121 $authsenabled = get_enabled_auth_plugins(); 4122 4123 if ($user) { 4124 // Use manual if auth not set. 4125 $auth = empty($user->auth) ? 'manual' : $user->auth; 4126 4127 if (in_array($user->auth, $authsenabled)) { 4128 $authplugin = get_auth_plugin($user->auth); 4129 $authplugin->pre_user_login_hook($user); 4130 } 4131 4132 if (!empty($user->suspended)) { 4133 $failurereason = AUTH_LOGIN_SUSPENDED; 4134 4135 // Trigger login failed event. 4136 $event = \core\event\user_login_failed::create(array('userid' => $user->id, 4137 'other' => array('username' => $username, 'reason' => $failurereason))); 4138 $event->trigger(); 4139 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']); 4140 return false; 4141 } 4142 if ($auth=='nologin' or !is_enabled_auth($auth)) { 4143 // Legacy way to suspend user. 4144 $failurereason = AUTH_LOGIN_SUSPENDED; 4145 4146 // Trigger login failed event. 4147 $event = \core\event\user_login_failed::create(array('userid' => $user->id, 4148 'other' => array('username' => $username, 'reason' => $failurereason))); 4149 $event->trigger(); 4150 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Disabled Login: $username ".$_SERVER['HTTP_USER_AGENT']); 4151 return false; 4152 } 4153 $auths = array($auth); 4154 4155 } else { 4156 // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user(). 4157 if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'deleted' => 1))) { 4158 $failurereason = AUTH_LOGIN_NOUSER; 4159 4160 // Trigger login failed event. 4161 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, 4162 'reason' => $failurereason))); 4163 $event->trigger(); 4164 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Deleted Login: $username ".$_SERVER['HTTP_USER_AGENT']); 4165 return false; 4166 } 4167 4168 // User does not exist. 4169 $auths = $authsenabled; 4170 $user = new stdClass(); 4171 $user->id = 0; 4172 } 4173 4174 if ($ignorelockout) { 4175 // Some other mechanism protects against brute force password guessing, for example login form might include reCAPTCHA 4176 // or this function is called from a SSO script. 4177 } else if ($user->id) { 4178 // Verify login lockout after other ways that may prevent user login. 4179 if (login_is_lockedout($user)) { 4180 $failurereason = AUTH_LOGIN_LOCKOUT; 4181 4182 // Trigger login failed event. 4183 $event = \core\event\user_login_failed::create(array('userid' => $user->id, 4184 'other' => array('username' => $username, 'reason' => $failurereason))); 4185 $event->trigger(); 4186 4187 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Login lockout: $username ".$_SERVER['HTTP_USER_AGENT']); 4188 return false; 4189 } 4190 } else { 4191 // We can not lockout non-existing accounts. 4192 } 4193 4194 foreach ($auths as $auth) { 4195 $authplugin = get_auth_plugin($auth); 4196 4197 // On auth fail fall through to the next plugin. 4198 if (!$authplugin->user_login($username, $password)) { 4199 continue; 4200 } 4201 4202 // Successful authentication. 4203 if ($user->id) { 4204 // User already exists in database. 4205 if (empty($user->auth)) { 4206 // For some reason auth isn't set yet. 4207 $DB->set_field('user', 'auth', $auth, array('id' => $user->id)); 4208 $user->auth = $auth; 4209 } 4210 4211 // If the existing hash is using an out-of-date algorithm (or the legacy md5 algorithm), then we should update to 4212 // the current hash algorithm while we have access to the user's password. 4213 update_internal_user_password($user, $password); 4214 4215 if ($authplugin->is_synchronised_with_external()) { 4216 // Update user record from external DB. 4217 $user = update_user_record_by_id($user->id); 4218 } 4219 } else { 4220 // The user is authenticated but user creation may be disabled. 4221 if (!empty($CFG->authpreventaccountcreation)) { 4222 $failurereason = AUTH_LOGIN_UNAUTHORISED; 4223 4224 // Trigger login failed event. 4225 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, 4226 'reason' => $failurereason))); 4227 $event->trigger(); 4228 4229 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Unknown user, can not create new accounts: $username ". 4230 $_SERVER['HTTP_USER_AGENT']); 4231 return false; 4232 } else { 4233 $user = create_user_record($username, $password, $auth); 4234 } 4235 } 4236 4237 $authplugin->sync_roles($user); 4238 4239 foreach ($authsenabled as $hau) { 4240 $hauth = get_auth_plugin($hau); 4241 $hauth->user_authenticated_hook($user, $username, $password); 4242 } 4243 4244 if (empty($user->id)) { 4245 $failurereason = AUTH_LOGIN_NOUSER; 4246 // Trigger login failed event. 4247 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, 4248 'reason' => $failurereason))); 4249 $event->trigger(); 4250 return false; 4251 } 4252 4253 if (!empty($user->suspended)) { 4254 // Just in case some auth plugin suspended account. 4255 $failurereason = AUTH_LOGIN_SUSPENDED; 4256 // Trigger login failed event. 4257 $event = \core\event\user_login_failed::create(array('userid' => $user->id, 4258 'other' => array('username' => $username, 'reason' => $failurereason))); 4259 $event->trigger(); 4260 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']); 4261 return false; 4262 } 4263 4264 login_attempt_valid($user); 4265 $failurereason = AUTH_LOGIN_OK; 4266 return $user; 4267 } 4268 4269 // Failed if all the plugins have failed. 4270 if (debugging('', DEBUG_ALL)) { 4271 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Failed Login: $username ".$_SERVER['HTTP_USER_AGENT']); 4272 } 4273 4274 if ($user->id) { 4275 login_attempt_failed($user); 4276 $failurereason = AUTH_LOGIN_FAILED; 4277 // Trigger login failed event. 4278 $event = \core\event\user_login_failed::create(array('userid' => $user->id, 4279 'other' => array('username' => $username, 'reason' => $failurereason))); 4280 $event->trigger(); 4281 } else { 4282 $failurereason = AUTH_LOGIN_NOUSER; 4283 // Trigger login failed event. 4284 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, 4285 'reason' => $failurereason))); 4286 $event->trigger(); 4287 } 4288 4289 return false; 4290 } 4291 4292 /** 4293 * Call to complete the user login process after authenticate_user_login() 4294 * has succeeded. It will setup the $USER variable and other required bits 4295 * and pieces. 4296 * 4297 * NOTE: 4298 * - It will NOT log anything -- up to the caller to decide what to log. 4299 * - this function does not set any cookies any more! 4300 * 4301 * @param stdClass $user 4302 * @return stdClass A {@link $USER} object - BC only, do not use 4303 */ 4304 function complete_user_login($user) { 4305 global $CFG, $USER, $SESSION; 4306 4307 \core\session\manager::login_user($user); 4308 4309 // Reload preferences from DB. 4310 unset($USER->preference); 4311 check_user_preferences_loaded($USER); 4312 4313 // Update login times. 4314 update_user_login_times(); 4315 4316 // Extra session prefs init. 4317 set_login_session_preferences(); 4318 4319 // Trigger login event. 4320 $event = \core\event\user_loggedin::create( 4321 array( 4322 'userid' => $USER->id, 4323 'objectid' => $USER->id, 4324 'other' => array('username' => $USER->username), 4325 ) 4326 ); 4327 $event->trigger(); 4328 4329 if (isguestuser()) { 4330 // No need to continue when user is THE guest. 4331 return $USER; 4332 } 4333 4334 if (CLI_SCRIPT) { 4335 // We can redirect to password change URL only in browser. 4336 return $USER; 4337 } 4338 4339 // Select password change url. 4340 $userauth = get_auth_plugin($USER->auth); 4341 4342 // Check whether the user should be changing password. 4343 if (get_user_preferences('auth_forcepasswordchange', false)) { 4344 if ($userauth->can_change_password()) { 4345 if ($changeurl = $userauth->change_password_url()) { 4346 redirect($changeurl); 4347 } else { 4348 $SESSION->wantsurl = core_login_get_return_url(); 4349 redirect($CFG->httpswwwroot.'/login/change_password.php'); 4350 } 4351 } else { 4352 print_error('nopasswordchangeforced', 'auth'); 4353 } 4354 } 4355 return $USER; 4356 } 4357 4358 /** 4359 * Check a password hash to see if it was hashed using the legacy hash algorithm (md5). 4360 * 4361 * @param string $password String to check. 4362 * @return boolean True if the $password matches the format of an md5 sum. 4363 */ 4364 function password_is_legacy_hash($password) { 4365 return (bool) preg_match('/^[0-9a-f]{32}$/', $password); 4366 } 4367 4368 /** 4369 * Compare password against hash stored in user object to determine if it is valid. 4370 * 4371 * If necessary it also updates the stored hash to the current format. 4372 * 4373 * @param stdClass $user (Password property may be updated). 4374 * @param string $password Plain text password. 4375 * @return bool True if password is valid. 4376 */ 4377 function validate_internal_user_password($user, $password) { 4378 global $CFG; 4379 4380 if ($user->password === AUTH_PASSWORD_NOT_CACHED) { 4381 // Internal password is not used at all, it can not validate. 4382 return false; 4383 } 4384 4385 // If hash isn't a legacy (md5) hash, validate using the library function. 4386 if (!password_is_legacy_hash($user->password)) { 4387 return password_verify($password, $user->password); 4388 } 4389 4390 // Otherwise we need to check for a legacy (md5) hash instead. If the hash 4391 // is valid we can then update it to the new algorithm. 4392 4393 $sitesalt = isset($CFG->passwordsaltmain) ? $CFG->passwordsaltmain : ''; 4394 $validated = false; 4395 4396 if ($user->password === md5($password.$sitesalt) 4397 or $user->password === md5($password) 4398 or $user->password === md5(addslashes($password).$sitesalt) 4399 or $user->password === md5(addslashes($password))) { 4400 // Note: we are intentionally using the addslashes() here because we 4401 // need to accept old password hashes of passwords with magic quotes. 4402 $validated = true; 4403 4404 } else { 4405 for ($i=1; $i<=20; $i++) { // 20 alternative salts should be enough, right? 4406 $alt = 'passwordsaltalt'.$i; 4407 if (!empty($CFG->$alt)) { 4408 if ($user->password === md5($password.$CFG->$alt) or $user->password === md5(addslashes($password).$CFG->$alt)) { 4409 $validated = true; 4410 break; 4411 } 4412 } 4413 } 4414 } 4415 4416 if ($validated) { 4417 // If the password matches the existing md5 hash, update to the 4418 // current hash algorithm while we have access to the user's password. 4419 update_internal_user_password($user, $password); 4420 } 4421 4422 return $validated; 4423 } 4424 4425 /** 4426 * Calculate hash for a plain text password. 4427 * 4428 * @param string $password Plain text password to be hashed. 4429 * @param bool $fasthash If true, use a low cost factor when generating the hash 4430 * This is much faster to generate but makes the hash 4431 * less secure. It is used when lots of hashes need to 4432 * be generated quickly. 4433 * @return string The hashed password. 4434 * 4435 * @throws moodle_exception If a problem occurs while generating the hash. 4436 */ 4437 function hash_internal_user_password($password, $fasthash = false) { 4438 global $CFG; 4439 4440 // Set the cost factor to 4 for fast hashing, otherwise use default cost. 4441 $options = ($fasthash) ? array('cost' => 4) : array(); 4442 4443 $generatedhash = password_hash($password, PASSWORD_DEFAULT, $options); 4444 4445 if ($generatedhash === false || $generatedhash === null) { 4446 throw new moodle_exception('Failed to generate password hash.'); 4447 } 4448 4449 return $generatedhash; 4450 } 4451 4452 /** 4453 * Update password hash in user object (if necessary). 4454 * 4455 * The password is updated if: 4456 * 1. The password has changed (the hash of $user->password is different 4457 * to the hash of $password). 4458 * 2. The existing hash is using an out-of-date algorithm (or the legacy 4459 * md5 algorithm). 4460 * 4461 * Updating the password will modify the $user object and the database 4462 * record to use the current hashing algorithm. 4463 * 4464 * @param stdClass $user User object (password property may be updated). 4465 * @param string $password Plain text password. 4466 * @param bool $fasthash If true, use a low cost factor when generating the hash 4467 * This is much faster to generate but makes the hash 4468 * less secure. It is used when lots of hashes need to 4469 * be generated quickly. 4470 * @return bool Always returns true. 4471 */ 4472 function update_internal_user_password($user, $password, $fasthash = false) { 4473 global $CFG, $DB; 4474 4475 // Figure out what the hashed password should be. 4476 if (!isset($user->auth)) { 4477 debugging('User record in update_internal_user_password() must include field auth', 4478 DEBUG_DEVELOPER); 4479 $user->auth = $DB->get_field('user', 'auth', array('id' => $user->id)); 4480 } 4481 $authplugin = get_auth_plugin($user->auth); 4482 if ($authplugin->prevent_local_passwords()) { 4483 $hashedpassword = AUTH_PASSWORD_NOT_CACHED; 4484 } else { 4485 $hashedpassword = hash_internal_user_password($password, $fasthash); 4486 } 4487 4488 $algorithmchanged = false; 4489 4490 if ($hashedpassword === AUTH_PASSWORD_NOT_CACHED) { 4491 // Password is not cached, update it if not set to AUTH_PASSWORD_NOT_CACHED. 4492 $passwordchanged = ($user->password !== $hashedpassword); 4493 4494 } else if (isset($user->password)) { 4495 // If verification fails then it means the password has changed. 4496 $passwordchanged = !password_verify($password, $user->password); 4497 $algorithmchanged = password_needs_rehash($user->password, PASSWORD_DEFAULT); 4498 } else { 4499 // While creating new user, password in unset in $user object, to avoid 4500 // saving it with user_create() 4501 $passwordchanged = true; 4502 } 4503 4504 if ($passwordchanged || $algorithmchanged) { 4505 $DB->set_field('user', 'password', $hashedpassword, array('id' => $user->id)); 4506 $user->password = $hashedpassword; 4507 4508 // Trigger event. 4509 $user = $DB->get_record('user', array('id' => $user->id)); 4510 \core\event\user_password_updated::create_from_user($user)->trigger(); 4511 } 4512 4513 return true; 4514 } 4515 4516 /** 4517 * Get a complete user record, which includes all the info in the user record. 4518 * 4519 * Intended for setting as $USER session variable 4520 * 4521 * @param string $field The user field to be checked for a given value. 4522 * @param string $value The value to match for $field. 4523 * @param int $mnethostid 4524 * @return mixed False, or A {@link $USER} object. 4525 */ 4526 function get_complete_user_data($field, $value, $mnethostid = null) { 4527 global $CFG, $DB; 4528 4529 if (!$field || !$value) { 4530 return false; 4531 } 4532 4533 // Build the WHERE clause for an SQL query. 4534 $params = array('fieldval' => $value); 4535 $constraints = "$field = :fieldval AND deleted <> 1"; 4536 4537 // If we are loading user data based on anything other than id, 4538 // we must also restrict our search based on mnet host. 4539 if ($field != 'id') { 4540 if (empty($mnethostid)) { 4541 // If empty, we restrict to local users. 4542 $mnethostid = $CFG->mnet_localhost_id; 4543 } 4544 } 4545 if (!empty($mnethostid)) { 4546 $params['mnethostid'] = $mnethostid; 4547 $constraints .= " AND mnethostid = :mnethostid"; 4548 } 4549 4550 // Get all the basic user data. 4551 if (! $user = $DB->get_record_select('user', $constraints, $params)) { 4552 return false; 4553 } 4554 4555 // Get various settings and preferences. 4556 4557 // Preload preference cache. 4558 check_user_preferences_loaded($user); 4559 4560 // Load course enrolment related stuff. 4561 $user->lastcourseaccess = array(); // During last session. 4562 $user->currentcourseaccess = array(); // During current session. 4563 if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid' => $user->id))) { 4564 foreach ($lastaccesses as $lastaccess) { 4565 $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess; 4566 } 4567 } 4568 4569 $sql = "SELECT g.id, g.courseid 4570 FROM {groups} g, {groups_members} gm 4571 WHERE gm.groupid=g.id AND gm.userid=?"; 4572 4573 // This is a special hack to speedup calendar display. 4574 $user->groupmember = array(); 4575 if (!isguestuser($user)) { 4576 if ($groups = $DB->get_records_sql($sql, array($user->id))) { 4577 foreach ($groups as $group) { 4578 if (!array_key_exists($group->courseid, $user->groupmember)) { 4579 $user->groupmember[$group->courseid] = array(); 4580 } 4581 $user->groupmember[$group->courseid][$group->id] = $group->id; 4582 } 4583 } 4584 } 4585 4586 // Add the custom profile fields to the user record. 4587 $user->profile = array(); 4588 if (!isguestuser($user)) { 4589 require_once($CFG->dirroot.'/user/profile/lib.php'); 4590 profile_load_custom_fields($user); 4591 } 4592 4593 // Rewrite some variables if necessary. 4594 if (!empty($user->description)) { 4595 // No need to cart all of it around. 4596 $user->description = true; 4597 } 4598 if (isguestuser($user)) { 4599 // Guest language always same as site. 4600 $user->lang = $CFG->lang; 4601 // Name always in current language. 4602 $user->firstname = get_string('guestuser'); 4603 $user->lastname = ' '; 4604 } 4605 4606 return $user; 4607 } 4608 4609 /** 4610 * Validate a password against the configured password policy 4611 * 4612 * @param string $password the password to be checked against the password policy 4613 * @param string $errmsg the error message to display when the password doesn't comply with the policy. 4614 * @return bool true if the password is valid according to the policy. false otherwise. 4615 */ 4616 function check_password_policy($password, &$errmsg) { 4617 global $CFG; 4618 4619 if (empty($CFG->passwordpolicy)) { 4620 return true; 4621 } 4622 4623 $errmsg = ''; 4624 if (core_text::strlen($password) < $CFG->minpasswordlength) { 4625 $errmsg .= '<div>'. get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength) .'</div>'; 4626 4627 } 4628 if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) { 4629 $errmsg .= '<div>'. get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits) .'</div>'; 4630 4631 } 4632 if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) { 4633 $errmsg .= '<div>'. get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower) .'</div>'; 4634 4635 } 4636 if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) { 4637 $errmsg .= '<div>'. get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper) .'</div>'; 4638 4639 } 4640 if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) { 4641 $errmsg .= '<div>'. get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum) .'</div>'; 4642 } 4643 if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) { 4644 $errmsg .= '<div>'. get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars) .'</div>'; 4645 } 4646 4647 if ($errmsg == '') { 4648 return true; 4649 } else { 4650 return false; 4651 } 4652 } 4653 4654 4655 /** 4656 * When logging in, this function is run to set certain preferences for the current SESSION. 4657 */ 4658 function set_login_session_preferences() { 4659 global $SESSION; 4660 4661 $SESSION->justloggedin = true; 4662 4663 unset($SESSION->lang); 4664 unset($SESSION->forcelang); 4665 unset($SESSION->load_navigation_admin); 4666 } 4667 4668 4669 /** 4670 * Delete a course, including all related data from the database, and any associated files. 4671 * 4672 * @param mixed $courseorid The id of the course or course object to delete. 4673 * @param bool $showfeedback Whether to display notifications of each action the function performs. 4674 * @return bool true if all the removals succeeded. false if there were any failures. If this 4675 * method returns false, some of the removals will probably have succeeded, and others 4676 * failed, but you have no way of knowing which. 4677 */ 4678 function delete_course($courseorid, $showfeedback = true) { 4679 global $DB; 4680 4681 if (is_object($courseorid)) { 4682 $courseid = $courseorid->id; 4683 $course = $courseorid; 4684 } else { 4685 $courseid = $courseorid; 4686 if (!$course = $DB->get_record('course', array('id' => $courseid))) { 4687 return false; 4688 } 4689 } 4690 $context = context_course::instance($courseid); 4691 4692 // Frontpage course can not be deleted!! 4693 if ($courseid == SITEID) { 4694 return false; 4695 } 4696 4697 // Allow plugins to use this course before we completely delete it. 4698 if ($pluginsfunction = get_plugins_with_function('pre_course_delete')) { 4699 foreach ($pluginsfunction as $plugintype => $plugins) { 4700 foreach ($plugins as $pluginfunction) { 4701 $pluginfunction($course); 4702 } 4703 } 4704 } 4705 4706 // Make the course completely empty. 4707 remove_course_contents($courseid, $showfeedback); 4708 4709 // Delete the course and related context instance. 4710 context_helper::delete_instance(CONTEXT_COURSE, $courseid); 4711 4712 $DB->delete_records("course", array("id" => $courseid)); 4713 $DB->delete_records("course_format_options", array("courseid" => $courseid)); 4714 4715 // Reset all course related caches here. 4716 if (class_exists('format_base', false)) { 4717 format_base::reset_course_cache($courseid); 4718 } 4719 4720 // Trigger a course deleted event. 4721 $event = \core\event\course_deleted::create(array( 4722 'objectid' => $course->id, 4723 'context' => $context, 4724 'other' => array( 4725 'shortname' => $course->shortname, 4726 'fullname' => $course->fullname, 4727 'idnumber' => $course->idnumber 4728 ) 4729 )); 4730 $event->add_record_snapshot('course', $course); 4731 $event->trigger(); 4732 4733 return true; 4734 } 4735 4736 /** 4737 * Clear a course out completely, deleting all content but don't delete the course itself. 4738 * 4739 * This function does not verify any permissions. 4740 * 4741 * Please note this function also deletes all user enrolments, 4742 * enrolment instances and role assignments by default. 4743 * 4744 * $options: 4745 * - 'keep_roles_and_enrolments' - false by default 4746 * - 'keep_groups_and_groupings' - false by default 4747 * 4748 * @param int $courseid The id of the course that is being deleted 4749 * @param bool $showfeedback Whether to display notifications of each action the function performs. 4750 * @param array $options extra options 4751 * @return bool true if all the removals succeeded. false if there were any failures. If this 4752 * method returns false, some of the removals will probably have succeeded, and others 4753 * failed, but you have no way of knowing which. 4754 */ 4755 function remove_course_contents($courseid, $showfeedback = true, array $options = null) { 4756 global $CFG, $DB, $OUTPUT; 4757 4758 require_once($CFG->libdir.'/badgeslib.php'); 4759 require_once($CFG->libdir.'/completionlib.php'); 4760 require_once($CFG->libdir.'/questionlib.php'); 4761 require_once($CFG->libdir.'/gradelib.php'); 4762 require_once($CFG->dirroot.'/group/lib.php'); 4763 require_once($CFG->dirroot.'/comment/lib.php'); 4764 require_once($CFG->dirroot.'/rating/lib.php'); 4765 require_once($CFG->dirroot.'/notes/lib.php'); 4766 4767 // Handle course badges. 4768 badges_handle_course_deletion($courseid); 4769 4770 // NOTE: these concatenated strings are suboptimal, but it is just extra info... 4771 $strdeleted = get_string('deleted').' - '; 4772 4773 // Some crazy wishlist of stuff we should skip during purging of course content. 4774 $options = (array)$options; 4775 4776 $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); 4777 $coursecontext = context_course::instance($courseid); 4778 $fs = get_file_storage(); 4779 4780 // Delete course completion information, this has to be done before grades and enrols. 4781 $cc = new completion_info($course); 4782 $cc->clear_criteria(); 4783 if ($showfeedback) { 4784 echo $OUTPUT->notification($strdeleted.get_string('completion', 'completion'), 'notifysuccess'); 4785 } 4786 4787 // Remove all data from gradebook - this needs to be done before course modules 4788 // because while deleting this information, the system may need to reference 4789 // the course modules that own the grades. 4790 remove_course_grades($courseid, $showfeedback); 4791 remove_grade_letters($coursecontext, $showfeedback); 4792 4793 // Delete course blocks in any all child contexts, 4794 // they may depend on modules so delete them first. 4795 $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2. 4796 foreach ($childcontexts as $childcontext) { 4797 blocks_delete_all_for_context($childcontext->id); 4798 } 4799 unset($childcontexts); 4800 blocks_delete_all_for_context($coursecontext->id); 4801 if ($showfeedback) { 4802 echo $OUTPUT->notification($strdeleted.get_string('type_block_plural', 'plugin'), 'notifysuccess'); 4803 } 4804 4805 // Get the list of all modules that are properly installed. 4806 $allmodules = $DB->get_records_menu('modules', array(), '', 'name, id'); 4807 4808 // Delete every instance of every module, 4809 // this has to be done before deleting of course level stuff. 4810 $locations = core_component::get_plugin_list('mod'); 4811 foreach ($locations as $modname => $moddir) { 4812 if ($modname === 'NEWMODULE') { 4813 continue; 4814 } 4815 if (array_key_exists($modname, $allmodules)) { 4816 $sql = "SELECT cm.*, m.id AS modinstance, m.name, '$modname' AS modname 4817 FROM {".$modname."} m 4818 LEFT JOIN {course_modules} cm ON cm.instance = m.id AND cm.module = :moduleid 4819 WHERE m.course = :courseid"; 4820 $instances = $DB->get_records_sql($sql, array('courseid' => $course->id, 4821 'modulename' => $modname, 'moduleid' => $allmodules[$modname])); 4822 4823 include_once("$moddir/lib.php"); // Shows php warning only if plugin defective. 4824 $moddelete = $modname .'_delete_instance'; // Delete everything connected to an instance. 4825 $moddeletecourse = $modname .'_delete_course'; // Delete other stray stuff (uncommon). 4826 4827 if ($instances) { 4828 foreach ($instances as $cm) { 4829 if ($cm->id) { 4830 // Delete activity context questions and question categories. 4831 question_delete_activity($cm, $showfeedback); 4832 // Notify the competency subsystem. 4833 \core_competency\api::hook_course_module_deleted($cm); 4834 } 4835 if (function_exists($moddelete)) { 4836 // This purges all module data in related tables, extra user prefs, settings, etc. 4837 $moddelete($cm->modinstance); 4838 } else { 4839 // NOTE: we should not allow installation of modules with missing delete support! 4840 debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!"); 4841 $DB->delete_records($modname, array('id' => $cm->modinstance)); 4842 } 4843 4844 if ($cm->id) { 4845 // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition. 4846 context_helper::delete_instance(CONTEXT_MODULE, $cm->id); 4847 $DB->delete_records('course_modules', array('id' => $cm->id)); 4848 } 4849 } 4850 } 4851 if (function_exists($moddeletecourse)) { 4852 // Execute optional course cleanup callback. Deprecated since Moodle 3.2. TODO MDL-53297 remove in 3.6. 4853 debugging("Callback delete_course is deprecated. Function $moddeletecourse should be converted " . 4854 'to observer of event \core\event\course_content_deleted', DEBUG_DEVELOPER); 4855 $moddeletecourse($course, $showfeedback); 4856 } 4857 if ($instances and $showfeedback) { 4858 echo $OUTPUT->notification($strdeleted.get_string('pluginname', $modname), 'notifysuccess'); 4859 } 4860 } else { 4861 // Ooops, this module is not properly installed, force-delete it in the next block. 4862 } 4863 } 4864 4865 // We have tried to delete everything the nice way - now let's force-delete any remaining module data. 4866 4867 // Remove all data from availability and completion tables that is associated 4868 // with course-modules belonging to this course. Note this is done even if the 4869 // features are not enabled now, in case they were enabled previously. 4870 $DB->delete_records_select('course_modules_completion', 4871 'coursemoduleid IN (SELECT id from {course_modules} WHERE course=?)', 4872 array($courseid)); 4873 4874 // Remove course-module data that has not been removed in modules' _delete_instance callbacks. 4875 $cms = $DB->get_records('course_modules', array('course' => $course->id)); 4876 $allmodulesbyid = array_flip($allmodules); 4877 foreach ($cms as $cm) { 4878 if (array_key_exists($cm->module, $allmodulesbyid)) { 4879 try { 4880 $DB->delete_records($allmodulesbyid[$cm->module], array('id' => $cm->instance)); 4881 } catch (Exception $e) { 4882 // Ignore weird or missing table problems. 4883 } 4884 } 4885 context_helper::delete_instance(CONTEXT_MODULE, $cm->id); 4886 $DB->delete_records('course_modules', array('id' => $cm->id)); 4887 } 4888 4889 if ($showfeedback) { 4890 echo $OUTPUT->notification($strdeleted.get_string('type_mod_plural', 'plugin'), 'notifysuccess'); 4891 } 4892 4893 // Cleanup the rest of plugins. Deprecated since Moodle 3.2. TODO MDL-53297 remove in 3.6. 4894 $cleanuplugintypes = array('report', 'coursereport', 'format'); 4895 $callbacks = get_plugins_with_function('delete_course', 'lib.php'); 4896 foreach ($cleanuplugintypes as $type) { 4897 if (!empty($callbacks[$type])) { 4898 foreach ($callbacks[$type] as $pluginfunction) { 4899 debugging("Callback delete_course is deprecated. Function $pluginfunction should be converted " . 4900 'to observer of event \core\event\course_content_deleted', DEBUG_DEVELOPER); 4901 $pluginfunction($course->id, $showfeedback); 4902 } 4903 if ($showfeedback) { 4904 echo $OUTPUT->notification($strdeleted.get_string('type_'.$type.'_plural', 'plugin'), 'notifysuccess'); 4905 } 4906 } 4907 } 4908 4909 // Delete questions and question categories. 4910 question_delete_course($course, $showfeedback); 4911 if ($showfeedback) { 4912 echo $OUTPUT->notification($strdeleted.get_string('questions', 'question'), 'notifysuccess'); 4913 } 4914 4915 // Make sure there are no subcontexts left - all valid blocks and modules should be already gone. 4916 $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2. 4917 foreach ($childcontexts as $childcontext) { 4918 $childcontext->delete(); 4919 } 4920 unset($childcontexts); 4921 4922 // Remove all roles and enrolments by default. 4923 if (empty($options['keep_roles_and_enrolments'])) { 4924 // This hack is used in restore when deleting contents of existing course. 4925 role_unassign_all(array('contextid' => $coursecontext->id, 'component' => ''), true); 4926 enrol_course_delete($course); 4927 if ($showfeedback) { 4928 echo $OUTPUT->notification($strdeleted.get_string('type_enrol_plural', 'plugin'), 'notifysuccess'); 4929 } 4930 } 4931 4932 // Delete any groups, removing members and grouping/course links first. 4933 if (empty($options['keep_groups_and_groupings'])) { 4934 groups_delete_groupings($course->id, $showfeedback); 4935 groups_delete_groups($course->id, $showfeedback); 4936 } 4937 4938 // Filters be gone! 4939 filter_delete_all_for_context($coursecontext->id); 4940 4941 // Notes, you shall not pass! 4942 note_delete_all($course->id); 4943 4944 // Die comments! 4945 comment::delete_comments($coursecontext->id); 4946 4947 // Ratings are history too. 4948 $delopt = new stdclass(); 4949 $delopt->contextid = $coursecontext->id; 4950 $rm = new rating_manager(); 4951 $rm->delete_ratings($delopt); 4952 4953 // Delete course tags. 4954 core_tag_tag::remove_all_item_tags('core', 'course', $course->id); 4955 4956 // Notify the competency subsystem. 4957 \core_competency\api::hook_course_deleted($course); 4958 4959 // Delete calendar events. 4960 $DB->delete_records('event', array('courseid' => $course->id)); 4961 $fs->delete_area_files($coursecontext->id, 'calendar'); 4962 4963 // Delete all related records in other core tables that may have a courseid 4964 // This array stores the tables that need to be cleared, as 4965 // table_name => column_name that contains the course id. 4966 $tablestoclear = array( 4967 'backup_courses' => 'courseid', // Scheduled backup stuff. 4968 'user_lastaccess' => 'courseid', // User access info. 4969 ); 4970 foreach ($tablestoclear as $table => $col) { 4971 $DB->delete_records($table, array($col => $course->id)); 4972 } 4973 4974 // Delete all course backup files. 4975 $fs->delete_area_files($coursecontext->id, 'backup'); 4976 4977 // Cleanup course record - remove links to deleted stuff. 4978 $oldcourse = new stdClass(); 4979 $oldcourse->id = $course->id; 4980 $oldcourse->summary = ''; 4981 $oldcourse->cacherev = 0; 4982 $oldcourse->legacyfiles = 0; 4983 if (!empty($options['keep_groups_and_groupings'])) { 4984 $oldcourse->defaultgroupingid = 0; 4985 } 4986 $DB->update_record('course', $oldcourse); 4987 4988 // Delete course sections. 4989 $DB->delete_records('course_sections', array('course' => $course->id)); 4990 4991 // Delete legacy, section and any other course files. 4992 $fs->delete_area_files($coursecontext->id, 'course'); // Files from summary and section. 4993 4994 // Delete all remaining stuff linked to context such as files, comments, ratings, etc. 4995 if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) { 4996 // Easy, do not delete the context itself... 4997 $coursecontext->delete_content(); 4998 } else { 4999 // Hack alert!!!! 5000 // We can not drop all context stuff because it would bork enrolments and roles, 5001 // there might be also files used by enrol plugins... 5002 } 5003 5004 // Delete legacy files - just in case some files are still left there after conversion to new file api, 5005 // also some non-standard unsupported plugins may try to store something there. 5006 fulldelete($CFG->dataroot.'/'.$course->id); 5007 5008 // Delete from cache to reduce the cache size especially makes sense in case of bulk course deletion. 5009 $cachemodinfo = cache::make('core', 'coursemodinfo'); 5010 $cachemodinfo->delete($courseid); 5011 5012 // Trigger a course content deleted event. 5013 $event = \core\event\course_content_deleted::create(array( 5014 'objectid' => $course->id, 5015 'context' => $coursecontext, 5016 'other' => array('shortname' => $course->shortname, 5017 'fullname' => $course->fullname, 5018 'options' => $options) // Passing this for legacy reasons. 5019 )); 5020 $event->add_record_snapshot('course', $course); 5021 $event->trigger(); 5022 5023 return true; 5024 } 5025 5026 /** 5027 * Change dates in module - used from course reset. 5028 * 5029 * @param string $modname forum, assignment, etc 5030 * @param array $fields array of date fields from mod table 5031 * @param int $timeshift time difference 5032 * @param int $courseid 5033 * @param int $modid (Optional) passed if specific mod instance in course needs to be updated. 5034 * @return bool success 5035 */ 5036 function shift_course_mod_dates($modname, $fields, $timeshift, $courseid, $modid = 0) { 5037 global $CFG, $DB; 5038 include_once($CFG->dirroot.'/mod/'.$modname.'/lib.php'); 5039 5040 $return = true; 5041 $params = array($timeshift, $courseid); 5042 foreach ($fields as $field) { 5043 $updatesql = "UPDATE {".$modname."} 5044 SET $field = $field + ? 5045 WHERE course=? AND $field<>0"; 5046 if ($modid) { 5047 $updatesql .= ' AND id=?'; 5048 $params[] = $modid; 5049 } 5050 $return = $DB->execute($updatesql, $params) && $return; 5051 } 5052 5053 $refreshfunction = $modname.'_refresh_events'; 5054 if (function_exists($refreshfunction)) { 5055 $refreshfunction($courseid); 5056 } 5057 5058 return $return; 5059 } 5060 5061 /** 5062 * This function will empty a course of user data. 5063 * It will retain the activities and the structure of the course. 5064 * 5065 * @param object $data an object containing all the settings including courseid (without magic quotes) 5066 * @return array status array of array component, item, error 5067 */ 5068 function reset_course_userdata($data) { 5069 global $CFG, $DB; 5070 require_once($CFG->libdir.'/gradelib.php'); 5071 require_once($CFG->libdir.'/completionlib.php'); 5072 require_once($CFG->dirroot.'/group/lib.php'); 5073 5074 $data->courseid = $data->id; 5075 $context = context_course::instance($data->courseid); 5076 5077 $eventparams = array( 5078 'context' => $context, 5079 'courseid' => $data->id, 5080 'other' => array( 5081 'reset_options' => (array) $data 5082 ) 5083 ); 5084 $event = \core\event\course_reset_started::create($eventparams); 5085 $event->trigger(); 5086 5087 // Calculate the time shift of dates. 5088 if (!empty($data->reset_start_date)) { 5089 // Time part of course startdate should be zero. 5090 $data->timeshift = $data->reset_start_date - usergetmidnight($data->reset_start_date_old); 5091 } else { 5092 $data->timeshift = 0; 5093 } 5094 5095 // Result array: component, item, error. 5096 $status = array(); 5097 5098 // Start the resetting. 5099 $componentstr = get_string('general'); 5100 5101 // Move the course start time. 5102 if (!empty($data->reset_start_date) and $data->timeshift) { 5103 // Change course start data. 5104 $DB->set_field('course', 'startdate', $data->reset_start_date, array('id' => $data->courseid)); 5105 // Update all course and group events - do not move activity events. 5106 $updatesql = "UPDATE {event} 5107 SET timestart = timestart + ? 5108 WHERE courseid=? AND instance=0"; 5109 $DB->execute($updatesql, array($data->timeshift, $data->courseid)); 5110 5111 // Update any date activity restrictions. 5112 if ($CFG->enableavailability) { 5113 \availability_date\condition::update_all_dates($data->courseid, $data->timeshift); 5114 } 5115 5116 $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false); 5117 } 5118 5119 if (!empty($data->reset_events)) { 5120 $DB->delete_records('event', array('courseid' => $data->courseid)); 5121 $status[] = array('component' => $componentstr, 'item' => get_string('deleteevents', 'calendar'), 'error' => false); 5122 } 5123 5124 if (!empty($data->reset_notes)) { 5125 require_once($CFG->dirroot.'/notes/lib.php'); 5126 note_delete_all($data->courseid); 5127 $status[] = array('component' => $componentstr, 'item' => get_string('deletenotes', 'notes'), 'error' => false); 5128 } 5129 5130 if (!empty($data->delete_blog_associations)) { 5131 require_once($CFG->dirroot.'/blog/lib.php'); 5132 blog_remove_associations_for_course($data->courseid); 5133 $status[] = array('component' => $componentstr, 'item' => get_string('deleteblogassociations', 'blog'), 'error' => false); 5134 } 5135 5136 if (!empty($data->reset_completion)) { 5137 // Delete course and activity completion information. 5138 $course = $DB->get_record('course', array('id' => $data->courseid)); 5139 $cc = new completion_info($course); 5140 $cc->delete_all_completion_data(); 5141 $status[] = array('component' => $componentstr, 5142 'item' => get_string('deletecompletiondata', 'completion'), 'error' => false); 5143 } 5144 5145 if (!empty($data->reset_competency_ratings)) { 5146 \core_competency\api::hook_course_reset_competency_ratings($data->courseid); 5147 $status[] = array('component' => $componentstr, 5148 'item' => get_string('deletecompetencyratings', 'core_competency'), 'error' => false); 5149 } 5150 5151 $componentstr = get_string('roles'); 5152 5153 if (!empty($data->reset_roles_overrides)) { 5154 $children = $context->get_child_contexts(); 5155 foreach ($children as $child) { 5156 $DB->delete_records('role_capabilities', array('contextid' => $child->id)); 5157 } 5158 $DB->delete_records('role_capabilities', array('contextid' => $context->id)); 5159 // Force refresh for logged in users. 5160 $context->mark_dirty(); 5161 $status[] = array('component' => $componentstr, 'item' => get_string('deletecourseoverrides', 'role'), 'error' => false); 5162 } 5163 5164 if (!empty($data->reset_roles_local)) { 5165 $children = $context->get_child_contexts(); 5166 foreach ($children as $child) { 5167 role_unassign_all(array('contextid' => $child->id)); 5168 } 5169 // Force refresh for logged in users. 5170 $context->mark_dirty(); 5171 $status[] = array('component' => $componentstr, 'item' => get_string('deletelocalroles', 'role'), 'error' => false); 5172 } 5173 5174 // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc. 5175 $data->unenrolled = array(); 5176 if (!empty($data->unenrol_users)) { 5177 $plugins = enrol_get_plugins(true); 5178 $instances = enrol_get_instances($data->courseid, true); 5179 foreach ($instances as $key => $instance) { 5180 if (!isset($plugins[$instance->enrol])) { 5181 unset($instances[$key]); 5182 continue; 5183 } 5184 } 5185 5186 foreach ($data->unenrol_users as $withroleid) { 5187 if ($withroleid) { 5188 $sql = "SELECT ue.* 5189 FROM {user_enrolments} ue 5190 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid) 5191 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid) 5192 JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)"; 5193 $params = array('courseid' => $data->courseid, 'roleid' => $withroleid, 'courselevel' => CONTEXT_COURSE); 5194 5195 } else { 5196 // Without any role assigned at course context. 5197 $sql = "SELECT ue.* 5198 FROM {user_enrolments} ue 5199 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid) 5200 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid) 5201 LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid) 5202 WHERE ra.id IS null"; 5203 $params = array('courseid' => $data->courseid, 'courselevel' => CONTEXT_COURSE); 5204 } 5205 5206 $rs = $DB->get_recordset_sql($sql, $params); 5207 foreach ($rs as $ue) { 5208 if (!isset($instances[$ue->enrolid])) { 5209 continue; 5210 } 5211 $instance = $instances[$ue->enrolid]; 5212 $plugin = $plugins[$instance->enrol]; 5213 if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) { 5214 continue; 5215 } 5216 5217 $plugin->unenrol_user($instance, $ue->userid); 5218 $data->unenrolled[$ue->userid] = $ue->userid; 5219 } 5220 $rs->close(); 5221 } 5222 } 5223 if (!empty($data->unenrolled)) { 5224 $status[] = array( 5225 'component' => $componentstr, 5226 'item' => get_string('unenrol', 'enrol').' ('.count($data->unenrolled).')', 5227 'error' => false 5228 ); 5229 } 5230 5231 $componentstr = get_string('groups'); 5232 5233 // Remove all group members. 5234 if (!empty($data->reset_groups_members)) { 5235 groups_delete_group_members($data->courseid); 5236 $status[] = array('component' => $componentstr, 'item' => get_string('removegroupsmembers', 'group'), 'error' => false); 5237 } 5238 5239 // Remove all groups. 5240 if (!empty($data->reset_groups_remove)) { 5241 groups_delete_groups($data->courseid, false); 5242 $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroups', 'group'), 'error' => false); 5243 } 5244 5245 // Remove all grouping members. 5246 if (!empty($data->reset_groupings_members)) { 5247 groups_delete_groupings_groups($data->courseid, false); 5248 $status[] = array('component' => $componentstr, 'item' => get_string('removegroupingsmembers', 'group'), 'error' => false); 5249 } 5250 5251 // Remove all groupings. 5252 if (!empty($data->reset_groupings_remove)) { 5253 groups_delete_groupings($data->courseid, false); 5254 $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroupings', 'group'), 'error' => false); 5255 } 5256 5257 // Look in every instance of every module for data to delete. 5258 $unsupportedmods = array(); 5259 if ($allmods = $DB->get_records('modules') ) { 5260 foreach ($allmods as $mod) { 5261 $modname = $mod->name; 5262 $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php'; 5263 $moddeleteuserdata = $modname.'_reset_userdata'; // Function to delete user data. 5264 if (file_exists($modfile)) { 5265 if (!$DB->count_records($modname, array('course' => $data->courseid))) { 5266 continue; // Skip mods with no instances. 5267 } 5268 include_once($modfile); 5269 if (function_exists($moddeleteuserdata)) { 5270 $modstatus = $moddeleteuserdata($data); 5271 if (is_array($modstatus)) { 5272 $status = array_merge($status, $modstatus); 5273 } else { 5274 debugging('Module '.$modname.' returned incorrect staus - must be an array!'); 5275 } 5276 } else { 5277 $unsupportedmods[] = $mod; 5278 } 5279 } else { 5280 debugging('Missing lib.php in '.$modname.' module!'); 5281 } 5282 } 5283 } 5284 5285 // Mention unsupported mods. 5286 if (!empty($unsupportedmods)) { 5287 foreach ($unsupportedmods as $mod) { 5288 $status[] = array( 5289 'component' => get_string('modulenameplural', $mod->name), 5290 'item' => '', 5291 'error' => get_string('resetnotimplemented') 5292 ); 5293 } 5294 } 5295 5296 $componentstr = get_string('gradebook', 'grades'); 5297 // Reset gradebook,. 5298 if (!empty($data->reset_gradebook_items)) { 5299 remove_course_grades($data->courseid, false); 5300 grade_grab_course_grades($data->courseid); 5301 grade_regrade_final_grades($data->courseid); 5302 $status[] = array('component' => $componentstr, 'item' => get_string('removeallcourseitems', 'grades'), 'error' => false); 5303 5304 } else if (!empty($data->reset_gradebook_grades)) { 5305 grade_course_reset($data->courseid); 5306 $status[] = array('component' => $componentstr, 'item' => get_string('removeallcoursegrades', 'grades'), 'error' => false); 5307 } 5308 // Reset comments. 5309 if (!empty($data->reset_comments)) { 5310 require_once($CFG->dirroot.'/comment/lib.php'); 5311 comment::reset_course_page_comments($context); 5312 } 5313 5314 $event = \core\event\course_reset_ended::create($eventparams); 5315 $event->trigger(); 5316 5317 return $status; 5318 } 5319 5320 /** 5321 * Generate an email processing address. 5322 * 5323 * @param int $modid 5324 * @param string $modargs 5325 * @return string Returns email processing address 5326 */ 5327 function generate_email_processing_address($modid, $modargs) { 5328 global $CFG; 5329 5330 $header = $CFG->mailprefix . substr(base64_encode(pack('C', $modid)), 0, 2).$modargs; 5331 return $header . substr(md5($header.get_site_identifier()), 0, 16).'@'.$CFG->maildomain; 5332 } 5333 5334 /** 5335 * ? 5336 * 5337 * @todo Finish documenting this function 5338 * 5339 * @param string $modargs 5340 * @param string $body Currently unused 5341 */ 5342 function moodle_process_email($modargs, $body) { 5343 global $DB; 5344 5345 // The first char should be an unencoded letter. We'll take this as an action. 5346 switch ($modargs{0}) { 5347 case 'B': { // Bounce. 5348 list(, $userid) = unpack('V', base64_decode(substr($modargs, 1, 8))); 5349 if ($user = $DB->get_record("user", array('id' => $userid), "id,email")) { 5350 // Check the half md5 of their email. 5351 $md5check = substr(md5($user->email), 0, 16); 5352 if ($md5check == substr($modargs, -16)) { 5353 set_bounce_count($user); 5354 } 5355 // Else maybe they've already changed it? 5356 } 5357 } 5358 break; 5359 // Maybe more later? 5360 } 5361 } 5362 5363 // CORRESPONDENCE. 5364 5365 /** 5366 * Get mailer instance, enable buffering, flush buffer or disable buffering. 5367 * 5368 * @param string $action 'get', 'buffer', 'close' or 'flush' 5369 * @return moodle_phpmailer|null mailer instance if 'get' used or nothing 5370 */ 5371 function get_mailer($action='get') { 5372 global $CFG; 5373 5374 /** @var moodle_phpmailer $mailer */ 5375 static $mailer = null; 5376 static $counter = 0; 5377 5378 if (!isset($CFG->smtpmaxbulk)) { 5379 $CFG->smtpmaxbulk = 1; 5380 } 5381 5382 if ($action == 'get') { 5383 $prevkeepalive = false; 5384 5385 if (isset($mailer) and $mailer->Mailer == 'smtp') { 5386 if ($counter < $CFG->smtpmaxbulk and !$mailer->isError()) { 5387 $counter++; 5388 // Reset the mailer. 5389 $mailer->Priority = 3; 5390 $mailer->CharSet = 'UTF-8'; // Our default. 5391 $mailer->ContentType = "text/plain"; 5392 $mailer->Encoding = "8bit"; 5393 $mailer->From = "root@localhost"; 5394 $mailer->FromName = "Root User"; 5395 $mailer->Sender = ""; 5396 $mailer->Subject = ""; 5397 $mailer->Body = ""; 5398 $mailer->AltBody = ""; 5399 $mailer->ConfirmReadingTo = ""; 5400 5401 $mailer->clearAllRecipients(); 5402 $mailer->clearReplyTos(); 5403 $mailer->clearAttachments(); 5404 $mailer->clearCustomHeaders(); 5405 return $mailer; 5406 } 5407 5408 $prevkeepalive = $mailer->SMTPKeepAlive; 5409 get_mailer('flush'); 5410 } 5411 5412 require_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php'); 5413 $mailer = new moodle_phpmailer(); 5414 5415 $counter = 1; 5416 5417 if ($CFG->smtphosts == 'qmail') { 5418 // Use Qmail system. 5419 $mailer->isQmail(); 5420 5421 } else if (empty($CFG->smtphosts)) { 5422 // Use PHP mail() = sendmail. 5423 $mailer->isMail(); 5424 5425 } else { 5426 // Use SMTP directly. 5427 $mailer->isSMTP(); 5428 if (!empty($CFG->debugsmtp)) { 5429 $mailer->SMTPDebug = true; 5430 } 5431 // Specify main and backup servers. 5432 $mailer->Host = $CFG->smtphosts; 5433 // Specify secure connection protocol. 5434 $mailer->SMTPSecure = $CFG->smtpsecure; 5435 // Use previous keepalive. 5436 $mailer->SMTPKeepAlive = $prevkeepalive; 5437 5438 if ($CFG->smtpuser) { 5439 // Use SMTP authentication. 5440 $mailer->SMTPAuth = true; 5441 $mailer->Username = $CFG->smtpuser; 5442 $mailer->Password = $CFG->smtppass; 5443 } 5444 } 5445 5446 return $mailer; 5447 } 5448 5449 $nothing = null; 5450 5451 // Keep smtp session open after sending. 5452 if ($action == 'buffer') { 5453 if (!empty($CFG->smtpmaxbulk)) { 5454 get_mailer('flush'); 5455 $m = get_mailer(); 5456 if ($m->Mailer == 'smtp') { 5457 $m->SMTPKeepAlive = true; 5458 } 5459 } 5460 return $nothing; 5461 } 5462 5463 // Close smtp session, but continue buffering. 5464 if ($action == 'flush') { 5465 if (isset($mailer) and $mailer->Mailer == 'smtp') { 5466 if (!empty($mailer->SMTPDebug)) { 5467 echo '<pre>'."\n"; 5468 } 5469 $mailer->SmtpClose(); 5470 if (!empty($mailer->SMTPDebug)) { 5471 echo '</pre>'; 5472 } 5473 } 5474 return $nothing; 5475 } 5476 5477 // Close smtp session, do not buffer anymore. 5478 if ($action == 'close') { 5479 if (isset($mailer) and $mailer->Mailer == 'smtp') { 5480 get_mailer('flush'); 5481 $mailer->SMTPKeepAlive = false; 5482 } 5483 $mailer = null; // Better force new instance. 5484 return $nothing; 5485 } 5486 } 5487 5488 /** 5489 * A helper function to test for email diversion 5490 * 5491 * @param string $email 5492 * @return bool Returns true if the email should be diverted 5493 */ 5494 function email_should_be_diverted($email) { 5495 global $CFG; 5496 5497 if (empty($CFG->divertallemailsto)) { 5498 return false; 5499 } 5500 5501 if (empty($CFG->divertallemailsexcept)) { 5502 return true; 5503 } 5504 5505 $patterns = array_map('trim', explode(',', $CFG->divertallemailsexcept)); 5506 foreach ($patterns as $pattern) { 5507 if (preg_match("/$pattern/", $email)) { 5508 return false; 5509 } 5510 } 5511 5512 return true; 5513 } 5514 5515 /** 5516 * Generate a unique email Message-ID using the moodle domain and install path 5517 * 5518 * @param string $localpart An optional unique message id prefix. 5519 * @return string The formatted ID ready for appending to the email headers. 5520 */ 5521 function generate_email_messageid($localpart = null) { 5522 global $CFG; 5523 5524 $urlinfo = parse_url($CFG->wwwroot); 5525 $base = '@' . $urlinfo['host']; 5526 5527 // If multiple moodles are on the same domain we want to tell them 5528 // apart so we add the install path to the local part. This means 5529 // that the id local part should never contain a / character so 5530 // we can correctly parse the id to reassemble the wwwroot. 5531 if (isset($urlinfo['path'])) { 5532 $base = $urlinfo['path'] . $base; 5533 } 5534 5535 if (empty($localpart)) { 5536 $localpart = uniqid('', true); 5537 } 5538 5539 // Because we may have an option /installpath suffix to the local part 5540 // of the id we need to escape any / chars which are in the $localpart. 5541 $localpart = str_replace('/', '%2F', $localpart); 5542 5543 return '<' . $localpart . $base . '>'; 5544 } 5545 5546 /** 5547 * Send an email to a specified user 5548 * 5549 * @param stdClass $user A {@link $USER} object 5550 * @param stdClass $from A {@link $USER} object 5551 * @param string $subject plain text subject line of the email 5552 * @param string $messagetext plain text version of the message 5553 * @param string $messagehtml complete html version of the message (optional) 5554 * @param string $attachment a file on the filesystem, either relative to $CFG->dataroot or a full path to a file in $CFG->tempdir 5555 * @param string $attachname the name of the file (extension indicates MIME) 5556 * @param bool $usetrueaddress determines whether $from email address should 5557 * be sent out. Will be overruled by user profile setting for maildisplay 5558 * @param string $replyto Email address to reply to 5559 * @param string $replytoname Name of reply to recipient 5560 * @param int $wordwrapwidth custom word wrap width, default 79 5561 * @return bool Returns true if mail was sent OK and false if there was an error. 5562 */ 5563 function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '', $attachment = '', $attachname = '', 5564 $usetrueaddress = true, $replyto = '', $replytoname = '', $wordwrapwidth = 79) { 5565 5566 global $CFG, $PAGE, $SITE; 5567 5568 if (empty($user) or empty($user->id)) { 5569 debugging('Can not send email to null user', DEBUG_DEVELOPER); 5570 return false; 5571 } 5572 5573 if (empty($user->email)) { 5574 debugging('Can not send email to user without email: '.$user->id, DEBUG_DEVELOPER); 5575 return false; 5576 } 5577 5578 if (!empty($user->deleted)) { 5579 debugging('Can not send email to deleted user: '.$user->id, DEBUG_DEVELOPER); 5580 return false; 5581 } 5582 5583 if (defined('BEHAT_SITE_RUNNING')) { 5584 // Fake email sending in behat. 5585 return true; 5586 } 5587 5588 if (!empty($CFG->noemailever)) { 5589 // Hidden setting for development sites, set in config.php if needed. 5590 debugging('Not sending email due to $CFG->noemailever config setting', DEBUG_NORMAL); 5591 return true; 5592 } 5593 5594 if (email_should_be_diverted($user->email)) { 5595 $subject = "[DIVERTED {$user->email}] $subject"; 5596 $user = clone($user); 5597 $user->email = $CFG->divertallemailsto; 5598 } 5599 5600 // Skip mail to suspended users. 5601 if ((isset($user->auth) && $user->auth=='nologin') or (isset($user->suspended) && $user->suspended)) { 5602 return true; 5603 } 5604 5605 if (!validate_email($user->email)) { 5606 // We can not send emails to invalid addresses - it might create security issue or confuse the mailer. 5607 debugging("email_to_user: User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending."); 5608 return false; 5609 } 5610 5611 if (over_bounce_threshold($user)) { 5612 debugging("email_to_user: User $user->id (".fullname($user).") is over bounce threshold! Not sending."); 5613 return false; 5614 } 5615 5616 // TLD .invalid is specifically reserved for invalid domain names. 5617 // For More information, see {@link http://tools.ietf.org/html/rfc2606#section-2}. 5618 if (substr($user->email, -8) == '.invalid') { 5619 debugging("email_to_user: User $user->id (".fullname($user).") email domain ($user->email) is invalid! Not sending."); 5620 return true; // This is not an error. 5621 } 5622 5623 // If the user is a remote mnet user, parse the email text for URL to the 5624 // wwwroot and modify the url to direct the user's browser to login at their 5625 // home site (identity provider - idp) before hitting the link itself. 5626 if (is_mnet_remote_user($user)) { 5627 require_once($CFG->dirroot.'/mnet/lib.php'); 5628 5629 $jumpurl = mnet_get_idp_jump_url($user); 5630 $callback = partial('mnet_sso_apply_indirection', $jumpurl); 5631 5632 $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%", 5633 $callback, 5634 $messagetext); 5635 $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%", 5636 $callback, 5637 $messagehtml); 5638 } 5639 $mail = get_mailer(); 5640 5641 if (!empty($mail->SMTPDebug)) { 5642 echo '<pre>' . "\n"; 5643 } 5644 5645 $temprecipients = array(); 5646 $tempreplyto = array(); 5647 5648 $supportuser = core_user::get_support_user(); 5649 5650 // Make up an email address for handling bounces. 5651 if (!empty($CFG->handlebounces)) { 5652 $modargs = 'B'.base64_encode(pack('V', $user->id)).substr(md5($user->email), 0, 16); 5653 $mail->Sender = generate_email_processing_address(0, $modargs); 5654 } else { 5655 $mail->Sender = $supportuser->email; 5656 } 5657 5658 if (!empty($CFG->emailonlyfromnoreplyaddress)) { 5659 $usetrueaddress = false; 5660 if (empty($replyto) && $from->maildisplay) { 5661 $replyto = $from->email; 5662 $replytoname = fullname($from); 5663 } 5664 } 5665 5666 if (is_string($from)) { // So we can pass whatever we want if there is need. 5667 $mail->From = $CFG->noreplyaddress; 5668 $mail->FromName = $from; 5669 } else if ($usetrueaddress and $from->maildisplay) { 5670 $mail->From = $from->email; 5671 $mail->FromName = fullname($from); 5672 } else { 5673 $mail->From = $CFG->noreplyaddress; 5674 $mail->FromName = fullname($from); 5675 if (empty($replyto)) { 5676 $tempreplyto[] = array($CFG->noreplyaddress, get_string('noreplyname')); 5677 } 5678 } 5679 5680 if (!empty($replyto)) { 5681 $tempreplyto[] = array($replyto, $replytoname); 5682 } 5683 5684 $temprecipients[] = array($user->email, fullname($user)); 5685 5686 // Set word wrap. 5687 $mail->WordWrap = $wordwrapwidth; 5688 5689 if (!empty($from->customheaders)) { 5690 // Add custom headers. 5691 if (is_array($from->customheaders)) { 5692 foreach ($from->customheaders as $customheader) { 5693 $mail->addCustomHeader($customheader); 5694 } 5695 } else { 5696 $mail->addCustomHeader($from->customheaders); 5697 } 5698 } 5699 5700 // If the X-PHP-Originating-Script email header is on then also add an additional 5701 // header with details of where exactly in moodle the email was triggered from, 5702 // either a call to message_send() or to email_to_user(). 5703 if (ini_get('mail.add_x_header')) { 5704 5705 $stack = debug_backtrace(false); 5706 $origin = $stack[0]; 5707 5708 foreach ($stack as $depth => $call) { 5709 if ($call['function'] == 'message_send') { 5710 $origin = $call; 5711 } 5712 } 5713 5714 $originheader = $CFG->wwwroot . ' => ' . gethostname() . ':' 5715 . str_replace($CFG->dirroot . '/', '', $origin['file']) . ':' . $origin['line']; 5716 $mail->addCustomHeader('X-Moodle-Originating-Script: ' . $originheader); 5717 } 5718 5719 if (!empty($from->priority)) { 5720 $mail->Priority = $from->priority; 5721 } 5722 5723 $renderer = $PAGE->get_renderer('core'); 5724 $context = array( 5725 'sitefullname' => $SITE->fullname, 5726 'siteshortname' => $SITE->shortname, 5727 'sitewwwroot' => $CFG->wwwroot, 5728 'subject' => $subject, 5729 'to' => $user->email, 5730 'toname' => fullname($user), 5731 'from' => $mail->From, 5732 'fromname' => $mail->FromName, 5733 ); 5734 if (!empty($tempreplyto[0])) { 5735 $context['replyto'] = $tempreplyto[0][0]; 5736 $context['replytoname'] = $tempreplyto[0][1]; 5737 } 5738 if ($user->id > 0) { 5739 $context['touserid'] = $user->id; 5740 $context['tousername'] = $user->username; 5741 } 5742 5743 if (!empty($user->mailformat) && $user->mailformat == 1) { 5744 // Only process html templates if the user preferences allow html email. 5745 5746 if ($messagehtml) { 5747 // If html has been given then pass it through the template. 5748 $context['body'] = $messagehtml; 5749 $messagehtml = $renderer->render_from_template('core/email_html', $context); 5750 5751 } else { 5752 // If no html has been given, BUT there is an html wrapping template then 5753 // auto convert the text to html and then wrap it. 5754 $autohtml = trim(text_to_html($messagetext)); 5755 $context['body'] = $autohtml; 5756 $temphtml = $renderer->render_from_template('core/email_html', $context); 5757 if ($autohtml != $temphtml) { 5758 $messagehtml = $temphtml; 5759 } 5760 } 5761 } 5762 5763 $context['body'] = $messagetext; 5764 $mail->Subject = $renderer->render_from_template('core/email_subject', $context); 5765 $mail->FromName = $renderer->render_from_template('core/email_fromname', $context); 5766 $messagetext = $renderer->render_from_template('core/email_text', $context); 5767 5768 // Autogenerate a MessageID if it's missing. 5769 if (empty($mail->MessageID)) { 5770 $mail->MessageID = generate_email_messageid(); 5771 } 5772 5773 if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) { 5774 // Don't ever send HTML to users who don't want it. 5775 $mail->isHTML(true); 5776 $mail->Encoding = 'quoted-printable'; 5777 $mail->Body = $messagehtml; 5778 $mail->AltBody = "\n$messagetext\n"; 5779 } else { 5780 $mail->IsHTML(false); 5781 $mail->Body = "\n$messagetext\n"; 5782 } 5783 5784 if ($attachment && $attachname) { 5785 if (preg_match( "~\\.\\.~" , $attachment )) { 5786 // Security check for ".." in dir path. 5787 $temprecipients[] = array($supportuser->email, fullname($supportuser, true)); 5788 $mail->addStringAttachment('Error in attachment. User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain'); 5789 } else { 5790 require_once($CFG->libdir.'/filelib.php'); 5791 $mimetype = mimeinfo('type', $attachname); 5792 5793 $attachmentpath = $attachment; 5794 5795 // Before doing the comparison, make sure that the paths are correct (Windows uses slashes in the other direction). 5796 $attachpath = str_replace('\\', '/', $attachmentpath); 5797 // Make sure both variables are normalised before comparing. 5798 $temppath = str_replace('\\', '/', realpath($CFG->tempdir)); 5799 5800 // If the attachment is a full path to a file in the tempdir, use it as is, 5801 // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons). 5802 if (strpos($attachpath, $temppath) !== 0) { 5803 $attachmentpath = $CFG->dataroot . '/' . $attachmentpath; 5804 } 5805 5806 $mail->addAttachment($attachmentpath, $attachname, 'base64', $mimetype); 5807 } 5808 } 5809 5810 // Check if the email should be sent in an other charset then the default UTF-8. 5811 if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) { 5812 5813 // Use the defined site mail charset or eventually the one preferred by the recipient. 5814 $charset = $CFG->sitemailcharset; 5815 if (!empty($CFG->allowusermailcharset)) { 5816 if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) { 5817 $charset = $useremailcharset; 5818 } 5819 } 5820 5821 // Convert all the necessary strings if the charset is supported. 5822 $charsets = get_list_of_charsets(); 5823 unset($charsets['UTF-8']); 5824 if (in_array($charset, $charsets)) { 5825 $mail->CharSet = $charset; 5826 $mail->FromName = core_text::convert($mail->FromName, 'utf-8', strtolower($charset)); 5827 $mail->Subject = core_text::convert($mail->Subject, 'utf-8', strtolower($charset)); 5828 $mail->Body = core_text::convert($mail->Body, 'utf-8', strtolower($charset)); 5829 $mail->AltBody = core_text::convert($mail->AltBody, 'utf-8', strtolower($charset)); 5830 5831 foreach ($temprecipients as $key => $values) { 5832 $temprecipients[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset)); 5833 } 5834 foreach ($tempreplyto as $key => $values) { 5835 $tempreplyto[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset)); 5836 } 5837 } 5838 } 5839 5840 foreach ($temprecipients as $values) { 5841 $mail->addAddress($values[0], $values[1]); 5842 } 5843 foreach ($tempreplyto as $values) { 5844 $mail->addReplyTo($values[0], $values[1]); 5845 } 5846 5847 if ($mail->send()) { 5848 set_send_count($user); 5849 if (!empty($mail->SMTPDebug)) { 5850 echo '</pre>'; 5851 } 5852 return true; 5853 } else { 5854 // Trigger event for failing to send email. 5855 $event = \core\event\email_failed::create(array( 5856 'context' => context_system::instance(), 5857 'userid' => $from->id, 5858 'relateduserid' => $user->id, 5859 'other' => array( 5860 'subject' => $subject, 5861 'message' => $messagetext, 5862 'errorinfo' => $mail->ErrorInfo 5863 ) 5864 )); 5865 $event->trigger(); 5866 if (CLI_SCRIPT) { 5867 mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo); 5868 } 5869 if (!empty($mail->SMTPDebug)) { 5870 echo '</pre>'; 5871 } 5872 return false; 5873 } 5874 } 5875 5876 /** 5877 * Generate a signoff for emails based on support settings 5878 * 5879 * @return string 5880 */ 5881 function generate_email_signoff() { 5882 global $CFG; 5883 5884 $signoff = "\n"; 5885 if (!empty($CFG->supportname)) { 5886 $signoff .= $CFG->supportname."\n"; 5887 } 5888 if (!empty($CFG->supportemail)) { 5889 $signoff .= $CFG->supportemail."\n"; 5890 } 5891 if (!empty($CFG->supportpage)) { 5892 $signoff .= $CFG->supportpage."\n"; 5893 } 5894 return $signoff; 5895 } 5896 5897 /** 5898 * Sets specified user's password and send the new password to the user via email. 5899 * 5900 * @param stdClass $user A {@link $USER} object 5901 * @param bool $fasthash If true, use a low cost factor when generating the hash for speed. 5902 * @return bool|string Returns "true" if mail was sent OK and "false" if there was an error 5903 */ 5904 function setnew_password_and_mail($user, $fasthash = false) { 5905 global $CFG, $DB; 5906 5907 // We try to send the mail in language the user understands, 5908 // unfortunately the filter_string() does not support alternative langs yet 5909 // so multilang will not work properly for site->fullname. 5910 $lang = empty($user->lang) ? $CFG->lang : $user->lang; 5911 5912 $site = get_site(); 5913 5914 $supportuser = core_user::get_support_user(); 5915 5916 $newpassword = generate_password(); 5917 5918 update_internal_user_password($user, $newpassword, $fasthash); 5919 5920 $a = new stdClass(); 5921 $a->firstname = fullname($user, true); 5922 $a->sitename = format_string($site->fullname); 5923 $a->username = $user->username; 5924 $a->newpassword = $newpassword; 5925 $a->link = $CFG->wwwroot .'/login/'; 5926 $a->signoff = generate_email_signoff(); 5927 5928 $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang); 5929 5930 $subject = format_string($site->fullname) .': '. (string)new lang_string('newusernewpasswordsubj', '', $a, $lang); 5931 5932 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 5933 return email_to_user($user, $supportuser, $subject, $message); 5934 5935 } 5936 5937 /** 5938 * Resets specified user's password and send the new password to the user via email. 5939 * 5940 * @param stdClass $user A {@link $USER} object 5941 * @return bool Returns true if mail was sent OK and false if there was an error. 5942 */ 5943 function reset_password_and_mail($user) { 5944 global $CFG; 5945 5946 $site = get_site(); 5947 $supportuser = core_user::get_support_user(); 5948 5949 $userauth = get_auth_plugin($user->auth); 5950 if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) { 5951 trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth."); 5952 return false; 5953 } 5954 5955 $newpassword = generate_password(); 5956 5957 if (!$userauth->user_update_password($user, $newpassword)) { 5958 print_error("cannotsetpassword"); 5959 } 5960 5961 $a = new stdClass(); 5962 $a->firstname = $user->firstname; 5963 $a->lastname = $user->lastname; 5964 $a->sitename = format_string($site->fullname); 5965 $a->username = $user->username; 5966 $a->newpassword = $newpassword; 5967 $a->link = $CFG->httpswwwroot .'/login/change_password.php'; 5968 $a->signoff = generate_email_signoff(); 5969 5970 $message = get_string('newpasswordtext', '', $a); 5971 5972 $subject = format_string($site->fullname) .': '. get_string('changedpassword'); 5973 5974 unset_user_preference('create_password', $user); // Prevent cron from generating the password. 5975 5976 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 5977 return email_to_user($user, $supportuser, $subject, $message); 5978 } 5979 5980 /** 5981 * Send email to specified user with confirmation text and activation link. 5982 * 5983 * @param stdClass $user A {@link $USER} object 5984 * @return bool Returns true if mail was sent OK and false if there was an error. 5985 */ 5986 function send_confirmation_email($user) { 5987 global $CFG; 5988 5989 $site = get_site(); 5990 $supportuser = core_user::get_support_user(); 5991 5992 $data = new stdClass(); 5993 $data->firstname = fullname($user); 5994 $data->sitename = format_string($site->fullname); 5995 $data->admin = generate_email_signoff(); 5996 5997 $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname)); 5998 5999 $username = urlencode($user->username); 6000 $username = str_replace('.', '%2E', $username); // Prevent problems with trailing dots. 6001 $data->link = $CFG->wwwroot .'/login/confirm.php?data='. $user->secret .'/'. $username; 6002 $message = get_string('emailconfirmation', '', $data); 6003 $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true); 6004 6005 $user->mailformat = 1; // Always send HTML version as well. 6006 6007 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 6008 return email_to_user($user, $supportuser, $subject, $message, $messagehtml); 6009 } 6010 6011 /** 6012 * Sends a password change confirmation email. 6013 * 6014 * @param stdClass $user A {@link $USER} object 6015 * @param stdClass $resetrecord An object tracking metadata regarding password reset request 6016 * @return bool Returns true if mail was sent OK and false if there was an error. 6017 */ 6018 function send_password_change_confirmation_email($user, $resetrecord) { 6019 global $CFG; 6020 6021 $site = get_site(); 6022 $supportuser = core_user::get_support_user(); 6023 $pwresetmins = isset($CFG->pwresettime) ? floor($CFG->pwresettime / MINSECS) : 30; 6024 6025 $data = new stdClass(); 6026 $data->firstname = $user->firstname; 6027 $data->lastname = $user->lastname; 6028 $data->username = $user->username; 6029 $data->sitename = format_string($site->fullname); 6030 $data->link = $CFG->httpswwwroot .'/login/forgot_password.php?token='. $resetrecord->token; 6031 $data->admin = generate_email_signoff(); 6032 $data->resetminutes = $pwresetmins; 6033 6034 $message = get_string('emailresetconfirmation', '', $data); 6035 $subject = get_string('emailresetconfirmationsubject', '', format_string($site->fullname)); 6036 6037 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 6038 return email_to_user($user, $supportuser, $subject, $message); 6039 6040 } 6041 6042 /** 6043 * Sends an email containinginformation on how to change your password. 6044 * 6045 * @param stdClass $user A {@link $USER} object 6046 * @return bool Returns true if mail was sent OK and false if there was an error. 6047 */ 6048 function send_password_change_info($user) { 6049 global $CFG; 6050 6051 $site = get_site(); 6052 $supportuser = core_user::get_support_user(); 6053 $systemcontext = context_system::instance(); 6054 6055 $data = new stdClass(); 6056 $data->firstname = $user->firstname; 6057 $data->lastname = $user->lastname; 6058 $data->sitename = format_string($site->fullname); 6059 $data->admin = generate_email_signoff(); 6060 6061 $userauth = get_auth_plugin($user->auth); 6062 6063 if (!is_enabled_auth($user->auth) or $user->auth == 'nologin') { 6064 $message = get_string('emailpasswordchangeinfodisabled', '', $data); 6065 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname)); 6066 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 6067 return email_to_user($user, $supportuser, $subject, $message); 6068 } 6069 6070 if ($userauth->can_change_password() and $userauth->change_password_url()) { 6071 // We have some external url for password changing. 6072 $data->link .= $userauth->change_password_url(); 6073 6074 } else { 6075 // No way to change password, sorry. 6076 $data->link = ''; 6077 } 6078 6079 if (!empty($data->link) and has_capability('moodle/user:changeownpassword', $systemcontext, $user->id)) { 6080 $message = get_string('emailpasswordchangeinfo', '', $data); 6081 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname)); 6082 } else { 6083 $message = get_string('emailpasswordchangeinfofail', '', $data); 6084 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname)); 6085 } 6086 6087 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 6088 return email_to_user($user, $supportuser, $subject, $message); 6089 6090 } 6091 6092 /** 6093 * Check that an email is allowed. It returns an error message if there was a problem. 6094 * 6095 * @param string $email Content of email 6096 * @return string|false 6097 */ 6098 function email_is_not_allowed($email) { 6099 global $CFG; 6100 6101 if (!empty($CFG->allowemailaddresses)) { 6102 $allowed = explode(' ', $CFG->allowemailaddresses); 6103 foreach ($allowed as $allowedpattern) { 6104 $allowedpattern = trim($allowedpattern); 6105 if (!$allowedpattern) { 6106 continue; 6107 } 6108 if (strpos($allowedpattern, '.') === 0) { 6109 if (strpos(strrev($email), strrev($allowedpattern)) === 0) { 6110 // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com". 6111 return false; 6112 } 6113 6114 } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) { 6115 return false; 6116 } 6117 } 6118 return get_string('emailonlyallowed', '', $CFG->allowemailaddresses); 6119 6120 } else if (!empty($CFG->denyemailaddresses)) { 6121 $denied = explode(' ', $CFG->denyemailaddresses); 6122 foreach ($denied as $deniedpattern) { 6123 $deniedpattern = trim($deniedpattern); 6124 if (!$deniedpattern) { 6125 continue; 6126 } 6127 if (strpos($deniedpattern, '.') === 0) { 6128 if (strpos(strrev($email), strrev($deniedpattern)) === 0) { 6129 // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com". 6130 return get_string('emailnotallowed', '', $CFG->denyemailaddresses); 6131 } 6132 6133 } else if (strpos(strrev($email), strrev('@'.$deniedpattern)) === 0) { 6134 return get_string('emailnotallowed', '', $CFG->denyemailaddresses); 6135 } 6136 } 6137 } 6138 6139 return false; 6140 } 6141 6142 // FILE HANDLING. 6143 6144 /** 6145 * Returns local file storage instance 6146 * 6147 * @return file_storage 6148 */ 6149 function get_file_storage() { 6150 global $CFG; 6151 6152 static $fs = null; 6153 6154 if ($fs) { 6155 return $fs; 6156 } 6157 6158 require_once("$CFG->libdir/filelib.php"); 6159 6160 if (isset($CFG->filedir)) { 6161 $filedir = $CFG->filedir; 6162 } else { 6163 $filedir = $CFG->dataroot.'/filedir'; 6164 } 6165 6166 if (isset($CFG->trashdir)) { 6167 $trashdirdir = $CFG->trashdir; 6168 } else { 6169 $trashdirdir = $CFG->dataroot.'/trashdir'; 6170 } 6171 6172 $fs = new file_storage($filedir, $trashdirdir, "$CFG->tempdir/filestorage", $CFG->directorypermissions, $CFG->filepermissions); 6173 6174 return $fs; 6175 } 6176 6177 /** 6178 * Returns local file storage instance 6179 * 6180 * @return file_browser 6181 */ 6182 function get_file_browser() { 6183 global $CFG; 6184 6185 static $fb = null; 6186 6187 if ($fb) { 6188 return $fb; 6189 } 6190 6191 require_once("$CFG->libdir/filelib.php"); 6192 6193 $fb = new file_browser(); 6194 6195 return $fb; 6196 } 6197 6198 /** 6199 * Returns file packer 6200 * 6201 * @param string $mimetype default application/zip 6202 * @return file_packer 6203 */ 6204 function get_file_packer($mimetype='application/zip') { 6205 global $CFG; 6206 6207 static $fp = array(); 6208 6209 if (isset($fp[$mimetype])) { 6210 return $fp[$mimetype]; 6211 } 6212 6213 switch ($mimetype) { 6214 case 'application/zip': 6215 case 'application/vnd.moodle.profiling': 6216 $classname = 'zip_packer'; 6217 break; 6218 6219 case 'application/x-gzip' : 6220 $classname = 'tgz_packer'; 6221 break; 6222 6223 case 'application/vnd.moodle.backup': 6224 $classname = 'mbz_packer'; 6225 break; 6226 6227 default: 6228 return false; 6229 } 6230 6231 require_once("$CFG->libdir/filestorage/$classname.php"); 6232 $fp[$mimetype] = new $classname(); 6233 6234 return $fp[$mimetype]; 6235 } 6236 6237 /** 6238 * Returns current name of file on disk if it exists. 6239 * 6240 * @param string $newfile File to be verified 6241 * @return string Current name of file on disk if true 6242 */ 6243 function valid_uploaded_file($newfile) { 6244 if (empty($newfile)) { 6245 return ''; 6246 } 6247 if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) { 6248 return $newfile['tmp_name']; 6249 } else { 6250 return ''; 6251 } 6252 } 6253 6254 /** 6255 * Returns the maximum size for uploading files. 6256 * 6257 * There are seven possible upload limits: 6258 * 1. in Apache using LimitRequestBody (no way of checking or changing this) 6259 * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP) 6260 * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP) 6261 * 4. in php.ini for 'post_max_size' (can not be changed inside PHP) 6262 * 5. by the Moodle admin in $CFG->maxbytes 6263 * 6. by the teacher in the current course $course->maxbytes 6264 * 7. by the teacher for the current module, eg $assignment->maxbytes 6265 * 6266 * These last two are passed to this function as arguments (in bytes). 6267 * Anything defined as 0 is ignored. 6268 * The smallest of all the non-zero numbers is returned. 6269 * 6270 * @todo Finish documenting this function 6271 * 6272 * @param int $sitebytes Set maximum size 6273 * @param int $coursebytes Current course $course->maxbytes (in bytes) 6274 * @param int $modulebytes Current module ->maxbytes (in bytes) 6275 * @param bool $unused This parameter has been deprecated and is not used any more. 6276 * @return int The maximum size for uploading files. 6277 */ 6278 function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0, $unused = false) { 6279 6280 if (! $filesize = ini_get('upload_max_filesize')) { 6281 $filesize = '5M'; 6282 } 6283 $minimumsize = get_real_size($filesize); 6284 6285 if ($postsize = ini_get('post_max_size')) { 6286 $postsize = get_real_size($postsize); 6287 if ($postsize < $minimumsize) { 6288 $minimumsize = $postsize; 6289 } 6290 } 6291 6292 if (($sitebytes > 0) and ($sitebytes < $minimumsize)) { 6293 $minimumsize = $sitebytes; 6294 } 6295 6296 if (($coursebytes > 0) and ($coursebytes < $minimumsize)) { 6297 $minimumsize = $coursebytes; 6298 } 6299 6300 if (($modulebytes > 0) and ($modulebytes < $minimumsize)) { 6301 $minimumsize = $modulebytes; 6302 } 6303 6304 return $minimumsize; 6305 } 6306 6307 /** 6308 * Returns the maximum size for uploading files for the current user 6309 * 6310 * This function takes in account {@link get_max_upload_file_size()} the user's capabilities 6311 * 6312 * @param context $context The context in which to check user capabilities 6313 * @param int $sitebytes Set maximum size 6314 * @param int $coursebytes Current course $course->maxbytes (in bytes) 6315 * @param int $modulebytes Current module ->maxbytes (in bytes) 6316 * @param stdClass $user The user 6317 * @param bool $unused This parameter has been deprecated and is not used any more. 6318 * @return int The maximum size for uploading files. 6319 */ 6320 function get_user_max_upload_file_size($context, $sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $user = null, 6321 $unused = false) { 6322 global $USER; 6323 6324 if (empty($user)) { 6325 $user = $USER; 6326 } 6327 6328 if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) { 6329 return USER_CAN_IGNORE_FILE_SIZE_LIMITS; 6330 } 6331 6332 return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes); 6333 } 6334 6335 /** 6336 * Returns an array of possible sizes in local language 6337 * 6338 * Related to {@link get_max_upload_file_size()} - this function returns an 6339 * array of possible sizes in an array, translated to the 6340 * local language. 6341 * 6342 * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes. 6343 * 6344 * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)" 6345 * with the value set to 0. This option will be the first in the list. 6346 * 6347 * @uses SORT_NUMERIC 6348 * @param int $sitebytes Set maximum size 6349 * @param int $coursebytes Current course $course->maxbytes (in bytes) 6350 * @param int $modulebytes Current module ->maxbytes (in bytes) 6351 * @param int|array $custombytes custom upload size/s which will be added to list, 6352 * Only value/s smaller then maxsize will be added to list. 6353 * @return array 6354 */ 6355 function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null) { 6356 global $CFG; 6357 6358 if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) { 6359 return array(); 6360 } 6361 6362 if ($sitebytes == 0) { 6363 // Will get the minimum of upload_max_filesize or post_max_size. 6364 $sitebytes = get_max_upload_file_size(); 6365 } 6366 6367 $filesize = array(); 6368 $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152, 6369 5242880, 10485760, 20971520, 52428800, 104857600); 6370 6371 // If custombytes is given and is valid then add it to the list. 6372 if (is_number($custombytes) and $custombytes > 0) { 6373 $custombytes = (int)$custombytes; 6374 if (!in_array($custombytes, $sizelist)) { 6375 $sizelist[] = $custombytes; 6376 } 6377 } else if (is_array($custombytes)) { 6378 $sizelist = array_unique(array_merge($sizelist, $custombytes)); 6379 } 6380 6381 // Allow maxbytes to be selected if it falls outside the above boundaries. 6382 if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) { 6383 // Note: get_real_size() is used in order to prevent problems with invalid values. 6384 $sizelist[] = get_real_size($CFG->maxbytes); 6385 } 6386 6387 foreach ($sizelist as $sizebytes) { 6388 if ($sizebytes < $maxsize && $sizebytes > 0) { 6389 $filesize[(string)intval($sizebytes)] = display_size($sizebytes); 6390 } 6391 } 6392 6393 $limitlevel = ''; 6394 $displaysize = ''; 6395 if ($modulebytes && 6396 (($modulebytes < $coursebytes || $coursebytes == 0) && 6397 ($modulebytes < $sitebytes || $sitebytes == 0))) { 6398 $limitlevel = get_string('activity', 'core'); 6399 $displaysize = display_size($modulebytes); 6400 $filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list. 6401 6402 } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) { 6403 $limitlevel = get_string('course', 'core'); 6404 $displaysize = display_size($coursebytes); 6405 $filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list. 6406 6407 } else if ($sitebytes) { 6408 $limitlevel = get_string('site', 'core'); 6409 $displaysize = display_size($sitebytes); 6410 $filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list. 6411 } 6412 6413 krsort($filesize, SORT_NUMERIC); 6414 if ($limitlevel) { 6415 $params = (object) array('contextname' => $limitlevel, 'displaysize' => $displaysize); 6416 $filesize = array('0' => get_string('uploadlimitwithsize', 'core', $params)) + $filesize; 6417 } 6418 6419 return $filesize; 6420 } 6421 6422 /** 6423 * Returns an array with all the filenames in all subdirectories, relative to the given rootdir. 6424 * 6425 * If excludefiles is defined, then that file/directory is ignored 6426 * If getdirs is true, then (sub)directories are included in the output 6427 * If getfiles is true, then files are included in the output 6428 * (at least one of these must be true!) 6429 * 6430 * @todo Finish documenting this function. Add examples of $excludefile usage. 6431 * 6432 * @param string $rootdir A given root directory to start from 6433 * @param string|array $excludefiles If defined then the specified file/directory is ignored 6434 * @param bool $descend If true then subdirectories are recursed as well 6435 * @param bool $getdirs If true then (sub)directories are included in the output 6436 * @param bool $getfiles If true then files are included in the output 6437 * @return array An array with all the filenames in all subdirectories, relative to the given rootdir 6438 */ 6439 function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) { 6440 6441 $dirs = array(); 6442 6443 if (!$getdirs and !$getfiles) { // Nothing to show. 6444 return $dirs; 6445 } 6446 6447 if (!is_dir($rootdir)) { // Must be a directory. 6448 return $dirs; 6449 } 6450 6451 if (!$dir = opendir($rootdir)) { // Can't open it for some reason. 6452 return $dirs; 6453 } 6454 6455 if (!is_array($excludefiles)) { 6456 $excludefiles = array($excludefiles); 6457 } 6458 6459 while (false !== ($file = readdir($dir))) { 6460 $firstchar = substr($file, 0, 1); 6461 if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) { 6462 continue; 6463 } 6464 $fullfile = $rootdir .'/'. $file; 6465 if (filetype($fullfile) == 'dir') { 6466 if ($getdirs) { 6467 $dirs[] = $file; 6468 } 6469 if ($descend) { 6470 $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles); 6471 foreach ($subdirs as $subdir) { 6472 $dirs[] = $file .'/'. $subdir; 6473 } 6474 } 6475 } else if ($getfiles) { 6476 $dirs[] = $file; 6477 } 6478 } 6479 closedir($dir); 6480 6481 asort($dirs); 6482 6483 return $dirs; 6484 } 6485 6486 6487 /** 6488 * Adds up all the files in a directory and works out the size. 6489 * 6490 * @param string $rootdir The directory to start from 6491 * @param string $excludefile A file to exclude when summing directory size 6492 * @return int The summed size of all files and subfiles within the root directory 6493 */ 6494 function get_directory_size($rootdir, $excludefile='') { 6495 global $CFG; 6496 6497 // Do it this way if we can, it's much faster. 6498 if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) { 6499 $command = trim($CFG->pathtodu).' -sk '.escapeshellarg($rootdir); 6500 $output = null; 6501 $return = null; 6502 exec($command, $output, $return); 6503 if (is_array($output)) { 6504 // We told it to return k. 6505 return get_real_size(intval($output[0]).'k'); 6506 } 6507 } 6508 6509 if (!is_dir($rootdir)) { 6510 // Must be a directory. 6511 return 0; 6512 } 6513 6514 if (!$dir = @opendir($rootdir)) { 6515 // Can't open it for some reason. 6516 return 0; 6517 } 6518 6519 $size = 0; 6520 6521 while (false !== ($file = readdir($dir))) { 6522 $firstchar = substr($file, 0, 1); 6523 if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) { 6524 continue; 6525 } 6526 $fullfile = $rootdir .'/'. $file; 6527 if (filetype($fullfile) == 'dir') { 6528 $size += get_directory_size($fullfile, $excludefile); 6529 } else { 6530 $size += filesize($fullfile); 6531 } 6532 } 6533 closedir($dir); 6534 6535 return $size; 6536 } 6537 6538 /** 6539 * Converts bytes into display form 6540 * 6541 * @static string $gb Localized string for size in gigabytes 6542 * @static string $mb Localized string for size in megabytes 6543 * @static string $kb Localized string for size in kilobytes 6544 * @static string $b Localized string for size in bytes 6545 * @param int $size The size to convert to human readable form 6546 * @return string 6547 */ 6548 function display_size($size) { 6549 6550 static $gb, $mb, $kb, $b; 6551 6552 if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) { 6553 return get_string('unlimited'); 6554 } 6555 6556 if (empty($gb)) { 6557 $gb = get_string('sizegb'); 6558 $mb = get_string('sizemb'); 6559 $kb = get_string('sizekb'); 6560 $b = get_string('sizeb'); 6561 } 6562 6563 if ($size >= 1073741824) { 6564 $size = round($size / 1073741824 * 10) / 10 . $gb; 6565 } else if ($size >= 1048576) { 6566 $size = round($size / 1048576 * 10) / 10 . $mb; 6567 } else if ($size >= 1024) { 6568 $size = round($size / 1024 * 10) / 10 . $kb; 6569 } else { 6570 $size = intval($size) .' '. $b; // File sizes over 2GB can not work in 32bit PHP anyway. 6571 } 6572 return $size; 6573 } 6574 6575 /** 6576 * Cleans a given filename by removing suspicious or troublesome characters 6577 * 6578 * @see clean_param() 6579 * @param string $string file name 6580 * @return string cleaned file name 6581 */ 6582 function clean_filename($string) { 6583 return clean_param($string, PARAM_FILE); 6584 } 6585 6586 6587 // STRING TRANSLATION. 6588 6589 /** 6590 * Returns the code for the current language 6591 * 6592 * @category string 6593 * @return string 6594 */ 6595 function current_language() { 6596 global $CFG, $USER, $SESSION, $COURSE; 6597 6598 if (!empty($SESSION->forcelang)) { 6599 // Allows overriding course-forced language (useful for admins to check 6600 // issues in courses whose language they don't understand). 6601 // Also used by some code to temporarily get language-related information in a 6602 // specific language (see force_current_language()). 6603 $return = $SESSION->forcelang; 6604 6605 } else if (!empty($COURSE->id) and $COURSE->id != SITEID and !empty($COURSE->lang)) { 6606 // Course language can override all other settings for this page. 6607 $return = $COURSE->lang; 6608 6609 } else if (!empty($SESSION->lang)) { 6610 // Session language can override other settings. 6611 $return = $SESSION->lang; 6612 6613 } else if (!empty($USER->lang)) { 6614 $return = $USER->lang; 6615 6616 } else if (isset($CFG->lang)) { 6617 $return = $CFG->lang; 6618 6619 } else { 6620 $return = 'en'; 6621 } 6622 6623 // Just in case this slipped in from somewhere by accident. 6624 $return = str_replace('_utf8', '', $return); 6625 6626 return $return; 6627 } 6628 6629 /** 6630 * Returns parent language of current active language if defined 6631 * 6632 * @category string 6633 * @param string $lang null means current language 6634 * @return string 6635 */ 6636 function get_parent_language($lang=null) { 6637 6638 // Let's hack around the current language. 6639 if (!empty($lang)) { 6640 $oldforcelang = force_current_language($lang); 6641 } 6642 6643 $parentlang = get_string('parentlanguage', 'langconfig'); 6644 if ($parentlang === 'en') { 6645 $parentlang = ''; 6646 } 6647 6648 // Let's hack around the current language. 6649 if (!empty($lang)) { 6650 force_current_language($oldforcelang); 6651 } 6652 6653 return $parentlang; 6654 } 6655 6656 /** 6657 * Force the current language to get strings and dates localised in the given language. 6658 * 6659 * After calling this function, all strings will be provided in the given language 6660 * until this function is called again, or equivalent code is run. 6661 * 6662 * @param string $language 6663 * @return string previous $SESSION->forcelang value 6664 */ 6665 function force_current_language($language) { 6666 global $SESSION; 6667 $sessionforcelang = isset($SESSION->forcelang) ? $SESSION->forcelang : ''; 6668 if ($language !== $sessionforcelang) { 6669 // Seting forcelang to null or an empty string disables it's effect. 6670 if (empty($language) || get_string_manager()->translation_exists($language, false)) { 6671 $SESSION->forcelang = $language; 6672 moodle_setlocale(); 6673 } 6674 } 6675 return $sessionforcelang; 6676 } 6677 6678 /** 6679 * Returns current string_manager instance. 6680 * 6681 * The param $forcereload is needed for CLI installer only where the string_manager instance 6682 * must be replaced during the install.php script life time. 6683 * 6684 * @category string 6685 * @param bool $forcereload shall the singleton be released and new instance created instead? 6686 * @return core_string_manager 6687 */ 6688 function get_string_manager($forcereload=false) { 6689 global $CFG; 6690 6691 static $singleton = null; 6692 6693 if ($forcereload) { 6694 $singleton = null; 6695 } 6696 if ($singleton === null) { 6697 if (empty($CFG->early_install_lang)) { 6698 6699 if (empty($CFG->langlist)) { 6700 $translist = array(); 6701 } else { 6702 $translist = explode(',', $CFG->langlist); 6703 } 6704 6705 if (!empty($CFG->config_php_settings['customstringmanager'])) { 6706 $classname = $CFG->config_php_settings['customstringmanager']; 6707 6708 if (class_exists($classname)) { 6709 $implements = class_implements($classname); 6710 6711 if (isset($implements['core_string_manager'])) { 6712 $singleton = new $classname($CFG->langotherroot, $CFG->langlocalroot, $translist); 6713 return $singleton; 6714 6715 } else { 6716 debugging('Unable to instantiate custom string manager: class '.$classname. 6717 ' does not implement the core_string_manager interface.'); 6718 } 6719 6720 } else { 6721 debugging('Unable to instantiate custom string manager: class '.$classname.' can not be found.'); 6722 } 6723 } 6724 6725 $singleton = new core_string_manager_standard($CFG->langotherroot, $CFG->langlocalroot, $translist); 6726 6727 } else { 6728 $singleton = new core_string_manager_install(); 6729 } 6730 } 6731 6732 return $singleton; 6733 } 6734 6735 /** 6736 * Returns a localized string. 6737 * 6738 * Returns the translated string specified by $identifier as 6739 * for $module. Uses the same format files as STphp. 6740 * $a is an object, string or number that can be used 6741 * within translation strings 6742 * 6743 * eg 'hello {$a->firstname} {$a->lastname}' 6744 * or 'hello {$a}' 6745 * 6746 * If you would like to directly echo the localized string use 6747 * the function {@link print_string()} 6748 * 6749 * Example usage of this function involves finding the string you would 6750 * like a local equivalent of and using its identifier and module information 6751 * to retrieve it.<br/> 6752 * If you open moodle/lang/en/moodle.php and look near line 278 6753 * you will find a string to prompt a user for their word for 'course' 6754 * <code> 6755 * $string['course'] = 'Course'; 6756 * </code> 6757 * So if you want to display the string 'Course' 6758 * in any language that supports it on your site 6759 * you just need to use the identifier 'course' 6760 * <code> 6761 * $mystring = '<strong>'. get_string('course') .'</strong>'; 6762 * or 6763 * </code> 6764 * If the string you want is in another file you'd take a slightly 6765 * different approach. Looking in moodle/lang/en/calendar.php you find 6766 * around line 75: 6767 * <code> 6768 * $string['typecourse'] = 'Course event'; 6769 * </code> 6770 * If you want to display the string "Course event" in any language 6771 * supported you would use the identifier 'typecourse' and the module 'calendar' 6772 * (because it is in the file calendar.php): 6773 * <code> 6774 * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>'; 6775 * </code> 6776 * 6777 * As a last resort, should the identifier fail to map to a string 6778 * the returned string will be [[ $identifier ]] 6779 * 6780 * In Moodle 2.3 there is a new argument to this function $lazyload. 6781 * Setting $lazyload to true causes get_string to return a lang_string object 6782 * rather than the string itself. The fetching of the string is then put off until 6783 * the string object is first used. The object can be used by calling it's out 6784 * method or by casting the object to a string, either directly e.g. 6785 * (string)$stringobject 6786 * or indirectly by using the string within another string or echoing it out e.g. 6787 * echo $stringobject 6788 * return "<p>{$stringobject}</p>"; 6789 * It is worth noting that using $lazyload and attempting to use the string as an 6790 * array key will cause a fatal error as objects cannot be used as array keys. 6791 * But you should never do that anyway! 6792 * For more information {@link lang_string} 6793 * 6794 * @category string 6795 * @param string $identifier The key identifier for the localized string 6796 * @param string $component The module where the key identifier is stored, 6797 * usually expressed as the filename in the language pack without the 6798 * .php on the end but can also be written as mod/forum or grade/export/xls. 6799 * If none is specified then moodle.php is used. 6800 * @param string|object|array $a An object, string or number that can be used 6801 * within translation strings 6802 * @param bool $lazyload If set to true a string object is returned instead of 6803 * the string itself. The string then isn't calculated until it is first used. 6804 * @return string The localized string. 6805 * @throws coding_exception 6806 */ 6807 function get_string($identifier, $component = '', $a = null, $lazyload = false) { 6808 global $CFG; 6809 6810 // If the lazy load argument has been supplied return a lang_string object 6811 // instead. 6812 // We need to make sure it is true (and a bool) as you will see below there 6813 // used to be a forth argument at one point. 6814 if ($lazyload === true) { 6815 return new lang_string($identifier, $component, $a); 6816 } 6817 6818 if ($CFG->debugdeveloper && clean_param($identifier, PARAM_STRINGID) === '') { 6819 throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.', DEBUG_DEVELOPER); 6820 } 6821 6822 // There is now a forth argument again, this time it is a boolean however so 6823 // we can still check for the old extralocations parameter. 6824 if (!is_bool($lazyload) && !empty($lazyload)) { 6825 debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.'); 6826 } 6827 6828 if (strpos($component, '/') !== false) { 6829 debugging('The module name you passed to get_string is the deprecated format ' . 6830 'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER); 6831 $componentpath = explode('/', $component); 6832 6833 switch ($componentpath[0]) { 6834 case 'mod': 6835 $component = $componentpath[1]; 6836 break; 6837 case 'blocks': 6838 case 'block': 6839 $component = 'block_'.$componentpath[1]; 6840 break; 6841 case 'enrol': 6842 $component = 'enrol_'.$componentpath[1]; 6843 break; 6844 case 'format': 6845 $component = 'format_'.$componentpath[1]; 6846 break; 6847 case 'grade': 6848 $component = 'grade'.$componentpath[1].'_'.$componentpath[2]; 6849 break; 6850 } 6851 } 6852 6853 $result = get_string_manager()->get_string($identifier, $component, $a); 6854 6855 // Debugging feature lets you display string identifier and component. 6856 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) { 6857 $result .= ' {' . $identifier . '/' . $component . '}'; 6858 } 6859 return $result; 6860 } 6861 6862 /** 6863 * Converts an array of strings to their localized value. 6864 * 6865 * @param array $array An array of strings 6866 * @param string $component The language module that these strings can be found in. 6867 * @return stdClass translated strings. 6868 */ 6869 function get_strings($array, $component = '') { 6870 $string = new stdClass; 6871 foreach ($array as $item) { 6872 $string->$item = get_string($item, $component); 6873 } 6874 return $string; 6875 } 6876 6877 /** 6878 * Prints out a translated string. 6879 * 6880 * Prints out a translated string using the return value from the {@link get_string()} function. 6881 * 6882 * Example usage of this function when the string is in the moodle.php file:<br/> 6883 * <code> 6884 * echo '<strong>'; 6885 * print_string('course'); 6886 * echo '</strong>'; 6887 * </code> 6888 * 6889 * Example usage of this function when the string is not in the moodle.php file:<br/> 6890 * <code> 6891 * echo '<h1>'; 6892 * print_string('typecourse', 'calendar'); 6893 * echo '</h1>'; 6894 * </code> 6895 * 6896 * @category string 6897 * @param string $identifier The key identifier for the localized string 6898 * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used. 6899 * @param string|object|array $a An object, string or number that can be used within translation strings 6900 */ 6901 function print_string($identifier, $component = '', $a = null) { 6902 echo get_string($identifier, $component, $a); 6903 } 6904 6905 /** 6906 * Returns a list of charset codes 6907 * 6908 * Returns a list of charset codes. It's hardcoded, so they should be added manually 6909 * (checking that such charset is supported by the texlib library!) 6910 * 6911 * @return array And associative array with contents in the form of charset => charset 6912 */ 6913 function get_list_of_charsets() { 6914 6915 $charsets = array( 6916 'EUC-JP' => 'EUC-JP', 6917 'ISO-2022-JP'=> 'ISO-2022-JP', 6918 'ISO-8859-1' => 'ISO-8859-1', 6919 'SHIFT-JIS' => 'SHIFT-JIS', 6920 'GB2312' => 'GB2312', 6921 'GB18030' => 'GB18030', // GB18030 not supported by typo and mbstring. 6922 'UTF-8' => 'UTF-8'); 6923 6924 asort($charsets); 6925 6926 return $charsets; 6927 } 6928 6929 /** 6930 * Returns a list of valid and compatible themes 6931 * 6932 * @return array 6933 */ 6934 function get_list_of_themes() { 6935 global $CFG; 6936 6937 $themes = array(); 6938 6939 if (!empty($CFG->themelist)) { // Use admin's list of themes. 6940 $themelist = explode(',', $CFG->themelist); 6941 } else { 6942 $themelist = array_keys(core_component::get_plugin_list("theme")); 6943 } 6944 6945 foreach ($themelist as $key => $themename) { 6946 $theme = theme_config::load($themename); 6947 $themes[$themename] = $theme; 6948 } 6949 6950 core_collator::asort_objects_by_method($themes, 'get_theme_name'); 6951 6952 return $themes; 6953 } 6954 6955 /** 6956 * Factory function for emoticon_manager 6957 * 6958 * @return emoticon_manager singleton 6959 */ 6960 function get_emoticon_manager() { 6961 static $singleton = null; 6962 6963 if (is_null($singleton)) { 6964 $singleton = new emoticon_manager(); 6965 } 6966 6967 return $singleton; 6968 } 6969 6970 /** 6971 * Provides core support for plugins that have to deal with emoticons (like HTML editor or emoticon filter). 6972 * 6973 * Whenever this manager mentiones 'emoticon object', the following data 6974 * structure is expected: stdClass with properties text, imagename, imagecomponent, 6975 * altidentifier and altcomponent 6976 * 6977 * @see admin_setting_emoticons 6978 * 6979 * @copyright 2010 David Mudrak 6980 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 6981 */ 6982 class emoticon_manager { 6983 6984 /** 6985 * Returns the currently enabled emoticons 6986 * 6987 * @return array of emoticon objects 6988 */ 6989 public function get_emoticons() { 6990 global $CFG; 6991 6992 if (empty($CFG->emoticons)) { 6993 return array(); 6994 } 6995 6996 $emoticons = $this->decode_stored_config($CFG->emoticons); 6997 6998 if (!is_array($emoticons)) { 6999 // Something is wrong with the format of stored setting. 7000 debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL); 7001 return array(); 7002 } 7003 7004 return $emoticons; 7005 } 7006 7007 /** 7008 * Converts emoticon object into renderable pix_emoticon object 7009 * 7010 * @param stdClass $emoticon emoticon object 7011 * @param array $attributes explicit HTML attributes to set 7012 * @return pix_emoticon 7013 */ 7014 public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array()) { 7015 $stringmanager = get_string_manager(); 7016 if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) { 7017 $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent); 7018 } else { 7019 $alt = s($emoticon->text); 7020 } 7021 return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes); 7022 } 7023 7024 /** 7025 * Encodes the array of emoticon objects into a string storable in config table 7026 * 7027 * @see self::decode_stored_config() 7028 * @param array $emoticons array of emtocion objects 7029 * @return string 7030 */ 7031 public function encode_stored_config(array $emoticons) { 7032 return json_encode($emoticons); 7033 } 7034 7035 /** 7036 * Decodes the string into an array of emoticon objects 7037 * 7038 * @see self::encode_stored_config() 7039 * @param string $encoded 7040 * @return string|null 7041 */ 7042 public function decode_stored_config($encoded) { 7043 $decoded = json_decode($encoded); 7044 if (!is_array($decoded)) { 7045 return null; 7046 } 7047 return $decoded; 7048 } 7049 7050 /** 7051 * Returns default set of emoticons supported by Moodle 7052 * 7053 * @return array of sdtClasses 7054 */ 7055 public function default_emoticons() { 7056 return array( 7057 $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'), 7058 $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'), 7059 $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'), 7060 $this->prepare_emoticon_object(";-)", 's/wink', 'wink'), 7061 $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'), 7062 $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'), 7063 $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'), 7064 $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'), 7065 $this->prepare_emoticon_object("B-)", 's/cool', 'cool'), 7066 $this->prepare_emoticon_object("^-)", 's/approve', 'approve'), 7067 $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'), 7068 $this->prepare_emoticon_object(":o)", 's/clown', 'clown'), 7069 $this->prepare_emoticon_object(":-(", 's/sad', 'sad'), 7070 $this->prepare_emoticon_object(":(", 's/sad', 'sad'), 7071 $this->prepare_emoticon_object("8-.", 's/shy', 'shy'), 7072 $this->prepare_emoticon_object(":-I", 's/blush', 'blush'), 7073 $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'), 7074 $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'), 7075 $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'), 7076 $this->prepare_emoticon_object("8-[", 's/angry', 'angry'), 7077 $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'), 7078 $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'), 7079 $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'), 7080 $this->prepare_emoticon_object("}-]", 's/evil', 'evil'), 7081 $this->prepare_emoticon_object("(h)", 's/heart', 'heart'), 7082 $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'), 7083 $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'), 7084 $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'), 7085 $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'), 7086 $this->prepare_emoticon_object("( )", 's/egg', 'egg'), 7087 ); 7088 } 7089 7090 /** 7091 * Helper method preparing the stdClass with the emoticon properties 7092 * 7093 * @param string|array $text or array of strings 7094 * @param string $imagename to be used by {@link pix_emoticon} 7095 * @param string $altidentifier alternative string identifier, null for no alt 7096 * @param string $altcomponent where the alternative string is defined 7097 * @param string $imagecomponent to be used by {@link pix_emoticon} 7098 * @return stdClass 7099 */ 7100 protected function prepare_emoticon_object($text, $imagename, $altidentifier = null, 7101 $altcomponent = 'core_pix', $imagecomponent = 'core') { 7102 return (object)array( 7103 'text' => $text, 7104 'imagename' => $imagename, 7105 'imagecomponent' => $imagecomponent, 7106 'altidentifier' => $altidentifier, 7107 'altcomponent' => $altcomponent, 7108 ); 7109 } 7110 } 7111 7112 // ENCRYPTION. 7113 7114 /** 7115 * rc4encrypt 7116 * 7117 * @param string $data Data to encrypt. 7118 * @return string The now encrypted data. 7119 */ 7120 function rc4encrypt($data) { 7121 return endecrypt(get_site_identifier(), $data, ''); 7122 } 7123 7124 /** 7125 * rc4decrypt 7126 * 7127 * @param string $data Data to decrypt. 7128 * @return string The now decrypted data. 7129 */ 7130 function rc4decrypt($data) { 7131 return endecrypt(get_site_identifier(), $data, 'de'); 7132 } 7133 7134 /** 7135 * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com] 7136 * 7137 * @todo Finish documenting this function 7138 * 7139 * @param string $pwd The password to use when encrypting or decrypting 7140 * @param string $data The data to be decrypted/encrypted 7141 * @param string $case Either 'de' for decrypt or '' for encrypt 7142 * @return string 7143 */ 7144 function endecrypt ($pwd, $data, $case) { 7145 7146 if ($case == 'de') { 7147 $data = urldecode($data); 7148 } 7149 7150 $key[] = ''; 7151 $box[] = ''; 7152 $pwdlength = strlen($pwd); 7153 7154 for ($i = 0; $i <= 255; $i++) { 7155 $key[$i] = ord(substr($pwd, ($i % $pwdlength), 1)); 7156 $box[$i] = $i; 7157 } 7158 7159 $x = 0; 7160 7161 for ($i = 0; $i <= 255; $i++) { 7162 $x = ($x + $box[$i] + $key[$i]) % 256; 7163 $tempswap = $box[$i]; 7164 $box[$i] = $box[$x]; 7165 $box[$x] = $tempswap; 7166 } 7167 7168 $cipher = ''; 7169 7170 $a = 0; 7171 $j = 0; 7172 7173 for ($i = 0; $i < strlen($data); $i++) { 7174 $a = ($a + 1) % 256; 7175 $j = ($j + $box[$a]) % 256; 7176 $temp = $box[$a]; 7177 $box[$a] = $box[$j]; 7178 $box[$j] = $temp; 7179 $k = $box[(($box[$a] + $box[$j]) % 256)]; 7180 $cipherby = ord(substr($data, $i, 1)) ^ $k; 7181 $cipher .= chr($cipherby); 7182 } 7183 7184 if ($case == 'de') { 7185 $cipher = urldecode(urlencode($cipher)); 7186 } else { 7187 $cipher = urlencode($cipher); 7188 } 7189 7190 return $cipher; 7191 } 7192 7193 // ENVIRONMENT CHECKING. 7194 7195 /** 7196 * This method validates a plug name. It is much faster than calling clean_param. 7197 * 7198 * @param string $name a string that might be a plugin name. 7199 * @return bool if this string is a valid plugin name. 7200 */ 7201 function is_valid_plugin_name($name) { 7202 // This does not work for 'mod', bad luck, use any other type. 7203 return core_component::is_valid_plugin_name('tool', $name); 7204 } 7205 7206 /** 7207 * Get a list of all the plugins of a given type that define a certain API function 7208 * in a certain file. The plugin component names and function names are returned. 7209 * 7210 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'. 7211 * @param string $function the part of the name of the function after the 7212 * frankenstyle prefix. e.g 'hook' if you are looking for functions with 7213 * names like report_courselist_hook. 7214 * @param string $file the name of file within the plugin that defines the 7215 * function. Defaults to lib.php. 7216 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum') 7217 * and the function names as values (e.g. 'report_courselist_hook', 'forum_hook'). 7218 */ 7219 function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php') { 7220 global $CFG; 7221 7222 // We don't include here as all plugin types files would be included. 7223 $plugins = get_plugins_with_function($function, $file, false); 7224 7225 if (empty($plugins[$plugintype])) { 7226 return array(); 7227 } 7228 7229 $allplugins = core_component::get_plugin_list($plugintype); 7230 7231 // Reformat the array and include the files. 7232 $pluginfunctions = array(); 7233 foreach ($plugins[$plugintype] as $pluginname => $functionname) { 7234 7235 // Check that it has not been removed and the file is still available. 7236 if (!empty($allplugins[$pluginname])) { 7237 7238 $filepath = $allplugins[$pluginname] . DIRECTORY_SEPARATOR . $file; 7239 if (file_exists($filepath)) { 7240 include_once($filepath); 7241 $pluginfunctions[$plugintype . '_' . $pluginname] = $functionname; 7242 } 7243 } 7244 } 7245 7246 return $pluginfunctions; 7247 } 7248 7249 /** 7250 * Get a list of all the plugins that define a certain API function in a certain file. 7251 * 7252 * @param string $function the part of the name of the function after the 7253 * frankenstyle prefix. e.g 'hook' if you are looking for functions with 7254 * names like report_courselist_hook. 7255 * @param string $file the name of file within the plugin that defines the 7256 * function. Defaults to lib.php. 7257 * @param bool $include Whether to include the files that contain the functions or not. 7258 * @return array with [plugintype][plugin] = functionname 7259 */ 7260 function get_plugins_with_function($function, $file = 'lib.php', $include = true) { 7261 global $CFG; 7262 7263 $cache = \cache::make('core', 'plugin_functions'); 7264 7265 // Including both although I doubt that we will find two functions definitions with the same name. 7266 // Clearning the filename as cache_helper::hash_key only allows a-zA-Z0-9_. 7267 $key = $function . '_' . clean_param($file, PARAM_ALPHA); 7268 7269 if ($pluginfunctions = $cache->get($key)) { 7270 7271 // Checking that the files are still available. 7272 foreach ($pluginfunctions as $plugintype => $plugins) { 7273 7274 $allplugins = \core_component::get_plugin_list($plugintype); 7275 foreach ($plugins as $plugin => $fullpath) { 7276 7277 // Cache might be out of sync with the codebase, skip the plugin if it is not available. 7278 if (empty($allplugins[$plugin])) { 7279 unset($pluginfunctions[$plugintype][$plugin]); 7280 continue; 7281 } 7282 7283 $fileexists = file_exists($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file); 7284 if ($include && $fileexists) { 7285 // Include the files if it was requested. 7286 include_once($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file); 7287 } else if (!$fileexists) { 7288 // If the file is not available any more it should not be returned. 7289 unset($pluginfunctions[$plugintype][$plugin]); 7290 } 7291 } 7292 } 7293 return $pluginfunctions; 7294 } 7295 7296 $pluginfunctions = array(); 7297 7298 // To fill the cached. Also, everything should continue working with cache disabled. 7299 $plugintypes = \core_component::get_plugin_types(); 7300 foreach ($plugintypes as $plugintype => $unused) { 7301 7302 // We need to include files here. 7303 $pluginswithfile = \core_component::get_plugin_list_with_file($plugintype, $file, true); 7304 foreach ($pluginswithfile as $plugin => $notused) { 7305 7306 $fullfunction = $plugintype . '_' . $plugin . '_' . $function; 7307 7308 $pluginfunction = false; 7309 if (function_exists($fullfunction)) { 7310 // Function exists with standard name. Store, indexed by frankenstyle name of plugin. 7311 $pluginfunction = $fullfunction; 7312 7313 } else if ($plugintype === 'mod') { 7314 // For modules, we also allow plugin without full frankenstyle but just starting with the module name. 7315 $shortfunction = $plugin . '_' . $function; 7316 if (function_exists($shortfunction)) { 7317 $pluginfunction = $shortfunction; 7318 } 7319 } 7320 7321 if ($pluginfunction) { 7322 if (empty($pluginfunctions[$plugintype])) { 7323 $pluginfunctions[$plugintype] = array(); 7324 } 7325 $pluginfunctions[$plugintype][$plugin] = $pluginfunction; 7326 } 7327 7328 } 7329 } 7330 $cache->set($key, $pluginfunctions); 7331 7332 return $pluginfunctions; 7333 7334 } 7335 7336 /** 7337 * Lists plugin-like directories within specified directory 7338 * 7339 * This function was originally used for standard Moodle plugins, please use 7340 * new core_component::get_plugin_list() now. 7341 * 7342 * This function is used for general directory listing and backwards compatility. 7343 * 7344 * @param string $directory relative directory from root 7345 * @param string $exclude dir name to exclude from the list (defaults to none) 7346 * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot) 7347 * @return array Sorted array of directory names found under the requested parameters 7348 */ 7349 function get_list_of_plugins($directory='mod', $exclude='', $basedir='') { 7350 global $CFG; 7351 7352 $plugins = array(); 7353 7354 if (empty($basedir)) { 7355 $basedir = $CFG->dirroot .'/'. $directory; 7356 7357 } else { 7358 $basedir = $basedir .'/'. $directory; 7359 } 7360 7361 if ($CFG->debugdeveloper and empty($exclude)) { 7362 // Make sure devs do not use this to list normal plugins, 7363 // this is intended for general directories that are not plugins! 7364 7365 $subtypes = core_component::get_plugin_types(); 7366 if (in_array($basedir, $subtypes)) { 7367 debugging('get_list_of_plugins() should not be used to list real plugins, use core_component::get_plugin_list() instead!', DEBUG_DEVELOPER); 7368 } 7369 unset($subtypes); 7370 } 7371 7372 if (file_exists($basedir) && filetype($basedir) == 'dir') { 7373 if (!$dirhandle = opendir($basedir)) { 7374 debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER); 7375 return array(); 7376 } 7377 while (false !== ($dir = readdir($dirhandle))) { 7378 // Func: strpos is marginally but reliably faster than substr($dir, 0, 1). 7379 if (strpos($dir, '.') === 0 or $dir === 'CVS' or $dir === '_vti_cnf' or $dir === 'simpletest' or $dir === 'yui' or 7380 $dir === 'tests' or $dir === 'classes' or $dir === $exclude) { 7381 continue; 7382 } 7383 if (filetype($basedir .'/'. $dir) != 'dir') { 7384 continue; 7385 } 7386 $plugins[] = $dir; 7387 } 7388 closedir($dirhandle); 7389 } 7390 if ($plugins) { 7391 asort($plugins); 7392 } 7393 return $plugins; 7394 } 7395 7396 /** 7397 * Invoke plugin's callback functions 7398 * 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 7406 * 7407 * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743 7408 */ 7409 function plugin_callback($type, $name, $feature, $action, $params = null, $default = null) { 7410 return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default); 7411 } 7412 7413 /** 7414 * Invoke component's callback functions 7415 * 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 7421 */ 7422 function component_callback($component, $function, array $params = array(), $default = null) { 7423 7424 $functionname = component_callback_exists($component, $function); 7425 7426 if ($functionname) { 7427 // Function exists, so just return function result. 7428 $ret = call_user_func_array($functionname, $params); 7429 if (is_null($ret)) { 7430 return $default; 7431 } else { 7432 return $ret; 7433 } 7434 } 7435 return $default; 7436 } 7437 7438 /** 7439 * Determine if a component callback exists and return the function name to call. Note that this 7440 * function will include the required library files so that the functioname returned can be 7441 * called directly. 7442 * 7443 * @param string $component frankenstyle component name, e.g. 'mod_quiz' 7444 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron' 7445 * @return mixed Complete function name to call if the callback exists or false if it doesn't. 7446 * @throws coding_exception if invalid component specfied 7447 */ 7448 function component_callback_exists($component, $function) { 7449 global $CFG; // This is needed for the inclusions. 7450 7451 $cleancomponent = clean_param($component, PARAM_COMPONENT); 7452 if (empty($cleancomponent)) { 7453 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component); 7454 } 7455 $component = $cleancomponent; 7456 7457 list($type, $name) = core_component::normalize_component($component); 7458 $component = $type . '_' . $name; 7459 7460 $oldfunction = $name.'_'.$function; 7461 $function = $component.'_'.$function; 7462 7463 $dir = core_component::get_component_directory($component); 7464 if (empty($dir)) { 7465 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component); 7466 } 7467 7468 // Load library and look for function. 7469 if (file_exists($dir.'/lib.php')) { 7470 require_once($dir.'/lib.php'); 7471 } 7472 7473 if (!function_exists($function) and function_exists($oldfunction)) { 7474 if ($type !== 'mod' and $type !== 'core') { 7475 debugging("Please use new function name $function instead of legacy $oldfunction", DEBUG_DEVELOPER); 7476 } 7477 $function = $oldfunction; 7478 } 7479 7480 if (function_exists($function)) { 7481 return $function; 7482 } 7483 return false; 7484 } 7485 7486 /** 7487 * Checks whether a plugin supports a specified feature. 7488 * 7489 * @param string $type Plugin type e.g. 'mod' 7490 * @param string $name Plugin name e.g. 'forum' 7491 * @param string $feature Feature code (FEATURE_xx constant) 7492 * @param mixed $default default value if feature support unknown 7493 * @return mixed Feature result (false if not supported, null if feature is unknown, 7494 * otherwise usually true but may have other feature-specific value such as array) 7495 * @throws coding_exception 7496 */ 7497 function plugin_supports($type, $name, $feature, $default = null) { 7498 global $CFG; 7499 7500 if ($type === 'mod' and $name === 'NEWMODULE') { 7501 // Somebody forgot to rename the module template. 7502 return false; 7503 } 7504 7505 $component = clean_param($type . '_' . $name, PARAM_COMPONENT); 7506 if (empty($component)) { 7507 throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name); 7508 } 7509 7510 $function = null; 7511 7512 if ($type === 'mod') { 7513 // We need this special case because we support subplugins in modules, 7514 // otherwise it would end up in infinite loop. 7515 if (file_exists("$CFG->dirroot/mod/$name/lib.php")) { 7516 include_once("$CFG->dirroot/mod/$name/lib.php"); 7517 $function = $component.'_supports'; 7518 if (!function_exists($function)) { 7519 // Legacy non-frankenstyle function name. 7520 $function = $name.'_supports'; 7521 } 7522 } 7523 7524 } else { 7525 if (!$path = core_component::get_plugin_directory($type, $name)) { 7526 // Non existent plugin type. 7527 return false; 7528 } 7529 if (file_exists("$path/lib.php")) { 7530 include_once("$path/lib.php"); 7531 $function = $component.'_supports'; 7532 } 7533 } 7534 7535 if ($function and function_exists($function)) { 7536 $supports = $function($feature); 7537 if (is_null($supports)) { 7538 // Plugin does not know - use default. 7539 return $default; 7540 } else { 7541 return $supports; 7542 } 7543 } 7544 7545 // Plugin does not care, so use default. 7546 return $default; 7547 } 7548 7549 /** 7550 * Returns true if the current version of PHP is greater that the specified one. 7551 * 7552 * @todo Check PHP version being required here is it too low? 7553 * 7554 * @param string $version The version of php being tested. 7555 * @return bool 7556 */ 7557 function check_php_version($version='5.2.4') { 7558 return (version_compare(phpversion(), $version) >= 0); 7559 } 7560 7561 /** 7562 * Determine if moodle installation requires update. 7563 * 7564 * Checks version numbers of main code and all plugins to see 7565 * if there are any mismatches. 7566 * 7567 * @return bool 7568 */ 7569 function moodle_needs_upgrading() { 7570 global $CFG; 7571 7572 if (empty($CFG->version)) { 7573 return true; 7574 } 7575 7576 // There is no need to purge plugininfo caches here because 7577 // these caches are not used during upgrade and they are purged after 7578 // every upgrade. 7579 7580 if (empty($CFG->allversionshash)) { 7581 return true; 7582 } 7583 7584 $hash = core_component::get_all_versions_hash(); 7585 7586 return ($hash !== $CFG->allversionshash); 7587 } 7588 7589 /** 7590 * Returns the major version of this site 7591 * 7592 * Moodle version numbers consist of three numbers separated by a dot, for 7593 * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so 7594 * called major version. This function extracts the major version from either 7595 * $CFG->release (default) or eventually from the $release variable defined in 7596 * the main version.php. 7597 * 7598 * @param bool $fromdisk should the version if source code files be used 7599 * @return string|false the major version like '2.3', false if could not be determined 7600 */ 7601 function moodle_major_version($fromdisk = false) { 7602 global $CFG; 7603 7604 if ($fromdisk) { 7605 $release = null; 7606 require($CFG->dirroot.'/version.php'); 7607 if (empty($release)) { 7608 return false; 7609 } 7610 7611 } else { 7612 if (empty($CFG->release)) { 7613 return false; 7614 } 7615 $release = $CFG->release; 7616 } 7617 7618 if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) { 7619 return $matches[0]; 7620 } else { 7621 return false; 7622 } 7623 } 7624 7625 // MISCELLANEOUS. 7626 7627 /** 7628 * Sets the system locale 7629 * 7630 * @category string 7631 * @param string $locale Can be used to force a locale 7632 */ 7633 function moodle_setlocale($locale='') { 7634 global $CFG; 7635 7636 static $currentlocale = ''; // Last locale caching. 7637 7638 $oldlocale = $currentlocale; 7639 7640 // Fetch the correct locale based on ostype. 7641 if ($CFG->ostype == 'WINDOWS') { 7642 $stringtofetch = 'localewin'; 7643 } else { 7644 $stringtofetch = 'locale'; 7645 } 7646 7647 // The priority is the same as in get_string() - parameter, config, course, session, user, global language. 7648 if (!empty($locale)) { 7649 $currentlocale = $locale; 7650 } else if (!empty($CFG->locale)) { // Override locale for all language packs. 7651 $currentlocale = $CFG->locale; 7652 } else { 7653 $currentlocale = get_string($stringtofetch, 'langconfig'); 7654 } 7655 7656 // Do nothing if locale already set up. 7657 if ($oldlocale == $currentlocale) { 7658 return; 7659 } 7660 7661 // Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values, 7662 // set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7 7663 // Some day, numeric, monetary and other categories should be set too, I think. :-/. 7664 7665 // Get current values. 7666 $monetary= setlocale (LC_MONETARY, 0); 7667 $numeric = setlocale (LC_NUMERIC, 0); 7668 $ctype = setlocale (LC_CTYPE, 0); 7669 if ($CFG->ostype != 'WINDOWS') { 7670 $messages= setlocale (LC_MESSAGES, 0); 7671 } 7672 // Set locale to all. 7673 $result = setlocale (LC_ALL, $currentlocale); 7674 // If setting of locale fails try the other utf8 or utf-8 variant, 7675 // some operating systems support both (Debian), others just one (OSX). 7676 if ($result === false) { 7677 if (stripos($currentlocale, '.UTF-8') !== false) { 7678 $newlocale = str_ireplace('.UTF-8', '.UTF8', $currentlocale); 7679 setlocale (LC_ALL, $newlocale); 7680 } else if (stripos($currentlocale, '.UTF8') !== false) { 7681 $newlocale = str_ireplace('.UTF8', '.UTF-8', $currentlocale); 7682 setlocale (LC_ALL, $newlocale); 7683 } 7684 } 7685 // Set old values. 7686 setlocale (LC_MONETARY, $monetary); 7687 setlocale (LC_NUMERIC, $numeric); 7688 if ($CFG->ostype != 'WINDOWS') { 7689 setlocale (LC_MESSAGES, $messages); 7690 } 7691 if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') { 7692 // To workaround a well-known PHP problem with Turkish letter Ii. 7693 setlocale (LC_CTYPE, $ctype); 7694 } 7695 } 7696 7697 /** 7698 * Count words in a string. 7699 * 7700 * Words are defined as things between whitespace. 7701 * 7702 * @category string 7703 * @param string $string The text to be searched for words. 7704 * @return int The count of words in the specified string 7705 */ 7706 function count_words($string) { 7707 $string = strip_tags($string); 7708 // Decode HTML entities. 7709 $string = html_entity_decode($string); 7710 // Replace underscores (which are classed as word characters) with spaces. 7711 $string = preg_replace('/_/u', ' ', $string); 7712 // Remove any characters that shouldn't be treated as word boundaries. 7713 $string = preg_replace('/[\'"’-]/u', '', $string); 7714 // Remove dots and commas from within numbers only. 7715 $string = preg_replace('/([0-9])[.,]([0-9])/u', '$1$2', $string); 7716 7717 return count(preg_split('/\w\b/u', $string)) - 1; 7718 } 7719 7720 /** 7721 * Count letters in a string. 7722 * 7723 * Letters are defined as chars not in tags and different from whitespace. 7724 * 7725 * @category string 7726 * @param string $string The text to be searched for letters. 7727 * @return int The count of letters in the specified text. 7728 */ 7729 function count_letters($string) { 7730 $string = strip_tags($string); // Tags are out now. 7731 $string = preg_replace('/[[:space:]]*/', '', $string); // Whitespace are out now. 7732 7733 return core_text::strlen($string); 7734 } 7735 7736 /** 7737 * Generate and return a random string of the specified length. 7738 * 7739 * @param int $length The length of the string to be created. 7740 * @return string 7741 */ 7742 function random_string($length=15) { 7743 $randombytes = random_bytes_emulate($length); 7744 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 7745 $pool .= 'abcdefghijklmnopqrstuvwxyz'; 7746 $pool .= '0123456789'; 7747 $poollen = strlen($pool); 7748 $string = ''; 7749 for ($i = 0; $i < $length; $i++) { 7750 $rand = ord($randombytes[$i]); 7751 $string .= substr($pool, ($rand%($poollen)), 1); 7752 } 7753 return $string; 7754 } 7755 7756 /** 7757 * Generate a complex random string (useful for md5 salts) 7758 * 7759 * This function is based on the above {@link random_string()} however it uses a 7760 * larger pool of characters and generates a string between 24 and 32 characters 7761 * 7762 * @param int $length Optional if set generates a string to exactly this length 7763 * @return string 7764 */ 7765 function complex_random_string($length=null) { 7766 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 7767 $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} '; 7768 $poollen = strlen($pool); 7769 if ($length===null) { 7770 $length = floor(rand(24, 32)); 7771 } 7772 $randombytes = random_bytes_emulate($length); 7773 $string = ''; 7774 for ($i = 0; $i < $length; $i++) { 7775 $rand = ord($randombytes[$i]); 7776 $string .= $pool[($rand%$poollen)]; 7777 } 7778 return $string; 7779 } 7780 7781 /** 7782 * Try to generates cryptographically secure pseudo-random bytes. 7783 * 7784 * Note this is achieved by fallbacking between: 7785 * - PHP 7 random_bytes(). 7786 * - OpenSSL openssl_random_pseudo_bytes(). 7787 * - In house random generator getting its entropy from various, hard to guess, pseudo-random sources. 7788 * 7789 * @param int $length requested length in bytes 7790 * @return string binary data 7791 */ 7792 function random_bytes_emulate($length) { 7793 global $CFG; 7794 if ($length <= 0) { 7795 debugging('Invalid random bytes length', DEBUG_DEVELOPER); 7796 return ''; 7797 } 7798 if (function_exists('random_bytes')) { 7799 // Use PHP 7 goodness. 7800 $hash = @random_bytes($length); 7801 if ($hash !== false) { 7802 return $hash; 7803 } 7804 } 7805 if (function_exists('openssl_random_pseudo_bytes')) { 7806 // For PHP 5.3 and later with openssl extension. 7807 $hash = openssl_random_pseudo_bytes($length); 7808 if ($hash !== false) { 7809 return $hash; 7810 } 7811 } 7812 7813 // Bad luck, there is no reliable random generator, let's just hash some unique stuff that is hard to guess. 7814 $hash = sha1(serialize($CFG) . serialize($_SERVER) . microtime(true) . uniqid('', true), true); 7815 // NOTE: the last param in sha1() is true, this means we are getting 20 bytes, not 40 chars as usual. 7816 if ($length <= 20) { 7817 return substr($hash, 0, $length); 7818 } 7819 return $hash . random_bytes_emulate($length - 20); 7820 } 7821 7822 /** 7823 * Given some text (which may contain HTML) and an ideal length, 7824 * this function truncates the text neatly on a word boundary if possible 7825 * 7826 * @category string 7827 * @param string $text text to be shortened 7828 * @param int $ideal ideal string length 7829 * @param boolean $exact if false, $text will not be cut mid-word 7830 * @param string $ending The string to append if the passed string is truncated 7831 * @return string $truncate shortened string 7832 */ 7833 function shorten_text($text, $ideal=30, $exact = false, $ending='...') { 7834 // If the plain text is shorter than the maximum length, return the whole text. 7835 if (core_text::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) { 7836 return $text; 7837 } 7838 7839 // Splits on HTML tags. Each open/close/empty tag will be the first thing 7840 // and only tag in its 'line'. 7841 preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER); 7842 7843 $totallength = core_text::strlen($ending); 7844 $truncate = ''; 7845 7846 // This array stores information about open and close tags and their position 7847 // in the truncated string. Each item in the array is an object with fields 7848 // ->open (true if open), ->tag (tag name in lower case), and ->pos 7849 // (byte position in truncated text). 7850 $tagdetails = array(); 7851 7852 foreach ($lines as $linematchings) { 7853 // If there is any html-tag in this line, handle it and add it (uncounted) to the output. 7854 if (!empty($linematchings[1])) { 7855 // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>). 7856 if (!preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $linematchings[1])) { 7857 if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $linematchings[1], $tagmatchings)) { 7858 // Record closing tag. 7859 $tagdetails[] = (object) array( 7860 'open' => false, 7861 'tag' => core_text::strtolower($tagmatchings[1]), 7862 'pos' => core_text::strlen($truncate), 7863 ); 7864 7865 } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $linematchings[1], $tagmatchings)) { 7866 // Record opening tag. 7867 $tagdetails[] = (object) array( 7868 'open' => true, 7869 'tag' => core_text::strtolower($tagmatchings[1]), 7870 'pos' => core_text::strlen($truncate), 7871 ); 7872 } else if (preg_match('/^<!--\[if\s.*?\]>$/s', $linematchings[1], $tagmatchings)) { 7873 $tagdetails[] = (object) array( 7874 'open' => true, 7875 'tag' => core_text::strtolower('if'), 7876 'pos' => core_text::strlen($truncate), 7877 ); 7878 } else if (preg_match('/^<!--<!\[endif\]-->$/s', $linematchings[1], $tagmatchings)) { 7879 $tagdetails[] = (object) array( 7880 'open' => false, 7881 'tag' => core_text::strtolower('if'), 7882 'pos' => core_text::strlen($truncate), 7883 ); 7884 } 7885 } 7886 // Add html-tag to $truncate'd text. 7887 $truncate .= $linematchings[1]; 7888 } 7889 7890 // Calculate the length of the plain text part of the line; handle entities as one character. 7891 $contentlength = core_text::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $linematchings[2])); 7892 if ($totallength + $contentlength > $ideal) { 7893 // The number of characters which are left. 7894 $left = $ideal - $totallength; 7895 $entitieslength = 0; 7896 // Search for html entities. 7897 if (preg_match_all('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', $linematchings[2], $entities, PREG_OFFSET_CAPTURE)) { 7898 // Calculate the real length of all entities in the legal range. 7899 foreach ($entities[0] as $entity) { 7900 if ($entity[1]+1-$entitieslength <= $left) { 7901 $left--; 7902 $entitieslength += core_text::strlen($entity[0]); 7903 } else { 7904 // No more characters left. 7905 break; 7906 } 7907 } 7908 } 7909 $breakpos = $left + $entitieslength; 7910 7911 // If the words shouldn't be cut in the middle... 7912 if (!$exact) { 7913 // Search the last occurence of a space. 7914 for (; $breakpos > 0; $breakpos--) { 7915 if ($char = core_text::substr($linematchings[2], $breakpos, 1)) { 7916 if ($char === '.' or $char === ' ') { 7917 $breakpos += 1; 7918 break; 7919 } else if (strlen($char) > 2) { 7920 // Chinese/Japanese/Korean text can be truncated at any UTF-8 character boundary. 7921 $breakpos += 1; 7922 break; 7923 } 7924 } 7925 } 7926 } 7927 if ($breakpos == 0) { 7928 // This deals with the test_shorten_text_no_spaces case. 7929 $breakpos = $left + $entitieslength; 7930 } else if ($breakpos > $left + $entitieslength) { 7931 // This deals with the previous for loop breaking on the first char. 7932 $breakpos = $left + $entitieslength; 7933 } 7934 7935 $truncate .= core_text::substr($linematchings[2], 0, $breakpos); 7936 // Maximum length is reached, so get off the loop. 7937 break; 7938 } else { 7939 $truncate .= $linematchings[2]; 7940 $totallength += $contentlength; 7941 } 7942 7943 // If the maximum length is reached, get off the loop. 7944 if ($totallength >= $ideal) { 7945 break; 7946 } 7947 } 7948 7949 // Add the defined ending to the text. 7950 $truncate .= $ending; 7951 7952 // Now calculate the list of open html tags based on the truncate position. 7953 $opentags = array(); 7954 foreach ($tagdetails as $taginfo) { 7955 if ($taginfo->open) { 7956 // Add tag to the beginning of $opentags list. 7957 array_unshift($opentags, $taginfo->tag); 7958 } else { 7959 // Can have multiple exact same open tags, close the last one. 7960 $pos = array_search($taginfo->tag, array_reverse($opentags, true)); 7961 if ($pos !== false) { 7962 unset($opentags[$pos]); 7963 } 7964 } 7965 } 7966 7967 // Close all unclosed html-tags. 7968 foreach ($opentags as $tag) { 7969 if ($tag === 'if') { 7970 $truncate .= '<!--<![endif]-->'; 7971 } else { 7972 $truncate .= '</' . $tag . '>'; 7973 } 7974 } 7975 7976 return $truncate; 7977 } 7978 7979 7980 /** 7981 * Given dates in seconds, how many weeks is the date from startdate 7982 * The first week is 1, the second 2 etc ... 7983 * 7984 * @param int $startdate Timestamp for the start date 7985 * @param int $thedate Timestamp for the end date 7986 * @return string 7987 */ 7988 function getweek ($startdate, $thedate) { 7989 if ($thedate < $startdate) { 7990 return 0; 7991 } 7992 7993 return floor(($thedate - $startdate) / WEEKSECS) + 1; 7994 } 7995 7996 /** 7997 * Returns a randomly generated password of length $maxlen. inspired by 7998 * 7999 * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and 8000 * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254} 8001 * 8002 * @param int $maxlen The maximum size of the password being generated. 8003 * @return string 8004 */ 8005 function generate_password($maxlen=10) { 8006 global $CFG; 8007 8008 if (empty($CFG->passwordpolicy)) { 8009 $fillers = PASSWORD_DIGITS; 8010 $wordlist = file($CFG->wordlist); 8011 $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]); 8012 $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]); 8013 $filler1 = $fillers[rand(0, strlen($fillers) - 1)]; 8014 $password = $word1 . $filler1 . $word2; 8015 } else { 8016 $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0; 8017 $digits = $CFG->minpassworddigits; 8018 $lower = $CFG->minpasswordlower; 8019 $upper = $CFG->minpasswordupper; 8020 $nonalphanum = $CFG->minpasswordnonalphanum; 8021 $total = $lower + $upper + $digits + $nonalphanum; 8022 // Var minlength should be the greater one of the two ( $minlen and $total ). 8023 $minlen = $minlen < $total ? $total : $minlen; 8024 // Var maxlen can never be smaller than minlen. 8025 $maxlen = $minlen > $maxlen ? $minlen : $maxlen; 8026 $additional = $maxlen - $total; 8027 8028 // Make sure we have enough characters to fulfill 8029 // complexity requirements. 8030 $passworddigits = PASSWORD_DIGITS; 8031 while ($digits > strlen($passworddigits)) { 8032 $passworddigits .= PASSWORD_DIGITS; 8033 } 8034 $passwordlower = PASSWORD_LOWER; 8035 while ($lower > strlen($passwordlower)) { 8036 $passwordlower .= PASSWORD_LOWER; 8037 } 8038 $passwordupper = PASSWORD_UPPER; 8039 while ($upper > strlen($passwordupper)) { 8040 $passwordupper .= PASSWORD_UPPER; 8041 } 8042 $passwordnonalphanum = PASSWORD_NONALPHANUM; 8043 while ($nonalphanum > strlen($passwordnonalphanum)) { 8044 $passwordnonalphanum .= PASSWORD_NONALPHANUM; 8045 } 8046 8047 // Now mix and shuffle it all. 8048 $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) . 8049 substr(str_shuffle ($passwordupper), 0, $upper) . 8050 substr(str_shuffle ($passworddigits), 0, $digits) . 8051 substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) . 8052 substr(str_shuffle ($passwordlower . 8053 $passwordupper . 8054 $passworddigits . 8055 $passwordnonalphanum), 0 , $additional)); 8056 } 8057 8058 return substr ($password, 0, $maxlen); 8059 } 8060 8061 /** 8062 * Given a float, prints it nicely. 8063 * Localized floats must not be used in calculations! 8064 * 8065 * The stripzeros feature is intended for making numbers look nicer in small 8066 * areas where it is not necessary to indicate the degree of accuracy by showing 8067 * ending zeros. If you turn it on with $decimalpoints set to 3, for example, 8068 * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'. 8069 * 8070 * @param float $float The float to print 8071 * @param int $decimalpoints The number of decimal places to print. 8072 * @param bool $localized use localized decimal separator 8073 * @param bool $stripzeros If true, removes final zeros after decimal point 8074 * @return string locale float 8075 */ 8076 function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=false) { 8077 if (is_null($float)) { 8078 return ''; 8079 } 8080 if ($localized) { 8081 $separator = get_string('decsep', 'langconfig'); 8082 } else { 8083 $separator = '.'; 8084 } 8085 $result = number_format($float, $decimalpoints, $separator, ''); 8086 if ($stripzeros) { 8087 // Remove zeros and final dot if not needed. 8088 $result = preg_replace('~(' . preg_quote($separator) . ')?0+$~', '', $result); 8089 } 8090 return $result; 8091 } 8092 8093 /** 8094 * Converts locale specific floating point/comma number back to standard PHP float value 8095 * Do NOT try to do any math operations before this conversion on any user submitted floats! 8096 * 8097 * @param string $localefloat locale aware float representation 8098 * @param bool $strict If true, then check the input and return false if it is not a valid number. 8099 * @return mixed float|bool - false or the parsed float. 8100 */ 8101 function unformat_float($localefloat, $strict = false) { 8102 $localefloat = trim($localefloat); 8103 8104 if ($localefloat == '') { 8105 return null; 8106 } 8107 8108 $localefloat = str_replace(' ', '', $localefloat); // No spaces - those might be used as thousand separators. 8109 $localefloat = str_replace(get_string('decsep', 'langconfig'), '.', $localefloat); 8110 8111 if ($strict && !is_numeric($localefloat)) { 8112 return false; 8113 } 8114 8115 return (float)$localefloat; 8116 } 8117 8118 /** 8119 * Given a simple array, this shuffles it up just like shuffle() 8120 * Unlike PHP's shuffle() this function works on any machine. 8121 * 8122 * @param array $array The array to be rearranged 8123 * @return array 8124 */ 8125 function swapshuffle($array) { 8126 8127 $last = count($array) - 1; 8128 for ($i = 0; $i <= $last; $i++) { 8129 $from = rand(0, $last); 8130 $curr = $array[$i]; 8131 $array[$i] = $array[$from]; 8132 $array[$from] = $curr; 8133 } 8134 return $array; 8135 } 8136 8137 /** 8138 * Like {@link swapshuffle()}, but works on associative arrays 8139 * 8140 * @param array $array The associative array to be rearranged 8141 * @return array 8142 */ 8143 function swapshuffle_assoc($array) { 8144 8145 $newarray = array(); 8146 $newkeys = swapshuffle(array_keys($array)); 8147 8148 foreach ($newkeys as $newkey) { 8149 $newarray[$newkey] = $array[$newkey]; 8150 } 8151 return $newarray; 8152 } 8153 8154 /** 8155 * Given an arbitrary array, and a number of draws, 8156 * this function returns an array with that amount 8157 * of items. The indexes are retained. 8158 * 8159 * @todo Finish documenting this function 8160 * 8161 * @param array $array 8162 * @param int $draws 8163 * @return array 8164 */ 8165 function draw_rand_array($array, $draws) { 8166 8167 $return = array(); 8168 8169 $last = count($array); 8170 8171 if ($draws > $last) { 8172 $draws = $last; 8173 } 8174 8175 while ($draws > 0) { 8176 $last--; 8177 8178 $keys = array_keys($array); 8179 $rand = rand(0, $last); 8180 8181 $return[$keys[$rand]] = $array[$keys[$rand]]; 8182 unset($array[$keys[$rand]]); 8183 8184 $draws--; 8185 } 8186 8187 return $return; 8188 } 8189 8190 /** 8191 * Calculate the difference between two microtimes 8192 * 8193 * @param string $a The first Microtime 8194 * @param string $b The second Microtime 8195 * @return string 8196 */ 8197 function microtime_diff($a, $b) { 8198 list($adec, $asec) = explode(' ', $a); 8199 list($bdec, $bsec) = explode(' ', $b); 8200 return $bsec - $asec + $bdec - $adec; 8201 } 8202 8203 /** 8204 * Given a list (eg a,b,c,d,e) this function returns 8205 * an array of 1->a, 2->b, 3->c etc 8206 * 8207 * @param string $list The string to explode into array bits 8208 * @param string $separator The separator used within the list string 8209 * @return array The now assembled array 8210 */ 8211 function make_menu_from_list($list, $separator=',') { 8212 8213 $array = array_reverse(explode($separator, $list), true); 8214 foreach ($array as $key => $item) { 8215 $outarray[$key+1] = trim($item); 8216 } 8217 return $outarray; 8218 } 8219 8220 /** 8221 * Creates an array that represents all the current grades that 8222 * can be chosen using the given grading type. 8223 * 8224 * Negative numbers 8225 * are scales, zero is no grade, and positive numbers are maximum 8226 * grades. 8227 * 8228 * @todo Finish documenting this function or better deprecated this completely! 8229 * 8230 * @param int $gradingtype 8231 * @return array 8232 */ 8233 function make_grades_menu($gradingtype) { 8234 global $DB; 8235 8236 $grades = array(); 8237 if ($gradingtype < 0) { 8238 if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) { 8239 return make_menu_from_list($scale->scale); 8240 } 8241 } else if ($gradingtype > 0) { 8242 for ($i=$gradingtype; $i>=0; $i--) { 8243 $grades[$i] = $i .' / '. $gradingtype; 8244 } 8245 return $grades; 8246 } 8247 return $grades; 8248 } 8249 8250 /** 8251 * make_unique_id_code 8252 * 8253 * @todo Finish documenting this function 8254 * 8255 * @uses $_SERVER 8256 * @param string $extra Extra string to append to the end of the code 8257 * @return string 8258 */ 8259 function make_unique_id_code($extra = '') { 8260 8261 $hostname = 'unknownhost'; 8262 if (!empty($_SERVER['HTTP_HOST'])) { 8263 $hostname = $_SERVER['HTTP_HOST']; 8264 } else if (!empty($_ENV['HTTP_HOST'])) { 8265 $hostname = $_ENV['HTTP_HOST']; 8266 } else if (!empty($_SERVER['SERVER_NAME'])) { 8267 $hostname = $_SERVER['SERVER_NAME']; 8268 } else if (!empty($_ENV['SERVER_NAME'])) { 8269 $hostname = $_ENV['SERVER_NAME']; 8270 } 8271 8272 $date = gmdate("ymdHis"); 8273 8274 $random = random_string(6); 8275 8276 if ($extra) { 8277 return $hostname .'+'. $date .'+'. $random .'+'. $extra; 8278 } else { 8279 return $hostname .'+'. $date .'+'. $random; 8280 } 8281 } 8282 8283 8284 /** 8285 * Function to check the passed address is within the passed subnet 8286 * 8287 * The parameter is a comma separated string of subnet definitions. 8288 * Subnet strings can be in one of three formats: 8289 * 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn (number of bits in net mask) 8290 * 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) 8291 * 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. (incomplete address, a bit non-technical ;-) 8292 * Code for type 1 modified from user posted comments by mediator at 8293 * {@link http://au.php.net/manual/en/function.ip2long.php} 8294 * 8295 * @param string $addr The address you are checking 8296 * @param string $subnetstr The string of subnet addresses 8297 * @return bool 8298 */ 8299 function address_in_subnet($addr, $subnetstr) { 8300 8301 if ($addr == '0.0.0.0') { 8302 return false; 8303 } 8304 $subnets = explode(',', $subnetstr); 8305 $found = false; 8306 $addr = trim($addr); 8307 $addr = cleanremoteaddr($addr, false); // Normalise. 8308 if ($addr === null) { 8309 return false; 8310 } 8311 $addrparts = explode(':', $addr); 8312 8313 $ipv6 = strpos($addr, ':'); 8314 8315 foreach ($subnets as $subnet) { 8316 $subnet = trim($subnet); 8317 if ($subnet === '') { 8318 continue; 8319 } 8320 8321 if (strpos($subnet, '/') !== false) { 8322 // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn. 8323 list($ip, $mask) = explode('/', $subnet); 8324 $mask = trim($mask); 8325 if (!is_number($mask)) { 8326 continue; // Incorect mask number, eh? 8327 } 8328 $ip = cleanremoteaddr($ip, false); // Normalise. 8329 if ($ip === null) { 8330 continue; 8331 } 8332 if (strpos($ip, ':') !== false) { 8333 // IPv6. 8334 if (!$ipv6) { 8335 continue; 8336 } 8337 if ($mask > 128 or $mask < 0) { 8338 continue; // Nonsense. 8339 } 8340 if ($mask == 0) { 8341 return true; // Any address. 8342 } 8343 if ($mask == 128) { 8344 if ($ip === $addr) { 8345 return true; 8346 } 8347 continue; 8348 } 8349 $ipparts = explode(':', $ip); 8350 $modulo = $mask % 16; 8351 $ipnet = array_slice($ipparts, 0, ($mask-$modulo)/16); 8352 $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16); 8353 if (implode(':', $ipnet) === implode(':', $addrnet)) { 8354 if ($modulo == 0) { 8355 return true; 8356 } 8357 $pos = ($mask-$modulo)/16; 8358 $ipnet = hexdec($ipparts[$pos]); 8359 $addrnet = hexdec($addrparts[$pos]); 8360 $mask = 0xffff << (16 - $modulo); 8361 if (($addrnet & $mask) == ($ipnet & $mask)) { 8362 return true; 8363 } 8364 } 8365 8366 } else { 8367 // IPv4. 8368 if ($ipv6) { 8369 continue; 8370 } 8371 if ($mask > 32 or $mask < 0) { 8372 continue; // Nonsense. 8373 } 8374 if ($mask == 0) { 8375 return true; 8376 } 8377 if ($mask == 32) { 8378 if ($ip === $addr) { 8379 return true; 8380 } 8381 continue; 8382 } 8383 $mask = 0xffffffff << (32 - $mask); 8384 if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) { 8385 return true; 8386 } 8387 } 8388 8389 } else if (strpos($subnet, '-') !== false) { 8390 // 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. 8391 $parts = explode('-', $subnet); 8392 if (count($parts) != 2) { 8393 continue; 8394 } 8395 8396 if (strpos($subnet, ':') !== false) { 8397 // IPv6. 8398 if (!$ipv6) { 8399 continue; 8400 } 8401 $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise. 8402 if ($ipstart === null) { 8403 continue; 8404 } 8405 $ipparts = explode(':', $ipstart); 8406 $start = hexdec(array_pop($ipparts)); 8407 $ipparts[] = trim($parts[1]); 8408 $ipend = cleanremoteaddr(implode(':', $ipparts), false); // Normalise. 8409 if ($ipend === null) { 8410 continue; 8411 } 8412 $ipparts[7] = ''; 8413 $ipnet = implode(':', $ipparts); 8414 if (strpos($addr, $ipnet) !== 0) { 8415 continue; 8416 } 8417 $ipparts = explode(':', $ipend); 8418 $end = hexdec($ipparts[7]); 8419 8420 $addrend = hexdec($addrparts[7]); 8421 8422 if (($addrend >= $start) and ($addrend <= $end)) { 8423 return true; 8424 } 8425 8426 } else { 8427 // IPv4. 8428 if ($ipv6) { 8429 continue; 8430 } 8431 $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise. 8432 if ($ipstart === null) { 8433 continue; 8434 } 8435 $ipparts = explode('.', $ipstart); 8436 $ipparts[3] = trim($parts[1]); 8437 $ipend = cleanremoteaddr(implode('.', $ipparts), false); // Normalise. 8438 if ($ipend === null) { 8439 continue; 8440 } 8441 8442 if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) { 8443 return true; 8444 } 8445 } 8446 8447 } else { 8448 // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. 8449 if (strpos($subnet, ':') !== false) { 8450 // IPv6. 8451 if (!$ipv6) { 8452 continue; 8453 } 8454 $parts = explode(':', $subnet); 8455 $count = count($parts); 8456 if ($parts[$count-1] === '') { 8457 unset($parts[$count-1]); // Trim trailing :'s. 8458 $count--; 8459 $subnet = implode('.', $parts); 8460 } 8461 $isip = cleanremoteaddr($subnet, false); // Normalise. 8462 if ($isip !== null) { 8463 if ($isip === $addr) { 8464 return true; 8465 } 8466 continue; 8467 } else if ($count > 8) { 8468 continue; 8469 } 8470 $zeros = array_fill(0, 8-$count, '0'); 8471 $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16); 8472 if (address_in_subnet($addr, $subnet)) { 8473 return true; 8474 } 8475 8476 } else { 8477 // IPv4. 8478 if ($ipv6) { 8479 continue; 8480 } 8481 $parts = explode('.', $subnet); 8482 $count = count($parts); 8483 if ($parts[$count-1] === '') { 8484 unset($parts[$count-1]); // Trim trailing . 8485 $count--; 8486 $subnet = implode('.', $parts); 8487 } 8488 if ($count == 4) { 8489 $subnet = cleanremoteaddr($subnet, false); // Normalise. 8490 if ($subnet === $addr) { 8491 return true; 8492 } 8493 continue; 8494 } else if ($count > 4) { 8495 continue; 8496 } 8497 $zeros = array_fill(0, 4-$count, '0'); 8498 $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8); 8499 if (address_in_subnet($addr, $subnet)) { 8500 return true; 8501 } 8502 } 8503 } 8504 } 8505 8506 return false; 8507 } 8508 8509 /** 8510 * For outputting debugging info 8511 * 8512 * @param string $string The string to write 8513 * @param string $eol The end of line char(s) to use 8514 * @param string $sleep Period to make the application sleep 8515 * This ensures any messages have time to display before redirect 8516 */ 8517 function mtrace($string, $eol="\n", $sleep=0) { 8518 8519 if (defined('STDOUT') && !PHPUNIT_TEST && !defined('BEHAT_TEST')) { 8520 fwrite(STDOUT, $string.$eol); 8521 } else { 8522 echo $string . $eol; 8523 } 8524 8525 flush(); 8526 8527 // Delay to keep message on user's screen in case of subsequent redirect. 8528 if ($sleep) { 8529 sleep($sleep); 8530 } 8531 } 8532 8533 /** 8534 * Replace 1 or more slashes or backslashes to 1 slash 8535 * 8536 * @param string $path The path to strip 8537 * @return string the path with double slashes removed 8538 */ 8539 function cleardoubleslashes ($path) { 8540 return preg_replace('/(\/|\\\){1,}/', '/', $path); 8541 } 8542 8543 /** 8544 * Is current ip in give list? 8545 * 8546 * @param string $list 8547 * @return bool 8548 */ 8549 function remoteip_in_list($list) { 8550 $inlist = false; 8551 $clientip = getremoteaddr(null); 8552 8553 if (!$clientip) { 8554 // Ensure access on cli. 8555 return true; 8556 } 8557 8558 $list = explode("\n", $list); 8559 foreach ($list as $subnet) { 8560 $subnet = trim($subnet); 8561 if (address_in_subnet($clientip, $subnet)) { 8562 $inlist = true; 8563 break; 8564 } 8565 } 8566 return $inlist; 8567 } 8568 8569 /** 8570 * Returns most reliable client address 8571 * 8572 * @param string $default If an address can't be determined, then return this 8573 * @return string The remote IP address 8574 */ 8575 function getremoteaddr($default='0.0.0.0') { 8576 global $CFG; 8577 8578 if (empty($CFG->getremoteaddrconf)) { 8579 // This will happen, for example, before just after the upgrade, as the 8580 // user is redirected to the admin screen. 8581 $variablestoskip = 0; 8582 } else { 8583 $variablestoskip = $CFG->getremoteaddrconf; 8584 } 8585 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) { 8586 if (!empty($_SERVER['HTTP_CLIENT_IP'])) { 8587 $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']); 8588 return $address ? $address : $default; 8589 } 8590 } 8591 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) { 8592 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { 8593 $forwardedaddresses = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']); 8594 $address = $forwardedaddresses[0]; 8595 8596 if (substr_count($address, ":") > 1) { 8597 // Remove port and brackets from IPv6. 8598 if (preg_match("/\[(.*)\]:/", $address, $matches)) { 8599 $address = $matches[1]; 8600 } 8601 } else { 8602 // Remove port from IPv4. 8603 if (substr_count($address, ":") == 1) { 8604 $parts = explode(":", $address); 8605 $address = $parts[0]; 8606 } 8607 } 8608 8609 $address = cleanremoteaddr($address); 8610 return $address ? $address : $default; 8611 } 8612 } 8613 if (!empty($_SERVER['REMOTE_ADDR'])) { 8614 $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']); 8615 return $address ? $address : $default; 8616 } else { 8617 return $default; 8618 } 8619 } 8620 8621 /** 8622 * Cleans an ip address. Internal addresses are now allowed. 8623 * (Originally local addresses were not allowed.) 8624 * 8625 * @param string $addr IPv4 or IPv6 address 8626 * @param bool $compress use IPv6 address compression 8627 * @return string normalised ip address string, null if error 8628 */ 8629 function cleanremoteaddr($addr, $compress=false) { 8630 $addr = trim($addr); 8631 8632 // TODO: maybe add a separate function is_addr_public() or something like this. 8633 8634 if (strpos($addr, ':') !== false) { 8635 // Can be only IPv6. 8636 $parts = explode(':', $addr); 8637 $count = count($parts); 8638 8639 if (strpos($parts[$count-1], '.') !== false) { 8640 // Legacy ipv4 notation. 8641 $last = array_pop($parts); 8642 $ipv4 = cleanremoteaddr($last, true); 8643 if ($ipv4 === null) { 8644 return null; 8645 } 8646 $bits = explode('.', $ipv4); 8647 $parts[] = dechex($bits[0]).dechex($bits[1]); 8648 $parts[] = dechex($bits[2]).dechex($bits[3]); 8649 $count = count($parts); 8650 $addr = implode(':', $parts); 8651 } 8652 8653 if ($count < 3 or $count > 8) { 8654 return null; // Severly malformed. 8655 } 8656 8657 if ($count != 8) { 8658 if (strpos($addr, '::') === false) { 8659 return null; // Malformed. 8660 } 8661 // Uncompress. 8662 $insertat = array_search('', $parts, true); 8663 $missing = array_fill(0, 1 + 8 - $count, '0'); 8664 array_splice($parts, $insertat, 1, $missing); 8665 foreach ($parts as $key => $part) { 8666 if ($part === '') { 8667 $parts[$key] = '0'; 8668 } 8669 } 8670 } 8671 8672 $adr = implode(':', $parts); 8673 if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) { 8674 return null; // Incorrect format - sorry. 8675 } 8676 8677 // Normalise 0s and case. 8678 $parts = array_map('hexdec', $parts); 8679 $parts = array_map('dechex', $parts); 8680 8681 $result = implode(':', $parts); 8682 8683 if (!$compress) { 8684 return $result; 8685 } 8686 8687 if ($result === '0:0:0:0:0:0:0:0') { 8688 return '::'; // All addresses. 8689 } 8690 8691 $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1); 8692 if ($compressed !== $result) { 8693 return $compressed; 8694 } 8695 8696 $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1); 8697 if ($compressed !== $result) { 8698 return $compressed; 8699 } 8700 8701 $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1); 8702 if ($compressed !== $result) { 8703 return $compressed; 8704 } 8705 8706 return $result; 8707 } 8708 8709 // First get all things that look like IPv4 addresses. 8710 $parts = array(); 8711 if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) { 8712 return null; 8713 } 8714 unset($parts[0]); 8715 8716 foreach ($parts as $key => $match) { 8717 if ($match > 255) { 8718 return null; 8719 } 8720 $parts[$key] = (int)$match; // Normalise 0s. 8721 } 8722 8723 return implode('.', $parts); 8724 } 8725 8726 /** 8727 * This function will make a complete copy of anything it's given, 8728 * regardless of whether it's an object or not. 8729 * 8730 * @param mixed $thing Something you want cloned 8731 * @return mixed What ever it is you passed it 8732 */ 8733 function fullclone($thing) { 8734 return unserialize(serialize($thing)); 8735 } 8736 8737 /** 8738 * If new messages are waiting for the current user, then insert 8739 * JavaScript to pop up the messaging window into the page 8740 * 8741 * @return void 8742 */ 8743 function message_popup_window() { 8744 global $USER, $DB, $PAGE, $CFG; 8745 8746 if (!$PAGE->get_popup_notification_allowed() || empty($CFG->messaging)) { 8747 return; 8748 } 8749 8750 if (!isloggedin() || isguestuser()) { 8751 return; 8752 } 8753 8754 if (!isset($USER->message_lastpopup)) { 8755 $USER->message_lastpopup = 0; 8756 } else if ($USER->message_lastpopup > (time()-120)) { 8757 // Don't run the query to check whether to display a popup if its been run in the last 2 minutes. 8758 return; 8759 } 8760 8761 // A quick query to check whether the user has new messages. 8762 $messagecount = $DB->count_records('message', array('useridto' => $USER->id)); 8763 if ($messagecount < 1) { 8764 return; 8765 } 8766 8767 // There are unread messages so now do a more complex but slower query. 8768 $messagesql = "SELECT m.id, c.blocked 8769 FROM {message} m 8770 JOIN {message_working} mw ON m.id=mw.unreadmessageid 8771 JOIN {message_processors} p ON mw.processorid=p.id 8772 LEFT JOIN {message_contacts} c ON c.contactid = m.useridfrom 8773 AND c.userid = m.useridto 8774 WHERE m.useridto = :userid 8775 AND p.name='popup'"; 8776 8777 // If the user was last notified over an hour ago we can re-notify them of old messages 8778 // so don't worry about when the new message was sent. 8779 $lastnotifiedlongago = $USER->message_lastpopup < (time()-3600); 8780 if (!$lastnotifiedlongago) { 8781 $messagesql .= 'AND m.timecreated > :lastpopuptime'; 8782 } 8783 8784 $waitingmessages = $DB->get_records_sql($messagesql, array('userid' => $USER->id, 'lastpopuptime' => $USER->message_lastpopup)); 8785 8786 $validmessages = 0; 8787 foreach ($waitingmessages as $messageinfo) { 8788 if ($messageinfo->blocked) { 8789 // Message is from a user who has since been blocked so just mark it read. 8790 // Get the full message to mark as read. 8791 $messageobject = $DB->get_record('message', array('id' => $messageinfo->id)); 8792 message_mark_message_read($messageobject, time()); 8793 } else { 8794 $validmessages++; 8795 } 8796 } 8797 8798 if ($validmessages > 0) { 8799 $strmessages = get_string('unreadnewmessages', 'message', $validmessages); 8800 $strgomessage = get_string('gotomessages', 'message'); 8801 $strstaymessage = get_string('ignore', 'admin'); 8802 8803 $notificationsound = null; 8804 $beep = get_user_preferences('message_beepnewmessage', ''); 8805 if (!empty($beep)) { 8806 // Browsers will work down this list until they find something they support. 8807 $sourcetags = html_writer::empty_tag('source', array('src' => $CFG->wwwroot.'/message/bell.wav', 'type' => 'audio/wav')); 8808 $sourcetags .= html_writer::empty_tag('source', array('src' => $CFG->wwwroot.'/message/bell.ogg', 'type' => 'audio/ogg')); 8809 $sourcetags .= html_writer::empty_tag('source', array('src' => $CFG->wwwroot.'/message/bell.mp3', 'type' => 'audio/mpeg')); 8810 $sourcetags .= html_writer::empty_tag('embed', array('src' => $CFG->wwwroot.'/message/bell.wav', 'autostart' => 'true', 'hidden' => 'true')); 8811 8812 $notificationsound = html_writer::tag('audio', $sourcetags, array('preload' => 'auto', 'autoplay' => 'autoplay')); 8813 } 8814 8815 $url = $CFG->wwwroot.'/message/index.php'; 8816 $content = html_writer::start_tag('div', array('id' => 'newmessageoverlay', 'class' => 'mdl-align')). 8817 html_writer::start_tag('div', array('id' => 'newmessagetext')). 8818 $strmessages. 8819 html_writer::end_tag('div'). 8820 8821 $notificationsound. 8822 html_writer::start_tag('div', array('id' => 'newmessagelinks')). 8823 html_writer::link($url, $strgomessage, array('id' => 'notificationyes')).' '. 8824 html_writer::link('', $strstaymessage, array('id' => 'notificationno')). 8825 html_writer::end_tag('div'); 8826 html_writer::end_tag('div'); 8827 8828 $PAGE->requires->js_init_call('M.core_message.init_notification', array('', $content, $url)); 8829 8830 $USER->message_lastpopup = time(); 8831 } 8832 } 8833 8834 /** 8835 * Used to make sure that $min <= $value <= $max 8836 * 8837 * Make sure that value is between min, and max 8838 * 8839 * @param int $min The minimum value 8840 * @param int $value The value to check 8841 * @param int $max The maximum value 8842 * @return int 8843 */ 8844 function bounded_number($min, $value, $max) { 8845 if ($value < $min) { 8846 return $min; 8847 } 8848 if ($value > $max) { 8849 return $max; 8850 } 8851 return $value; 8852 } 8853 8854 /** 8855 * Check if there is a nested array within the passed array 8856 * 8857 * @param array $array 8858 * @return bool true if there is a nested array false otherwise 8859 */ 8860 function array_is_nested($array) { 8861 foreach ($array as $value) { 8862 if (is_array($value)) { 8863 return true; 8864 } 8865 } 8866 return false; 8867 } 8868 8869 /** 8870 * get_performance_info() pairs up with init_performance_info() 8871 * loaded in setup.php. Returns an array with 'html' and 'txt' 8872 * values ready for use, and each of the individual stats provided 8873 * separately as well. 8874 * 8875 * @return array 8876 */ 8877 function get_performance_info() { 8878 global $CFG, $PERF, $DB, $PAGE; 8879 8880 $info = array(); 8881 $info['html'] = ''; // Holds userfriendly HTML representation. 8882 $info['txt'] = me() . ' '; // Holds log-friendly representation. 8883 8884 $info['realtime'] = microtime_diff($PERF->starttime, microtime()); 8885 8886 $info['html'] .= '<span class="timeused">'.$info['realtime'].' secs</span> '; 8887 $info['txt'] .= 'time: '.$info['realtime'].'s '; 8888 8889 if (function_exists('memory_get_usage')) { 8890 $info['memory_total'] = memory_get_usage(); 8891 $info['memory_growth'] = memory_get_usage() - $PERF->startmemory; 8892 $info['html'] .= '<span class="memoryused">RAM: '.display_size($info['memory_total']).'</span> '; 8893 $info['txt'] .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '. 8894 $info['memory_growth'].'B ('.display_size($info['memory_growth']).') '; 8895 } 8896 8897 if (function_exists('memory_get_peak_usage')) { 8898 $info['memory_peak'] = memory_get_peak_usage(); 8899 $info['html'] .= '<span class="memoryused">RAM peak: '.display_size($info['memory_peak']).'</span> '; 8900 $info['txt'] .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') '; 8901 } 8902 8903 $inc = get_included_files(); 8904 $info['includecount'] = count($inc); 8905 $info['html'] .= '<span class="included">Included '.$info['includecount'].' files</span> '; 8906 $info['txt'] .= 'includecount: '.$info['includecount'].' '; 8907 8908 if (!empty($CFG->early_install_lang) or empty($PAGE)) { 8909 // We can not track more performance before installation or before PAGE init, sorry. 8910 return $info; 8911 } 8912 8913 $filtermanager = filter_manager::instance(); 8914 if (method_exists($filtermanager, 'get_performance_summary')) { 8915 list($filterinfo, $nicenames) = $filtermanager->get_performance_summary(); 8916 $info = array_merge($filterinfo, $info); 8917 foreach ($filterinfo as $key => $value) { 8918 $info['html'] .= "<span class='$key'>$nicenames[$key]: $value </span> "; 8919 $info['txt'] .= "$key: $value "; 8920 } 8921 } 8922 8923 $stringmanager = get_string_manager(); 8924 if (method_exists($stringmanager, 'get_performance_summary')) { 8925 list($filterinfo, $nicenames) = $stringmanager->get_performance_summary(); 8926 $info = array_merge($filterinfo, $info); 8927 foreach ($filterinfo as $key => $value) { 8928 $info['html'] .= "<span class='$key'>$nicenames[$key]: $value </span> "; 8929 $info['txt'] .= "$key: $value "; 8930 } 8931 } 8932 8933 if (!empty($PERF->logwrites)) { 8934 $info['logwrites'] = $PERF->logwrites; 8935 $info['html'] .= '<span class="logwrites">Log DB writes '.$info['logwrites'].'</span> '; 8936 $info['txt'] .= 'logwrites: '.$info['logwrites'].' '; 8937 } 8938 8939 $info['dbqueries'] = $DB->perf_get_reads().'/'.($DB->perf_get_writes() - $PERF->logwrites); 8940 $info['html'] .= '<span class="dbqueries">DB reads/writes: '.$info['dbqueries'].'</span> '; 8941 $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' '; 8942 8943 $info['dbtime'] = round($DB->perf_get_queries_time(), 5); 8944 $info['html'] .= '<span class="dbtime">DB queries time: '.$info['dbtime'].' secs</span> '; 8945 $info['txt'] .= 'db queries time: ' . $info['dbtime'] . 's '; 8946 8947 if (function_exists('posix_times')) { 8948 $ptimes = posix_times(); 8949 if (is_array($ptimes)) { 8950 foreach ($ptimes as $key => $val) { 8951 $info[$key] = $ptimes[$key] - $PERF->startposixtimes[$key]; 8952 } 8953 $info['html'] .= "<span class=\"posixtimes\">ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</span> "; 8954 $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] "; 8955 } 8956 } 8957 8958 // Grab the load average for the last minute. 8959 // /proc will only work under some linux configurations 8960 // while uptime is there under MacOSX/Darwin and other unices. 8961 if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) { 8962 list($serverload) = explode(' ', $loadavg[0]); 8963 unset($loadavg); 8964 } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) { 8965 if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) { 8966 $serverload = $matches[1]; 8967 } else { 8968 trigger_error('Could not parse uptime output!'); 8969 } 8970 } 8971 if (!empty($serverload)) { 8972 $info['serverload'] = $serverload; 8973 $info['html'] .= '<span class="serverload">Load average: '.$info['serverload'].'</span> '; 8974 $info['txt'] .= "serverload: {$info['serverload']} "; 8975 } 8976 8977 // Display size of session if session started. 8978 if ($si = \core\session\manager::get_performance_info()) { 8979 $info['sessionsize'] = $si['size']; 8980 $info['html'] .= $si['html']; 8981 $info['txt'] .= $si['txt']; 8982 } 8983 8984 if ($stats = cache_helper::get_stats()) { 8985 $html = '<span class="cachesused">'; 8986 $html .= '<span class="cache-stats-heading">Caches used (hits/misses/sets)</span>'; 8987 $text = 'Caches used (hits/misses/sets): '; 8988 $hits = 0; 8989 $misses = 0; 8990 $sets = 0; 8991 foreach ($stats as $definition => $details) { 8992 switch ($details['mode']) { 8993 case cache_store::MODE_APPLICATION: 8994 $modeclass = 'application'; 8995 $mode = ' <span title="application cache">[a]</span>'; 8996 break; 8997 case cache_store::MODE_SESSION: 8998 $modeclass = 'session'; 8999 $mode = ' <span title="session cache">[s]</span>'; 9000 break; 9001 case cache_store::MODE_REQUEST: 9002 $modeclass = 'request'; 9003 $mode = ' <span title="request cache">[r]</span>'; 9004 break; 9005 } 9006 $html .= '<span class="cache-definition-stats cache-mode-'.$modeclass.'">'; 9007 $html .= '<span class="cache-definition-stats-heading">'.$definition.$mode.'</span>'; 9008 $text .= "$definition {"; 9009 foreach ($details['stores'] as $store => $data) { 9010 $hits += $data['hits']; 9011 $misses += $data['misses']; 9012 $sets += $data['sets']; 9013 if ($data['hits'] == 0 and $data['misses'] > 0) { 9014 $cachestoreclass = 'nohits'; 9015 } else if ($data['hits'] < $data['misses']) { 9016 $cachestoreclass = 'lowhits'; 9017 } else { 9018 $cachestoreclass = 'hihits'; 9019 } 9020 $text .= "$store($data[hits]/$data[misses]/$data[sets]) "; 9021 $html .= "<span class=\"cache-store-stats $cachestoreclass\">$store: $data[hits] / $data[misses] / $data[sets]</span>"; 9022 } 9023 $html .= '</span>'; 9024 $text .= '} '; 9025 } 9026 $html .= "<span class='cache-total-stats'>Total: $hits / $misses / $sets</span>"; 9027 $html .= '</span> '; 9028 $info['cachesused'] = "$hits / $misses / $sets"; 9029 $info['html'] .= $html; 9030 $info['txt'] .= $text.'. '; 9031 } else { 9032 $info['cachesused'] = '0 / 0 / 0'; 9033 $info['html'] .= '<span class="cachesused">Caches used (hits/misses/sets): 0/0/0</span>'; 9034 $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 '; 9035 } 9036 9037 $info['html'] = '<div class="performanceinfo siteinfo">'.$info['html'].'</div>'; 9038 return $info; 9039 } 9040 9041 /** 9042 * Delete directory or only its content 9043 * 9044 * @param string $dir directory path 9045 * @param bool $contentonly 9046 * @return bool success, true also if dir does not exist 9047 */ 9048 function remove_dir($dir, $contentonly=false) { 9049 if (!file_exists($dir)) { 9050 // Nothing to do. 9051 return true; 9052 } 9053 if (!$handle = opendir($dir)) { 9054 return false; 9055 } 9056 $result = true; 9057 while (false!==($item = readdir($handle))) { 9058 if ($item != '.' && $item != '..') { 9059 if (is_dir($dir.'/'.$item)) { 9060 $result = remove_dir($dir.'/'.$item) && $result; 9061 } else { 9062 $result = unlink($dir.'/'.$item) && $result; 9063 } 9064 } 9065 } 9066 closedir($handle); 9067 if ($contentonly) { 9068 clearstatcache(); // Make sure file stat cache is properly invalidated. 9069 return $result; 9070 } 9071 $result = rmdir($dir); // If anything left the result will be false, no need for && $result. 9072 clearstatcache(); // Make sure file stat cache is properly invalidated. 9073 return $result; 9074 } 9075 9076 /** 9077 * Detect if an object or a class contains a given property 9078 * will take an actual object or the name of a class 9079 * 9080 * @param mix $obj Name of class or real object to test 9081 * @param string $property name of property to find 9082 * @return bool true if property exists 9083 */ 9084 function object_property_exists( $obj, $property ) { 9085 if (is_string( $obj )) { 9086 $properties = get_class_vars( $obj ); 9087 } else { 9088 $properties = get_object_vars( $obj ); 9089 } 9090 return array_key_exists( $property, $properties ); 9091 } 9092 9093 /** 9094 * Converts an object into an associative array 9095 * 9096 * This function converts an object into an associative array by iterating 9097 * over its public properties. Because this function uses the foreach 9098 * construct, Iterators are respected. It works recursively on arrays of objects. 9099 * Arrays and simple values are returned as is. 9100 * 9101 * If class has magic properties, it can implement IteratorAggregate 9102 * and return all available properties in getIterator() 9103 * 9104 * @param mixed $var 9105 * @return array 9106 */ 9107 function convert_to_array($var) { 9108 $result = array(); 9109 9110 // Loop over elements/properties. 9111 foreach ($var as $key => $value) { 9112 // Recursively convert objects. 9113 if (is_object($value) || is_array($value)) { 9114 $result[$key] = convert_to_array($value); 9115 } else { 9116 // Simple values are untouched. 9117 $result[$key] = $value; 9118 } 9119 } 9120 return $result; 9121 } 9122 9123 /** 9124 * Detect a custom script replacement in the data directory that will 9125 * replace an existing moodle script 9126 * 9127 * @return string|bool full path name if a custom script exists, false if no custom script exists 9128 */ 9129 function custom_script_path() { 9130 global $CFG, $SCRIPT; 9131 9132 if ($SCRIPT === null) { 9133 // Probably some weird external script. 9134 return false; 9135 } 9136 9137 $scriptpath = $CFG->customscripts . $SCRIPT; 9138 9139 // Check the custom script exists. 9140 if (file_exists($scriptpath) and is_file($scriptpath)) { 9141 return $scriptpath; 9142 } else { 9143 return false; 9144 } 9145 } 9146 9147 /** 9148 * Returns whether or not the user object is a remote MNET user. This function 9149 * is in moodlelib because it does not rely on loading any of the MNET code. 9150 * 9151 * @param object $user A valid user object 9152 * @return bool True if the user is from a remote Moodle. 9153 */ 9154 function is_mnet_remote_user($user) { 9155 global $CFG; 9156 9157 if (!isset($CFG->mnet_localhost_id)) { 9158 include_once($CFG->dirroot . '/mnet/lib.php'); 9159 $env = new mnet_environment(); 9160 $env->init(); 9161 unset($env); 9162 } 9163 9164 return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id); 9165 } 9166 9167 /** 9168 * This function will search for browser prefereed languages, setting Moodle 9169 * to use the best one available if $SESSION->lang is undefined 9170 */ 9171 function setup_lang_from_browser() { 9172 global $CFG, $SESSION, $USER; 9173 9174 if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) { 9175 // Lang is defined in session or user profile, nothing to do. 9176 return; 9177 } 9178 9179 if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do. 9180 return; 9181 } 9182 9183 // Extract and clean langs from headers. 9184 $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE']; 9185 $rawlangs = str_replace('-', '_', $rawlangs); // We are using underscores. 9186 $rawlangs = explode(',', $rawlangs); // Convert to array. 9187 $langs = array(); 9188 9189 $order = 1.0; 9190 foreach ($rawlangs as $lang) { 9191 if (strpos($lang, ';') === false) { 9192 $langs[(string)$order] = $lang; 9193 $order = $order-0.01; 9194 } else { 9195 $parts = explode(';', $lang); 9196 $pos = strpos($parts[1], '='); 9197 $langs[substr($parts[1], $pos+1)] = $parts[0]; 9198 } 9199 } 9200 krsort($langs, SORT_NUMERIC); 9201 9202 // Look for such langs under standard locations. 9203 foreach ($langs as $lang) { 9204 // Clean it properly for include. 9205 $lang = strtolower(clean_param($lang, PARAM_SAFEDIR)); 9206 if (get_string_manager()->translation_exists($lang, false)) { 9207 // Lang exists, set it in session. 9208 $SESSION->lang = $lang; 9209 // We have finished. Go out. 9210 break; 9211 } 9212 } 9213 return; 9214 } 9215 9216 /** 9217 * Check if $url matches anything in proxybypass list 9218 * 9219 * Any errors just result in the proxy being used (least bad) 9220 * 9221 * @param string $url url to check 9222 * @return boolean true if we should bypass the proxy 9223 */ 9224 function is_proxybypass( $url ) { 9225 global $CFG; 9226 9227 // Sanity check. 9228 if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) { 9229 return false; 9230 } 9231 9232 // Get the host part out of the url. 9233 if (!$host = parse_url( $url, PHP_URL_HOST )) { 9234 return false; 9235 } 9236 9237 // Get the possible bypass hosts into an array. 9238 $matches = explode( ',', $CFG->proxybypass ); 9239 9240 // Check for a match. 9241 // (IPs need to match the left hand side and hosts the right of the url, 9242 // but we can recklessly check both as there can't be a false +ve). 9243 foreach ($matches as $match) { 9244 $match = trim($match); 9245 9246 // Try for IP match (Left side). 9247 $lhs = substr($host, 0, strlen($match)); 9248 if (strcasecmp($match, $lhs)==0) { 9249 return true; 9250 } 9251 9252 // Try for host match (Right side). 9253 $rhs = substr($host, -strlen($match)); 9254 if (strcasecmp($match, $rhs)==0) { 9255 return true; 9256 } 9257 } 9258 9259 // Nothing matched. 9260 return false; 9261 } 9262 9263 /** 9264 * Check if the passed navigation is of the new style 9265 * 9266 * @param mixed $navigation 9267 * @return bool true for yes false for no 9268 */ 9269 function is_newnav($navigation) { 9270 if (is_array($navigation) && !empty($navigation['newnav'])) { 9271 return true; 9272 } else { 9273 return false; 9274 } 9275 } 9276 9277 /** 9278 * Checks whether the given variable name is defined as a variable within the given object. 9279 * 9280 * This will NOT work with stdClass objects, which have no class variables. 9281 * 9282 * @param string $var The variable name 9283 * @param object $object The object to check 9284 * @return boolean 9285 */ 9286 function in_object_vars($var, $object) { 9287 $classvars = get_class_vars(get_class($object)); 9288 $classvars = array_keys($classvars); 9289 return in_array($var, $classvars); 9290 } 9291 9292 /** 9293 * Returns an array without repeated objects. 9294 * This function is similar to array_unique, but for arrays that have objects as values 9295 * 9296 * @param array $array 9297 * @param bool $keepkeyassoc 9298 * @return array 9299 */ 9300 function object_array_unique($array, $keepkeyassoc = true) { 9301 $duplicatekeys = array(); 9302 $tmp = array(); 9303 9304 foreach ($array as $key => $val) { 9305 // Convert objects to arrays, in_array() does not support objects. 9306 if (is_object($val)) { 9307 $val = (array)$val; 9308 } 9309 9310 if (!in_array($val, $tmp)) { 9311 $tmp[] = $val; 9312 } else { 9313 $duplicatekeys[] = $key; 9314 } 9315 } 9316 9317 foreach ($duplicatekeys as $key) { 9318 unset($array[$key]); 9319 } 9320 9321 return $keepkeyassoc ? $array : array_values($array); 9322 } 9323 9324 /** 9325 * Is a userid the primary administrator? 9326 * 9327 * @param int $userid int id of user to check 9328 * @return boolean 9329 */ 9330 function is_primary_admin($userid) { 9331 $primaryadmin = get_admin(); 9332 9333 if ($userid == $primaryadmin->id) { 9334 return true; 9335 } else { 9336 return false; 9337 } 9338 } 9339 9340 /** 9341 * Returns the site identifier 9342 * 9343 * @return string $CFG->siteidentifier, first making sure it is properly initialised. 9344 */ 9345 function get_site_identifier() { 9346 global $CFG; 9347 // Check to see if it is missing. If so, initialise it. 9348 if (empty($CFG->siteidentifier)) { 9349 set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']); 9350 } 9351 // Return it. 9352 return $CFG->siteidentifier; 9353 } 9354 9355 /** 9356 * Check whether the given password has no more than the specified 9357 * number of consecutive identical characters. 9358 * 9359 * @param string $password password to be checked against the password policy 9360 * @param integer $maxchars maximum number of consecutive identical characters 9361 * @return bool 9362 */ 9363 function check_consecutive_identical_characters($password, $maxchars) { 9364 9365 if ($maxchars < 1) { 9366 return true; // Zero 0 is to disable this check. 9367 } 9368 if (strlen($password) <= $maxchars) { 9369 return true; // Too short to fail this test. 9370 } 9371 9372 $previouschar = ''; 9373 $consecutivecount = 1; 9374 foreach (str_split($password) as $char) { 9375 if ($char != $previouschar) { 9376 $consecutivecount = 1; 9377 } else { 9378 $consecutivecount++; 9379 if ($consecutivecount > $maxchars) { 9380 return false; // Check failed already. 9381 } 9382 } 9383 9384 $previouschar = $char; 9385 } 9386 9387 return true; 9388 } 9389 9390 /** 9391 * Helper function to do partial function binding. 9392 * so we can use it for preg_replace_callback, for example 9393 * this works with php functions, user functions, static methods and class methods 9394 * it returns you a callback that you can pass on like so: 9395 * 9396 * $callback = partial('somefunction', $arg1, $arg2); 9397 * or 9398 * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2); 9399 * or even 9400 * $obj = new someclass(); 9401 * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2); 9402 * 9403 * and then the arguments that are passed through at calltime are appended to the argument list. 9404 * 9405 * @param mixed $function a php callback 9406 * @param mixed $arg1,... $argv arguments to partially bind with 9407 * @return array Array callback 9408 */ 9409 function partial() { 9410 if (!class_exists('partial')) { 9411 /** 9412 * Used to manage function binding. 9413 * @copyright 2009 Penny Leach 9414 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 9415 */ 9416 class partial{ 9417 /** @var array */ 9418 public $values = array(); 9419 /** @var string The function to call as a callback. */ 9420 public $func; 9421 /** 9422 * Constructor 9423 * @param string $func 9424 * @param array $args 9425 */ 9426 public function __construct($func, $args) { 9427 $this->values = $args; 9428 $this->func = $func; 9429 } 9430 /** 9431 * Calls the callback function. 9432 * @return mixed 9433 */ 9434 public function method() { 9435 $args = func_get_args(); 9436 return call_user_func_array($this->func, array_merge($this->values, $args)); 9437 } 9438 } 9439 } 9440 $args = func_get_args(); 9441 $func = array_shift($args); 9442 $p = new partial($func, $args); 9443 return array($p, 'method'); 9444 } 9445 9446 /** 9447 * helper function to load up and initialise the mnet environment 9448 * this must be called before you use mnet functions. 9449 * 9450 * @return mnet_environment the equivalent of old $MNET global 9451 */ 9452 function get_mnet_environment() { 9453 global $CFG; 9454 require_once($CFG->dirroot . '/mnet/lib.php'); 9455 static $instance = null; 9456 if (empty($instance)) { 9457 $instance = new mnet_environment(); 9458 $instance->init(); 9459 } 9460 return $instance; 9461 } 9462 9463 /** 9464 * during xmlrpc server code execution, any code wishing to access 9465 * information about the remote peer must use this to get it. 9466 * 9467 * @return mnet_remote_client the equivalent of old $MNETREMOTE_CLIENT global 9468 */ 9469 function get_mnet_remote_client() { 9470 if (!defined('MNET_SERVER')) { 9471 debugging(get_string('notinxmlrpcserver', 'mnet')); 9472 return false; 9473 } 9474 global $MNET_REMOTE_CLIENT; 9475 if (isset($MNET_REMOTE_CLIENT)) { 9476 return $MNET_REMOTE_CLIENT; 9477 } 9478 return false; 9479 } 9480 9481 /** 9482 * during the xmlrpc server code execution, this will be called 9483 * to setup the object returned by {@link get_mnet_remote_client} 9484 * 9485 * @param mnet_remote_client $client the client to set up 9486 * @throws moodle_exception 9487 */ 9488 function set_mnet_remote_client($client) { 9489 if (!defined('MNET_SERVER')) { 9490 throw new moodle_exception('notinxmlrpcserver', 'mnet'); 9491 } 9492 global $MNET_REMOTE_CLIENT; 9493 $MNET_REMOTE_CLIENT = $client; 9494 } 9495 9496 /** 9497 * return the jump url for a given remote user 9498 * this is used for rewriting forum post links in emails, etc 9499 * 9500 * @param stdclass $user the user to get the idp url for 9501 */ 9502 function mnet_get_idp_jump_url($user) { 9503 global $CFG; 9504 9505 static $mnetjumps = array(); 9506 if (!array_key_exists($user->mnethostid, $mnetjumps)) { 9507 $idp = mnet_get_peer_host($user->mnethostid); 9508 $idpjumppath = mnet_get_app_jumppath($idp->applicationid); 9509 $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl='; 9510 } 9511 return $mnetjumps[$user->mnethostid]; 9512 } 9513 9514 /** 9515 * Gets the homepage to use for the current user 9516 * 9517 * @return int One of HOMEPAGE_* 9518 */ 9519 function get_home_page() { 9520 global $CFG; 9521 9522 if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) { 9523 if ($CFG->defaulthomepage == HOMEPAGE_MY) { 9524 return HOMEPAGE_MY; 9525 } else { 9526 return (int)get_user_preferences('user_home_page_preference', HOMEPAGE_MY); 9527 } 9528 } 9529 return HOMEPAGE_SITE; 9530 } 9531 9532 /** 9533 * Gets the name of a course to be displayed when showing a list of courses. 9534 * By default this is just $course->fullname but user can configure it. The 9535 * result of this function should be passed through print_string. 9536 * @param stdClass|course_in_list $course Moodle course object 9537 * @return string Display name of course (either fullname or short + fullname) 9538 */ 9539 function get_course_display_name_for_list($course) { 9540 global $CFG; 9541 if (!empty($CFG->courselistshortnames)) { 9542 if (!($course instanceof stdClass)) { 9543 $course = (object)convert_to_array($course); 9544 } 9545 return get_string('courseextendednamedisplay', '', $course); 9546 } else { 9547 return $course->fullname; 9548 } 9549 } 9550 9551 /** 9552 * The lang_string class 9553 * 9554 * This special class is used to create an object representation of a string request. 9555 * It is special because processing doesn't occur until the object is first used. 9556 * The class was created especially to aid performance in areas where strings were 9557 * required to be generated but were not necessarily used. 9558 * As an example the admin tree when generated uses over 1500 strings, of which 9559 * normally only 1/3 are ever actually printed at any time. 9560 * The performance advantage is achieved by not actually processing strings that 9561 * arn't being used, as such reducing the processing required for the page. 9562 * 9563 * How to use the lang_string class? 9564 * There are two methods of using the lang_string class, first through the 9565 * forth argument of the get_string function, and secondly directly. 9566 * The following are examples of both. 9567 * 1. Through get_string calls e.g. 9568 * $string = get_string($identifier, $component, $a, true); 9569 * $string = get_string('yes', 'moodle', null, true); 9570 * 2. Direct instantiation 9571 * $string = new lang_string($identifier, $component, $a, $lang); 9572 * $string = new lang_string('yes'); 9573 * 9574 * How do I use a lang_string object? 9575 * The lang_string object makes use of a magic __toString method so that you 9576 * are able to use the object exactly as you would use a string in most cases. 9577 * This means you are able to collect it into a variable and then directly 9578 * echo it, or concatenate it into another string, or similar. 9579 * The other thing you can do is manually get the string by calling the 9580 * lang_strings out method e.g. 9581 * $string = new lang_string('yes'); 9582 * $string->out(); 9583 * Also worth noting is that the out method can take one argument, $lang which 9584 * allows the developer to change the language on the fly. 9585 * 9586 * When should I use a lang_string object? 9587 * The lang_string object is designed to be used in any situation where a 9588 * string may not be needed, but needs to be generated. 9589 * The admin tree is a good example of where lang_string objects should be 9590 * used. 9591 * A more practical example would be any class that requries strings that may 9592 * not be printed (after all classes get renderer by renderers and who knows 9593 * what they will do ;)) 9594 * 9595 * When should I not use a lang_string object? 9596 * Don't use lang_strings when you are going to use a string immediately. 9597 * There is no need as it will be processed immediately and there will be no 9598 * advantage, and in fact perhaps a negative hit as a class has to be 9599 * instantiated for a lang_string object, however get_string won't require 9600 * that. 9601 * 9602 * Limitations: 9603 * 1. You cannot use a lang_string object as an array offset. Doing so will 9604 * result in PHP throwing an error. (You can use it as an object property!) 9605 * 9606 * @package core 9607 * @category string 9608 * @copyright 2011 Sam Hemelryk 9609 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 9610 */ 9611 class lang_string { 9612 9613 /** @var string The strings identifier */ 9614 protected $identifier; 9615 /** @var string The strings component. Default '' */ 9616 protected $component = ''; 9617 /** @var array|stdClass Any arguments required for the string. Default null */ 9618 protected $a = null; 9619 /** @var string The language to use when processing the string. Default null */ 9620 protected $lang = null; 9621 9622 /** @var string The processed string (once processed) */ 9623 protected $string = null; 9624 9625 /** 9626 * A special boolean. If set to true then the object has been woken up and 9627 * cannot be regenerated. If this is set then $this->string MUST be used. 9628 * @var bool 9629 */ 9630 protected $forcedstring = false; 9631 9632 /** 9633 * Constructs a lang_string object 9634 * 9635 * This function should do as little processing as possible to ensure the best 9636 * performance for strings that won't be used. 9637 * 9638 * @param string $identifier The strings identifier 9639 * @param string $component The strings component 9640 * @param stdClass|array $a Any arguments the string requires 9641 * @param string $lang The language to use when processing the string. 9642 * @throws coding_exception 9643 */ 9644 public function __construct($identifier, $component = '', $a = null, $lang = null) { 9645 if (empty($component)) { 9646 $component = 'moodle'; 9647 } 9648 9649 $this->identifier = $identifier; 9650 $this->component = $component; 9651 $this->lang = $lang; 9652 9653 // We MUST duplicate $a to ensure that it if it changes by reference those 9654 // changes are not carried across. 9655 // To do this we always ensure $a or its properties/values are strings 9656 // and that any properties/values that arn't convertable are forgotten. 9657 if (!empty($a)) { 9658 if (is_scalar($a)) { 9659 $this->a = $a; 9660 } else if ($a instanceof lang_string) { 9661 $this->a = $a->out(); 9662 } else if (is_object($a) or is_array($a)) { 9663 $a = (array)$a; 9664 $this->a = array(); 9665 foreach ($a as $key => $value) { 9666 // Make sure conversion errors don't get displayed (results in ''). 9667 if (is_array($value)) { 9668 $this->a[$key] = ''; 9669 } else if (is_object($value)) { 9670 if (method_exists($value, '__toString')) { 9671 $this->a[$key] = $value->__toString(); 9672 } else { 9673 $this->a[$key] = ''; 9674 } 9675 } else { 9676 $this->a[$key] = (string)$value; 9677 } 9678 } 9679 } 9680 } 9681 9682 if (debugging(false, DEBUG_DEVELOPER)) { 9683 if (clean_param($this->identifier, PARAM_STRINGID) == '') { 9684 throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition'); 9685 } 9686 if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') { 9687 throw new coding_exception('Invalid string compontent. Please check your string definition'); 9688 } 9689 if (!get_string_manager()->string_exists($this->identifier, $this->component)) { 9690 debugging('String does not exist. Please check your string definition for '.$this->identifier.'/'.$this->component, DEBUG_DEVELOPER); 9691 } 9692 } 9693 } 9694 9695 /** 9696 * Processes the string. 9697 * 9698 * This function actually processes the string, stores it in the string property 9699 * and then returns it. 9700 * You will notice that this function is VERY similar to the get_string method. 9701 * That is because it is pretty much doing the same thing. 9702 * However as this function is an upgrade it isn't as tolerant to backwards 9703 * compatibility. 9704 * 9705 * @return string 9706 * @throws coding_exception 9707 */ 9708 protected function get_string() { 9709 global $CFG; 9710 9711 // Check if we need to process the string. 9712 if ($this->string === null) { 9713 // Check the quality of the identifier. 9714 if ($CFG->debugdeveloper && clean_param($this->identifier, PARAM_STRINGID) === '') { 9715 throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition', DEBUG_DEVELOPER); 9716 } 9717 9718 // Process the string. 9719 $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang); 9720 // Debugging feature lets you display string identifier and component. 9721 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) { 9722 $this->string .= ' {' . $this->identifier . '/' . $this->component . '}'; 9723 } 9724 } 9725 // Return the string. 9726 return $this->string; 9727 } 9728 9729 /** 9730 * Returns the string 9731 * 9732 * @param string $lang The langauge to use when processing the string 9733 * @return string 9734 */ 9735 public function out($lang = null) { 9736 if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) { 9737 if ($this->forcedstring) { 9738 debugging('lang_string objects that have been used cannot be printed in another language. ('.$this->lang.' used)', DEBUG_DEVELOPER); 9739 return $this->get_string(); 9740 } 9741 $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang); 9742 return $translatedstring->out(); 9743 } 9744 return $this->get_string(); 9745 } 9746 9747 /** 9748 * Magic __toString method for printing a string 9749 * 9750 * @return string 9751 */ 9752 public function __toString() { 9753 return $this->get_string(); 9754 } 9755 9756 /** 9757 * Magic __set_state method used for var_export 9758 * 9759 * @return string 9760 */ 9761 public function __set_state() { 9762 return $this->get_string(); 9763 } 9764 9765 /** 9766 * Prepares the lang_string for sleep and stores only the forcedstring and 9767 * string properties... the string cannot be regenerated so we need to ensure 9768 * it is generated for this. 9769 * 9770 * @return string 9771 */ 9772 public function __sleep() { 9773 $this->get_string(); 9774 $this->forcedstring = true; 9775 return array('forcedstring', 'string', 'lang'); 9776 } 9777 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |