See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]
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 (English ascii letters [a-zA-Z]) 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 0-9 and English ascii letters [a-zA-Z] only. 87 */ 88 define('PARAM_ALPHANUM', 'alphanum'); 89 90 /** 91 * PARAM_ALPHANUMEXT - expected numbers 0-9, letters (English ascii letters [a-zA-Z]) and _- only. 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 receiving 119 * the input (required/optional_param or formslib) and then sanitise 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 * Use PARAM_LOCALISEDFLOAT instead. 141 */ 142 define('PARAM_FLOAT', 'float'); 143 144 /** 145 * PARAM_LOCALISEDFLOAT - a localised real/floating point number. 146 * This is preferred over PARAM_FLOAT for numbers typed in by the user. 147 * Cleans localised numbers to computer readable numbers; false for invalid numbers. 148 */ 149 define('PARAM_LOCALISEDFLOAT', 'localisedfloat'); 150 151 /** 152 * PARAM_HOST - expected fully qualified domain name (FQDN) or an IPv4 dotted quad (IP address) 153 */ 154 define('PARAM_HOST', 'host'); 155 156 /** 157 * PARAM_INT - integers only, use when expecting only numbers. 158 */ 159 define('PARAM_INT', 'int'); 160 161 /** 162 * PARAM_LANG - checks to see if the string is a valid installed language in the current site. 163 */ 164 define('PARAM_LANG', 'lang'); 165 166 /** 167 * PARAM_LOCALURL - expected properly formatted URL as well as one that refers to the local server itself. (NOT orthogonal to the 168 * others! Implies PARAM_URL!) 169 */ 170 define('PARAM_LOCALURL', 'localurl'); 171 172 /** 173 * PARAM_NOTAGS - all html tags are stripped from the text. Do not abuse this type. 174 */ 175 define('PARAM_NOTAGS', 'notags'); 176 177 /** 178 * PARAM_PATH - safe relative path name, all dangerous chars are stripped, protects against XSS, SQL injections and directory 179 * traversals note: the leading slash is not removed, window drive letter is not allowed 180 */ 181 define('PARAM_PATH', 'path'); 182 183 /** 184 * PARAM_PEM - Privacy Enhanced Mail format 185 */ 186 define('PARAM_PEM', 'pem'); 187 188 /** 189 * PARAM_PERMISSION - A permission, one of CAP_INHERIT, CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT. 190 */ 191 define('PARAM_PERMISSION', 'permission'); 192 193 /** 194 * PARAM_RAW specifies a parameter that is not cleaned/processed in any way except the discarding of the invalid utf-8 characters 195 */ 196 define('PARAM_RAW', 'raw'); 197 198 /** 199 * PARAM_RAW_TRIMMED like PARAM_RAW but leading and trailing whitespace is stripped. 200 */ 201 define('PARAM_RAW_TRIMMED', 'raw_trimmed'); 202 203 /** 204 * PARAM_SAFEDIR - safe directory name, suitable for include() and require() 205 */ 206 define('PARAM_SAFEDIR', 'safedir'); 207 208 /** 209 * PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths 210 * and other references to Moodle code files. 211 * 212 * This is NOT intended to be used for absolute paths or any user uploaded files. 213 */ 214 define('PARAM_SAFEPATH', 'safepath'); 215 216 /** 217 * PARAM_SEQUENCE - expects a sequence of numbers like 8 to 1,5,6,4,6,8,9. Numbers and comma only. 218 */ 219 define('PARAM_SEQUENCE', 'sequence'); 220 221 /** 222 * PARAM_TAG - one tag (interests, blogs, etc.) - mostly international characters and space, <> not supported 223 */ 224 define('PARAM_TAG', 'tag'); 225 226 /** 227 * PARAM_TAGLIST - list of tags separated by commas (interests, blogs, etc.) 228 */ 229 define('PARAM_TAGLIST', 'taglist'); 230 231 /** 232 * PARAM_TEXT - general plain text compatible with multilang filter, no other html tags. Please note '<', or '>' are allowed here. 233 */ 234 define('PARAM_TEXT', 'text'); 235 236 /** 237 * PARAM_THEME - Checks to see if the string is a valid theme name in the current site 238 */ 239 define('PARAM_THEME', 'theme'); 240 241 /** 242 * PARAM_URL - expected properly formatted URL. Please note that domain part is required, http://localhost/ is not accepted but 243 * http://localhost.localdomain/ is ok. 244 */ 245 define('PARAM_URL', 'url'); 246 247 /** 248 * PARAM_USERNAME - Clean username to only contains allowed characters. This is to be used ONLY when manually creating user 249 * accounts, do NOT use when syncing with external systems!! 250 */ 251 define('PARAM_USERNAME', 'username'); 252 253 /** 254 * PARAM_STRINGID - used to check if the given string is valid string identifier for get_string() 255 */ 256 define('PARAM_STRINGID', 'stringid'); 257 258 // DEPRECATED PARAM TYPES OR ALIASES - DO NOT USE FOR NEW CODE. 259 /** 260 * PARAM_CLEAN - obsoleted, please use a more specific type of parameter. 261 * It was one of the first types, that is why it is abused so much ;-) 262 * @deprecated since 2.0 263 */ 264 define('PARAM_CLEAN', 'clean'); 265 266 /** 267 * PARAM_INTEGER - deprecated alias for PARAM_INT 268 * @deprecated since 2.0 269 */ 270 define('PARAM_INTEGER', 'int'); 271 272 /** 273 * PARAM_NUMBER - deprecated alias of PARAM_FLOAT 274 * @deprecated since 2.0 275 */ 276 define('PARAM_NUMBER', 'float'); 277 278 /** 279 * PARAM_ACTION - deprecated alias for PARAM_ALPHANUMEXT, use for various actions in forms and urls 280 * NOTE: originally alias for PARAM_APLHA 281 * @deprecated since 2.0 282 */ 283 define('PARAM_ACTION', 'alphanumext'); 284 285 /** 286 * PARAM_FORMAT - deprecated alias for PARAM_ALPHANUMEXT, use for names of plugins, formats, etc. 287 * NOTE: originally alias for PARAM_APLHA 288 * @deprecated since 2.0 289 */ 290 define('PARAM_FORMAT', 'alphanumext'); 291 292 /** 293 * PARAM_MULTILANG - deprecated alias of PARAM_TEXT. 294 * @deprecated since 2.0 295 */ 296 define('PARAM_MULTILANG', 'text'); 297 298 /** 299 * PARAM_TIMEZONE - expected timezone. Timezone can be int +-(0-13) or float +-(0.5-12.5) or 300 * string separated by '/' and can have '-' &/ '_' (eg. America/North_Dakota/New_Salem 301 * America/Port-au-Prince) 302 */ 303 define('PARAM_TIMEZONE', 'timezone'); 304 305 /** 306 * PARAM_CLEANFILE - deprecated alias of PARAM_FILE; originally was removing regional chars too 307 */ 308 define('PARAM_CLEANFILE', 'file'); 309 310 /** 311 * PARAM_COMPONENT is used for full component names (aka frankenstyle) such as 'mod_forum', 'core_rating', 'auth_ldap'. 312 * Short legacy subsystem names and module names are accepted too ex: 'forum', 'rating', 'user'. 313 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter. 314 * NOTE: numbers and underscores are strongly discouraged in plugin names! 315 */ 316 define('PARAM_COMPONENT', 'component'); 317 318 /** 319 * PARAM_AREA is a name of area used when addressing files, comments, ratings, etc. 320 * It is usually used together with context id and component. 321 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter. 322 */ 323 define('PARAM_AREA', 'area'); 324 325 /** 326 * PARAM_PLUGIN is used for plugin names such as 'forum', 'glossary', 'ldap', 'paypal', 'completionstatus'. 327 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter. 328 * NOTE: numbers and underscores are strongly discouraged in plugin names! Underscores are forbidden in module names. 329 */ 330 define('PARAM_PLUGIN', 'plugin'); 331 332 333 // Web Services. 334 335 /** 336 * VALUE_REQUIRED - if the parameter is not supplied, there is an error 337 */ 338 define('VALUE_REQUIRED', 1); 339 340 /** 341 * VALUE_OPTIONAL - if the parameter is not supplied, then the param has no value 342 */ 343 define('VALUE_OPTIONAL', 2); 344 345 /** 346 * VALUE_DEFAULT - if the parameter is not supplied, then the default value is used 347 */ 348 define('VALUE_DEFAULT', 0); 349 350 /** 351 * NULL_NOT_ALLOWED - the parameter can not be set to null in the database 352 */ 353 define('NULL_NOT_ALLOWED', false); 354 355 /** 356 * NULL_ALLOWED - the parameter can be set to null in the database 357 */ 358 define('NULL_ALLOWED', true); 359 360 // Page types. 361 362 /** 363 * PAGE_COURSE_VIEW is a definition of a page type. For more information on the page class see moodle/lib/pagelib.php. 364 */ 365 define('PAGE_COURSE_VIEW', 'course-view'); 366 367 /** Get remote addr constant */ 368 define('GETREMOTEADDR_SKIP_HTTP_CLIENT_IP', '1'); 369 /** Get remote addr constant */ 370 define('GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR', '2'); 371 /** 372 * GETREMOTEADDR_SKIP_DEFAULT defines the default behavior remote IP address validation. 373 */ 374 define('GETREMOTEADDR_SKIP_DEFAULT', GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR|GETREMOTEADDR_SKIP_HTTP_CLIENT_IP); 375 376 // Blog access level constant declaration. 377 define ('BLOG_USER_LEVEL', 1); 378 define ('BLOG_GROUP_LEVEL', 2); 379 define ('BLOG_COURSE_LEVEL', 3); 380 define ('BLOG_SITE_LEVEL', 4); 381 define ('BLOG_GLOBAL_LEVEL', 5); 382 383 384 // Tag constants. 385 /** 386 * To prevent problems with multibytes strings,Flag updating in nav not working on the review page. this should not exceed the 387 * length of "varchar(255) / 3 (bytes / utf-8 character) = 85". 388 * TODO: this is not correct, varchar(255) are 255 unicode chars ;-) 389 * 390 * @todo define(TAG_MAX_LENGTH) this is not correct, varchar(255) are 255 unicode chars ;-) 391 */ 392 define('TAG_MAX_LENGTH', 50); 393 394 // Password policy constants. 395 define ('PASSWORD_LOWER', 'abcdefghijklmnopqrstuvwxyz'); 396 define ('PASSWORD_UPPER', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'); 397 define ('PASSWORD_DIGITS', '0123456789'); 398 define ('PASSWORD_NONALPHANUM', '.,;:!?_-+/*@#&$'); 399 400 // Feature constants. 401 // Used for plugin_supports() to report features that are, or are not, supported by a module. 402 403 /** True if module can provide a grade */ 404 define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade'); 405 /** True if module supports outcomes */ 406 define('FEATURE_GRADE_OUTCOMES', 'outcomes'); 407 /** True if module supports advanced grading methods */ 408 define('FEATURE_ADVANCED_GRADING', 'grade_advanced_grading'); 409 /** True if module controls the grade visibility over the gradebook */ 410 define('FEATURE_CONTROLS_GRADE_VISIBILITY', 'controlsgradevisbility'); 411 /** True if module supports plagiarism plugins */ 412 define('FEATURE_PLAGIARISM', 'plagiarism'); 413 414 /** True if module has code to track whether somebody viewed it */ 415 define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views'); 416 /** True if module has custom completion rules */ 417 define('FEATURE_COMPLETION_HAS_RULES', 'completion_has_rules'); 418 419 /** True if module has no 'view' page (like label) */ 420 define('FEATURE_NO_VIEW_LINK', 'viewlink'); 421 /** True (which is default) if the module wants support for setting the ID number for grade calculation purposes. */ 422 define('FEATURE_IDNUMBER', 'idnumber'); 423 /** True if module supports groups */ 424 define('FEATURE_GROUPS', 'groups'); 425 /** True if module supports groupings */ 426 define('FEATURE_GROUPINGS', 'groupings'); 427 /** 428 * True if module supports groupmembersonly (which no longer exists) 429 * @deprecated Since Moodle 2.8 430 */ 431 define('FEATURE_GROUPMEMBERSONLY', 'groupmembersonly'); 432 433 /** Type of module */ 434 define('FEATURE_MOD_ARCHETYPE', 'mod_archetype'); 435 /** True if module supports intro editor */ 436 define('FEATURE_MOD_INTRO', 'mod_intro'); 437 /** True if module has default completion */ 438 define('FEATURE_MODEDIT_DEFAULT_COMPLETION', 'modedit_default_completion'); 439 440 define('FEATURE_COMMENT', 'comment'); 441 442 define('FEATURE_RATE', 'rate'); 443 /** True if module supports backup/restore of moodle2 format */ 444 define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2'); 445 446 /** True if module can show description on course main page */ 447 define('FEATURE_SHOW_DESCRIPTION', 'showdescription'); 448 449 /** True if module uses the question bank */ 450 define('FEATURE_USES_QUESTIONS', 'usesquestions'); 451 452 /** 453 * Maximum filename char size 454 */ 455 define('MAX_FILENAME_SIZE', 100); 456 457 /** Unspecified module archetype */ 458 define('MOD_ARCHETYPE_OTHER', 0); 459 /** Resource-like type module */ 460 define('MOD_ARCHETYPE_RESOURCE', 1); 461 /** Assignment module archetype */ 462 define('MOD_ARCHETYPE_ASSIGNMENT', 2); 463 /** System (not user-addable) module archetype */ 464 define('MOD_ARCHETYPE_SYSTEM', 3); 465 466 /** 467 * Security token used for allowing access 468 * from external application such as web services. 469 * Scripts do not use any session, performance is relatively 470 * low because we need to load access info in each request. 471 * Scripts are executed in parallel. 472 */ 473 define('EXTERNAL_TOKEN_PERMANENT', 0); 474 475 /** 476 * Security token used for allowing access 477 * of embedded applications, the code is executed in the 478 * active user session. Token is invalidated after user logs out. 479 * Scripts are executed serially - normal session locking is used. 480 */ 481 define('EXTERNAL_TOKEN_EMBEDDED', 1); 482 483 /** 484 * The home page should be the site home 485 */ 486 define('HOMEPAGE_SITE', 0); 487 /** 488 * The home page should be the users my page 489 */ 490 define('HOMEPAGE_MY', 1); 491 /** 492 * The home page can be chosen by the user 493 */ 494 define('HOMEPAGE_USER', 2); 495 496 /** 497 * URL of the Moodle sites registration portal. 498 */ 499 defined('HUB_MOODLEORGHUBURL') || define('HUB_MOODLEORGHUBURL', 'https://stats.moodle.org'); 500 501 /** 502 * Moodle mobile app service name 503 */ 504 define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app'); 505 506 /** 507 * Indicates the user has the capabilities required to ignore activity and course file size restrictions 508 */ 509 define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1); 510 511 /** 512 * Course display settings: display all sections on one page. 513 */ 514 define('COURSE_DISPLAY_SINGLEPAGE', 0); 515 /** 516 * Course display settings: split pages into a page per section. 517 */ 518 define('COURSE_DISPLAY_MULTIPAGE', 1); 519 520 /** 521 * Authentication constant: String used in password field when password is not stored. 522 */ 523 define('AUTH_PASSWORD_NOT_CACHED', 'not cached'); 524 525 /** 526 * Email from header to never include via information. 527 */ 528 define('EMAIL_VIA_NEVER', 0); 529 530 /** 531 * Email from header to always include via information. 532 */ 533 define('EMAIL_VIA_ALWAYS', 1); 534 535 /** 536 * Email from header to only include via information if the address is no-reply. 537 */ 538 define('EMAIL_VIA_NO_REPLY_ONLY', 2); 539 540 // PARAMETER HANDLING. 541 542 /** 543 * Returns a particular value for the named variable, taken from 544 * POST or GET. If the parameter doesn't exist then an error is 545 * thrown because we require this variable. 546 * 547 * This function should be used to initialise all required values 548 * in a script that are based on parameters. Usually it will be 549 * used like this: 550 * $id = required_param('id', PARAM_INT); 551 * 552 * Please note the $type parameter is now required and the value can not be array. 553 * 554 * @param string $parname the name of the page parameter we want 555 * @param string $type expected type of parameter 556 * @return mixed 557 * @throws coding_exception 558 */ 559 function required_param($parname, $type) { 560 if (func_num_args() != 2 or empty($parname) or empty($type)) { 561 throw new coding_exception('required_param() requires $parname and $type to be specified (parameter: '.$parname.')'); 562 } 563 // POST has precedence. 564 if (isset($_POST[$parname])) { 565 $param = $_POST[$parname]; 566 } else if (isset($_GET[$parname])) { 567 $param = $_GET[$parname]; 568 } else { 569 print_error('missingparam', '', '', $parname); 570 } 571 572 if (is_array($param)) { 573 debugging('Invalid array parameter detected in required_param(): '.$parname); 574 // TODO: switch to fatal error in Moodle 2.3. 575 return required_param_array($parname, $type); 576 } 577 578 return clean_param($param, $type); 579 } 580 581 /** 582 * Returns a particular array value for the named variable, taken from 583 * POST or GET. If the parameter doesn't exist then an error is 584 * thrown because we require this variable. 585 * 586 * This function should be used to initialise all required values 587 * in a script that are based on parameters. Usually it will be 588 * used like this: 589 * $ids = required_param_array('ids', PARAM_INT); 590 * 591 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported 592 * 593 * @param string $parname the name of the page parameter we want 594 * @param string $type expected type of parameter 595 * @return array 596 * @throws coding_exception 597 */ 598 function required_param_array($parname, $type) { 599 if (func_num_args() != 2 or empty($parname) or empty($type)) { 600 throw new coding_exception('required_param_array() requires $parname and $type to be specified (parameter: '.$parname.')'); 601 } 602 // POST has precedence. 603 if (isset($_POST[$parname])) { 604 $param = $_POST[$parname]; 605 } else if (isset($_GET[$parname])) { 606 $param = $_GET[$parname]; 607 } else { 608 print_error('missingparam', '', '', $parname); 609 } 610 if (!is_array($param)) { 611 print_error('missingparam', '', '', $parname); 612 } 613 614 $result = array(); 615 foreach ($param as $key => $value) { 616 if (!preg_match('/^[a-z0-9_-]+$/i', $key)) { 617 debugging('Invalid key name in required_param_array() detected: '.$key.', parameter: '.$parname); 618 continue; 619 } 620 $result[$key] = clean_param($value, $type); 621 } 622 623 return $result; 624 } 625 626 /** 627 * Returns a particular value for the named variable, taken from 628 * POST or GET, otherwise returning a given default. 629 * 630 * This function should be used to initialise all optional values 631 * in a script that are based on parameters. Usually it will be 632 * used like this: 633 * $name = optional_param('name', 'Fred', PARAM_TEXT); 634 * 635 * Please note the $type parameter is now required and the value can not be array. 636 * 637 * @param string $parname the name of the page parameter we want 638 * @param mixed $default the default value to return if nothing is found 639 * @param string $type expected type of parameter 640 * @return mixed 641 * @throws coding_exception 642 */ 643 function optional_param($parname, $default, $type) { 644 if (func_num_args() != 3 or empty($parname) or empty($type)) { 645 throw new coding_exception('optional_param requires $parname, $default + $type to be specified (parameter: '.$parname.')'); 646 } 647 648 // POST has precedence. 649 if (isset($_POST[$parname])) { 650 $param = $_POST[$parname]; 651 } else if (isset($_GET[$parname])) { 652 $param = $_GET[$parname]; 653 } else { 654 return $default; 655 } 656 657 if (is_array($param)) { 658 debugging('Invalid array parameter detected in required_param(): '.$parname); 659 // TODO: switch to $default in Moodle 2.3. 660 return optional_param_array($parname, $default, $type); 661 } 662 663 return clean_param($param, $type); 664 } 665 666 /** 667 * Returns a particular array value for the named variable, taken from 668 * POST or GET, otherwise returning a given default. 669 * 670 * This function should be used to initialise all optional values 671 * in a script that are based on parameters. Usually it will be 672 * used like this: 673 * $ids = optional_param('id', array(), PARAM_INT); 674 * 675 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported 676 * 677 * @param string $parname the name of the page parameter we want 678 * @param mixed $default the default value to return if nothing is found 679 * @param string $type expected type of parameter 680 * @return array 681 * @throws coding_exception 682 */ 683 function optional_param_array($parname, $default, $type) { 684 if (func_num_args() != 3 or empty($parname) or empty($type)) { 685 throw new coding_exception('optional_param_array requires $parname, $default + $type to be specified (parameter: '.$parname.')'); 686 } 687 688 // POST has precedence. 689 if (isset($_POST[$parname])) { 690 $param = $_POST[$parname]; 691 } else if (isset($_GET[$parname])) { 692 $param = $_GET[$parname]; 693 } else { 694 return $default; 695 } 696 if (!is_array($param)) { 697 debugging('optional_param_array() expects array parameters only: '.$parname); 698 return $default; 699 } 700 701 $result = array(); 702 foreach ($param as $key => $value) { 703 if (!preg_match('/^[a-z0-9_-]+$/i', $key)) { 704 debugging('Invalid key name in optional_param_array() detected: '.$key.', parameter: '.$parname); 705 continue; 706 } 707 $result[$key] = clean_param($value, $type); 708 } 709 710 return $result; 711 } 712 713 /** 714 * Strict validation of parameter values, the values are only converted 715 * to requested PHP type. Internally it is using clean_param, the values 716 * before and after cleaning must be equal - otherwise 717 * an invalid_parameter_exception is thrown. 718 * Objects and classes are not accepted. 719 * 720 * @param mixed $param 721 * @param string $type PARAM_ constant 722 * @param bool $allownull are nulls valid value? 723 * @param string $debuginfo optional debug information 724 * @return mixed the $param value converted to PHP type 725 * @throws invalid_parameter_exception if $param is not of given type 726 */ 727 function validate_param($param, $type, $allownull=NULL_NOT_ALLOWED, $debuginfo='') { 728 if (is_null($param)) { 729 if ($allownull == NULL_ALLOWED) { 730 return null; 731 } else { 732 throw new invalid_parameter_exception($debuginfo); 733 } 734 } 735 if (is_array($param) or is_object($param)) { 736 throw new invalid_parameter_exception($debuginfo); 737 } 738 739 $cleaned = clean_param($param, $type); 740 741 if ($type == PARAM_FLOAT) { 742 // Do not detect precision loss here. 743 if (is_float($param) or is_int($param)) { 744 // These always fit. 745 } else if (!is_numeric($param) or !preg_match('/^[\+-]?[0-9]*\.?[0-9]*(e[-+]?[0-9]+)?$/i', (string)$param)) { 746 throw new invalid_parameter_exception($debuginfo); 747 } 748 } else if ((string)$param !== (string)$cleaned) { 749 // Conversion to string is usually lossless. 750 throw new invalid_parameter_exception($debuginfo); 751 } 752 753 return $cleaned; 754 } 755 756 /** 757 * Makes sure array contains only the allowed types, this function does not validate array key names! 758 * 759 * <code> 760 * $options = clean_param($options, PARAM_INT); 761 * </code> 762 * 763 * @param array $param the variable array we are cleaning 764 * @param string $type expected format of param after cleaning. 765 * @param bool $recursive clean recursive arrays 766 * @return array 767 * @throws coding_exception 768 */ 769 function clean_param_array(array $param = null, $type, $recursive = false) { 770 // Convert null to empty array. 771 $param = (array)$param; 772 foreach ($param as $key => $value) { 773 if (is_array($value)) { 774 if ($recursive) { 775 $param[$key] = clean_param_array($value, $type, true); 776 } else { 777 throw new coding_exception('clean_param_array can not process multidimensional arrays when $recursive is false.'); 778 } 779 } else { 780 $param[$key] = clean_param($value, $type); 781 } 782 } 783 return $param; 784 } 785 786 /** 787 * Used by {@link optional_param()} and {@link required_param()} to 788 * clean the variables and/or cast to specific types, based on 789 * an options field. 790 * <code> 791 * $course->format = clean_param($course->format, PARAM_ALPHA); 792 * $selectedgradeitem = clean_param($selectedgradeitem, PARAM_INT); 793 * </code> 794 * 795 * @param mixed $param the variable we are cleaning 796 * @param string $type expected format of param after cleaning. 797 * @return mixed 798 * @throws coding_exception 799 */ 800 function clean_param($param, $type) { 801 global $CFG; 802 803 if (is_array($param)) { 804 throw new coding_exception('clean_param() can not process arrays, please use clean_param_array() instead.'); 805 } else if (is_object($param)) { 806 if (method_exists($param, '__toString')) { 807 $param = $param->__toString(); 808 } else { 809 throw new coding_exception('clean_param() can not process objects, please use clean_param_array() instead.'); 810 } 811 } 812 813 switch ($type) { 814 case PARAM_RAW: 815 // No cleaning at all. 816 $param = fix_utf8($param); 817 return $param; 818 819 case PARAM_RAW_TRIMMED: 820 // No cleaning, but strip leading and trailing whitespace. 821 $param = fix_utf8($param); 822 return trim($param); 823 824 case PARAM_CLEAN: 825 // General HTML cleaning, try to use more specific type if possible this is deprecated! 826 // Please use more specific type instead. 827 if (is_numeric($param)) { 828 return $param; 829 } 830 $param = fix_utf8($param); 831 // Sweep for scripts, etc. 832 return clean_text($param); 833 834 case PARAM_CLEANHTML: 835 // Clean html fragment. 836 $param = fix_utf8($param); 837 // Sweep for scripts, etc. 838 $param = clean_text($param, FORMAT_HTML); 839 return trim($param); 840 841 case PARAM_INT: 842 // Convert to integer. 843 return (int)$param; 844 845 case PARAM_FLOAT: 846 // Convert to float. 847 return (float)$param; 848 849 case PARAM_LOCALISEDFLOAT: 850 // Convert to float. 851 return unformat_float($param, true); 852 853 case PARAM_ALPHA: 854 // Remove everything not `a-z`. 855 return preg_replace('/[^a-zA-Z]/i', '', $param); 856 857 case PARAM_ALPHAEXT: 858 // Remove everything not `a-zA-Z_-` (originally allowed "/" too). 859 return preg_replace('/[^a-zA-Z_-]/i', '', $param); 860 861 case PARAM_ALPHANUM: 862 // Remove everything not `a-zA-Z0-9`. 863 return preg_replace('/[^A-Za-z0-9]/i', '', $param); 864 865 case PARAM_ALPHANUMEXT: 866 // Remove everything not `a-zA-Z0-9_-`. 867 return preg_replace('/[^A-Za-z0-9_-]/i', '', $param); 868 869 case PARAM_SEQUENCE: 870 // Remove everything not `0-9,`. 871 return preg_replace('/[^0-9,]/i', '', $param); 872 873 case PARAM_BOOL: 874 // Convert to 1 or 0. 875 $tempstr = strtolower($param); 876 if ($tempstr === 'on' or $tempstr === 'yes' or $tempstr === 'true') { 877 $param = 1; 878 } else if ($tempstr === 'off' or $tempstr === 'no' or $tempstr === 'false') { 879 $param = 0; 880 } else { 881 $param = empty($param) ? 0 : 1; 882 } 883 return $param; 884 885 case PARAM_NOTAGS: 886 // Strip all tags. 887 $param = fix_utf8($param); 888 return strip_tags($param); 889 890 case PARAM_TEXT: 891 // Leave only tags needed for multilang. 892 $param = fix_utf8($param); 893 // If the multilang syntax is not correct we strip all tags because it would break xhtml strict which is required 894 // for accessibility standards please note this cleaning does not strip unbalanced '>' for BC compatibility reasons. 895 do { 896 if (strpos($param, '</lang>') !== false) { 897 // Old and future mutilang syntax. 898 $param = strip_tags($param, '<lang>'); 899 if (!preg_match_all('/<.*>/suU', $param, $matches)) { 900 break; 901 } 902 $open = false; 903 foreach ($matches[0] as $match) { 904 if ($match === '</lang>') { 905 if ($open) { 906 $open = false; 907 continue; 908 } else { 909 break 2; 910 } 911 } 912 if (!preg_match('/^<lang lang="[a-zA-Z0-9_-]+"\s*>$/u', $match)) { 913 break 2; 914 } else { 915 $open = true; 916 } 917 } 918 if ($open) { 919 break; 920 } 921 return $param; 922 923 } else if (strpos($param, '</span>') !== false) { 924 // Current problematic multilang syntax. 925 $param = strip_tags($param, '<span>'); 926 if (!preg_match_all('/<.*>/suU', $param, $matches)) { 927 break; 928 } 929 $open = false; 930 foreach ($matches[0] as $match) { 931 if ($match === '</span>') { 932 if ($open) { 933 $open = false; 934 continue; 935 } else { 936 break 2; 937 } 938 } 939 if (!preg_match('/^<span(\s+lang="[a-zA-Z0-9_-]+"|\s+class="multilang"){2}\s*>$/u', $match)) { 940 break 2; 941 } else { 942 $open = true; 943 } 944 } 945 if ($open) { 946 break; 947 } 948 return $param; 949 } 950 } while (false); 951 // Easy, just strip all tags, if we ever want to fix orphaned '&' we have to do that in format_string(). 952 return strip_tags($param); 953 954 case PARAM_COMPONENT: 955 // We do not want any guessing here, either the name is correct or not 956 // please note only normalised component names are accepted. 957 if (!preg_match('/^[a-z][a-z0-9]*(_[a-z][a-z0-9_]*)?[a-z0-9]+$/', $param)) { 958 return ''; 959 } 960 if (strpos($param, '__') !== false) { 961 return ''; 962 } 963 if (strpos($param, 'mod_') === 0) { 964 // Module names must not contain underscores because we need to differentiate them from invalid plugin types. 965 if (substr_count($param, '_') != 1) { 966 return ''; 967 } 968 } 969 return $param; 970 971 case PARAM_PLUGIN: 972 case PARAM_AREA: 973 // We do not want any guessing here, either the name is correct or not. 974 if (!is_valid_plugin_name($param)) { 975 return ''; 976 } 977 return $param; 978 979 case PARAM_SAFEDIR: 980 // Remove everything not a-zA-Z0-9_- . 981 return preg_replace('/[^a-zA-Z0-9_-]/i', '', $param); 982 983 case PARAM_SAFEPATH: 984 // Remove everything not a-zA-Z0-9/_- . 985 return preg_replace('/[^a-zA-Z0-9\/_-]/i', '', $param); 986 987 case PARAM_FILE: 988 // Strip all suspicious characters from filename. 989 $param = fix_utf8($param); 990 $param = preg_replace('~[[:cntrl:]]|[&<>"`\|\':\\\\/]~u', '', $param); 991 if ($param === '.' || $param === '..') { 992 $param = ''; 993 } 994 return $param; 995 996 case PARAM_PATH: 997 // Strip all suspicious characters from file path. 998 $param = fix_utf8($param); 999 $param = str_replace('\\', '/', $param); 1000 1001 // Explode the path and clean each element using the PARAM_FILE rules. 1002 $breadcrumb = explode('/', $param); 1003 foreach ($breadcrumb as $key => $crumb) { 1004 if ($crumb === '.' && $key === 0) { 1005 // Special condition to allow for relative current path such as ./currentdirfile.txt. 1006 } else { 1007 $crumb = clean_param($crumb, PARAM_FILE); 1008 } 1009 $breadcrumb[$key] = $crumb; 1010 } 1011 $param = implode('/', $breadcrumb); 1012 1013 // Remove multiple current path (./././) and multiple slashes (///). 1014 $param = preg_replace('~//+~', '/', $param); 1015 $param = preg_replace('~/(\./)+~', '/', $param); 1016 return $param; 1017 1018 case PARAM_HOST: 1019 // Allow FQDN or IPv4 dotted quad. 1020 $param = preg_replace('/[^\.\d\w-]/', '', $param ); 1021 // Match ipv4 dotted quad. 1022 if (preg_match('/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/', $param, $match)) { 1023 // Confirm values are ok. 1024 if ( $match[0] > 255 1025 || $match[1] > 255 1026 || $match[3] > 255 1027 || $match[4] > 255 ) { 1028 // Hmmm, what kind of dotted quad is this? 1029 $param = ''; 1030 } 1031 } else if ( preg_match('/^[\w\d\.-]+$/', $param) // Dots, hyphens, numbers. 1032 && !preg_match('/^[\.-]/', $param) // No leading dots/hyphens. 1033 && !preg_match('/[\.-]$/', $param) // No trailing dots/hyphens. 1034 ) { 1035 // All is ok - $param is respected. 1036 } else { 1037 // All is not ok... 1038 $param=''; 1039 } 1040 return $param; 1041 1042 case PARAM_URL: 1043 // Allow safe urls. 1044 $param = fix_utf8($param); 1045 include_once($CFG->dirroot . '/lib/validateurlsyntax.php'); 1046 if (!empty($param) && validateUrlSyntax($param, 's?H?S?F?E-u-P-a?I?p?f?q?r?')) { 1047 // All is ok, param is respected. 1048 } else { 1049 // Not really ok. 1050 $param =''; 1051 } 1052 return $param; 1053 1054 case PARAM_LOCALURL: 1055 // Allow http absolute, root relative and relative URLs within wwwroot. 1056 $param = clean_param($param, PARAM_URL); 1057 if (!empty($param)) { 1058 1059 if ($param === $CFG->wwwroot) { 1060 // Exact match; 1061 } else if (preg_match(':^/:', $param)) { 1062 // Root-relative, ok! 1063 } else if (preg_match('/^' . preg_quote($CFG->wwwroot . '/', '/') . '/i', $param)) { 1064 // Absolute, and matches our wwwroot. 1065 } else { 1066 1067 // Relative - let's make sure there are no tricks. 1068 if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?') && !preg_match('/javascript:/i', $param)) { 1069 // Looks ok. 1070 } else { 1071 $param = ''; 1072 } 1073 } 1074 } 1075 return $param; 1076 1077 case PARAM_PEM: 1078 $param = trim($param); 1079 // PEM formatted strings may contain letters/numbers and the symbols: 1080 // forward slash: / 1081 // plus sign: + 1082 // equal sign: = 1083 // , surrounded by BEGIN and END CERTIFICATE prefix and suffixes. 1084 if (preg_match('/^-----BEGIN CERTIFICATE-----([\s\w\/\+=]+)-----END CERTIFICATE-----$/', trim($param), $matches)) { 1085 list($wholething, $body) = $matches; 1086 unset($wholething, $matches); 1087 $b64 = clean_param($body, PARAM_BASE64); 1088 if (!empty($b64)) { 1089 return "-----BEGIN CERTIFICATE-----\n$b64\n-----END CERTIFICATE-----\n"; 1090 } else { 1091 return ''; 1092 } 1093 } 1094 return ''; 1095 1096 case PARAM_BASE64: 1097 if (!empty($param)) { 1098 // PEM formatted strings may contain letters/numbers and the symbols 1099 // forward slash: / 1100 // plus sign: + 1101 // equal sign: =. 1102 if (0 >= preg_match('/^([\s\w\/\+=]+)$/', trim($param))) { 1103 return ''; 1104 } 1105 $lines = preg_split('/[\s]+/', $param, -1, PREG_SPLIT_NO_EMPTY); 1106 // Each line of base64 encoded data must be 64 characters in length, except for the last line which may be less 1107 // than (or equal to) 64 characters long. 1108 for ($i=0, $j=count($lines); $i < $j; $i++) { 1109 if ($i + 1 == $j) { 1110 if (64 < strlen($lines[$i])) { 1111 return ''; 1112 } 1113 continue; 1114 } 1115 1116 if (64 != strlen($lines[$i])) { 1117 return ''; 1118 } 1119 } 1120 return implode("\n", $lines); 1121 } else { 1122 return ''; 1123 } 1124 1125 case PARAM_TAG: 1126 $param = fix_utf8($param); 1127 // Please note it is not safe to use the tag name directly anywhere, 1128 // it must be processed with s(), urlencode() before embedding anywhere. 1129 // Remove some nasties. 1130 $param = preg_replace('~[[:cntrl:]]|[<>`]~u', '', $param); 1131 // Convert many whitespace chars into one. 1132 $param = preg_replace('/\s+/u', ' ', $param); 1133 $param = core_text::substr(trim($param), 0, TAG_MAX_LENGTH); 1134 return $param; 1135 1136 case PARAM_TAGLIST: 1137 $param = fix_utf8($param); 1138 $tags = explode(',', $param); 1139 $result = array(); 1140 foreach ($tags as $tag) { 1141 $res = clean_param($tag, PARAM_TAG); 1142 if ($res !== '') { 1143 $result[] = $res; 1144 } 1145 } 1146 if ($result) { 1147 return implode(',', $result); 1148 } else { 1149 return ''; 1150 } 1151 1152 case PARAM_CAPABILITY: 1153 if (get_capability_info($param)) { 1154 return $param; 1155 } else { 1156 return ''; 1157 } 1158 1159 case PARAM_PERMISSION: 1160 $param = (int)$param; 1161 if (in_array($param, array(CAP_INHERIT, CAP_ALLOW, CAP_PREVENT, CAP_PROHIBIT))) { 1162 return $param; 1163 } else { 1164 return CAP_INHERIT; 1165 } 1166 1167 case PARAM_AUTH: 1168 $param = clean_param($param, PARAM_PLUGIN); 1169 if (empty($param)) { 1170 return ''; 1171 } else if (exists_auth_plugin($param)) { 1172 return $param; 1173 } else { 1174 return ''; 1175 } 1176 1177 case PARAM_LANG: 1178 $param = clean_param($param, PARAM_SAFEDIR); 1179 if (get_string_manager()->translation_exists($param)) { 1180 return $param; 1181 } else { 1182 // Specified language is not installed or param malformed. 1183 return ''; 1184 } 1185 1186 case PARAM_THEME: 1187 $param = clean_param($param, PARAM_PLUGIN); 1188 if (empty($param)) { 1189 return ''; 1190 } else if (file_exists("$CFG->dirroot/theme/$param/config.php")) { 1191 return $param; 1192 } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$param/config.php")) { 1193 return $param; 1194 } else { 1195 // Specified theme is not installed. 1196 return ''; 1197 } 1198 1199 case PARAM_USERNAME: 1200 $param = fix_utf8($param); 1201 $param = trim($param); 1202 // Convert uppercase to lowercase MDL-16919. 1203 $param = core_text::strtolower($param); 1204 if (empty($CFG->extendedusernamechars)) { 1205 $param = str_replace(" " , "", $param); 1206 // Regular expression, eliminate all chars EXCEPT: 1207 // alphanum, dash (-), underscore (_), at sign (@) and period (.) characters. 1208 $param = preg_replace('/[^-\.@_a-z0-9]/', '', $param); 1209 } 1210 return $param; 1211 1212 case PARAM_EMAIL: 1213 $param = fix_utf8($param); 1214 if (validate_email($param)) { 1215 return $param; 1216 } else { 1217 return ''; 1218 } 1219 1220 case PARAM_STRINGID: 1221 if (preg_match('|^[a-zA-Z][a-zA-Z0-9\.:/_-]*$|', $param)) { 1222 return $param; 1223 } else { 1224 return ''; 1225 } 1226 1227 case PARAM_TIMEZONE: 1228 // Can be int, float(with .5 or .0) or string seperated by '/' and can have '-_'. 1229 $param = fix_utf8($param); 1230 $timezonepattern = '/^(([+-]?(0?[0-9](\.[5|0])?|1[0-3](\.0)?|1[0-2]\.5))|(99)|[[:alnum:]]+(\/?[[:alpha:]_-])+)$/'; 1231 if (preg_match($timezonepattern, $param)) { 1232 return $param; 1233 } else { 1234 return ''; 1235 } 1236 1237 default: 1238 // Doh! throw error, switched parameters in optional_param or another serious problem. 1239 print_error("unknownparamtype", '', '', $type); 1240 } 1241 } 1242 1243 /** 1244 * Whether the PARAM_* type is compatible in RTL. 1245 * 1246 * Being compatible with RTL means that the data they contain can flow 1247 * from right-to-left or left-to-right without compromising the user experience. 1248 * 1249 * Take URLs for example, they are not RTL compatible as they should always 1250 * flow from the left to the right. This also applies to numbers, email addresses, 1251 * configuration snippets, base64 strings, etc... 1252 * 1253 * This function tries to best guess which parameters can contain localised strings. 1254 * 1255 * @param string $paramtype Constant PARAM_*. 1256 * @return bool 1257 */ 1258 function is_rtl_compatible($paramtype) { 1259 return $paramtype == PARAM_TEXT || $paramtype == PARAM_NOTAGS; 1260 } 1261 1262 /** 1263 * Makes sure the data is using valid utf8, invalid characters are discarded. 1264 * 1265 * Note: this function is not intended for full objects with methods and private properties. 1266 * 1267 * @param mixed $value 1268 * @return mixed with proper utf-8 encoding 1269 */ 1270 function fix_utf8($value) { 1271 if (is_null($value) or $value === '') { 1272 return $value; 1273 1274 } else if (is_string($value)) { 1275 if ((string)(int)$value === $value) { 1276 // Shortcut. 1277 return $value; 1278 } 1279 // No null bytes expected in our data, so let's remove it. 1280 $value = str_replace("\0", '', $value); 1281 1282 // Note: this duplicates min_fix_utf8() intentionally. 1283 static $buggyiconv = null; 1284 if ($buggyiconv === null) { 1285 $buggyiconv = (!function_exists('iconv') or @iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€'); 1286 } 1287 1288 if ($buggyiconv) { 1289 if (function_exists('mb_convert_encoding')) { 1290 $subst = mb_substitute_character(); 1291 mb_substitute_character(''); 1292 $result = mb_convert_encoding($value, 'utf-8', 'utf-8'); 1293 mb_substitute_character($subst); 1294 1295 } else { 1296 // Warn admins on admin/index.php page. 1297 $result = $value; 1298 } 1299 1300 } else { 1301 $result = @iconv('UTF-8', 'UTF-8//IGNORE', $value); 1302 } 1303 1304 return $result; 1305 1306 } else if (is_array($value)) { 1307 foreach ($value as $k => $v) { 1308 $value[$k] = fix_utf8($v); 1309 } 1310 return $value; 1311 1312 } else if (is_object($value)) { 1313 // Do not modify original. 1314 $value = clone($value); 1315 foreach ($value as $k => $v) { 1316 $value->$k = fix_utf8($v); 1317 } 1318 return $value; 1319 1320 } else { 1321 // This is some other type, no utf-8 here. 1322 return $value; 1323 } 1324 } 1325 1326 /** 1327 * Return true if given value is integer or string with integer value 1328 * 1329 * @param mixed $value String or Int 1330 * @return bool true if number, false if not 1331 */ 1332 function is_number($value) { 1333 if (is_int($value)) { 1334 return true; 1335 } else if (is_string($value)) { 1336 return ((string)(int)$value) === $value; 1337 } else { 1338 return false; 1339 } 1340 } 1341 1342 /** 1343 * Returns host part from url. 1344 * 1345 * @param string $url full url 1346 * @return string host, null if not found 1347 */ 1348 function get_host_from_url($url) { 1349 preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches); 1350 if ($matches) { 1351 return $matches[1]; 1352 } 1353 return null; 1354 } 1355 1356 /** 1357 * Tests whether anything was returned by text editor 1358 * 1359 * This function is useful for testing whether something you got back from 1360 * the HTML editor actually contains anything. Sometimes the HTML editor 1361 * appear to be empty, but actually you get back a <br> tag or something. 1362 * 1363 * @param string $string a string containing HTML. 1364 * @return boolean does the string contain any actual content - that is text, 1365 * images, objects, etc. 1366 */ 1367 function html_is_blank($string) { 1368 return trim(strip_tags($string, '<img><object><applet><input><select><textarea><hr>')) == ''; 1369 } 1370 1371 /** 1372 * Set a key in global configuration 1373 * 1374 * Set a key/value pair in both this session's {@link $CFG} global variable 1375 * and in the 'config' database table for future sessions. 1376 * 1377 * Can also be used to update keys for plugin-scoped configs in config_plugin table. 1378 * In that case it doesn't affect $CFG. 1379 * 1380 * A NULL value will delete the entry. 1381 * 1382 * NOTE: this function is called from lib/db/upgrade.php 1383 * 1384 * @param string $name the key to set 1385 * @param string $value the value to set (without magic quotes) 1386 * @param string $plugin (optional) the plugin scope, default null 1387 * @return bool true or exception 1388 */ 1389 function set_config($name, $value, $plugin=null) { 1390 global $CFG, $DB; 1391 1392 if (empty($plugin)) { 1393 if (!array_key_exists($name, $CFG->config_php_settings)) { 1394 // So it's defined for this invocation at least. 1395 if (is_null($value)) { 1396 unset($CFG->$name); 1397 } else { 1398 // Settings from db are always strings. 1399 $CFG->$name = (string)$value; 1400 } 1401 } 1402 1403 if ($DB->get_field('config', 'name', array('name' => $name))) { 1404 if ($value === null) { 1405 $DB->delete_records('config', array('name' => $name)); 1406 } else { 1407 $DB->set_field('config', 'value', $value, array('name' => $name)); 1408 } 1409 } else { 1410 if ($value !== null) { 1411 $config = new stdClass(); 1412 $config->name = $name; 1413 $config->value = $value; 1414 $DB->insert_record('config', $config, false); 1415 } 1416 // When setting config during a Behat test (in the CLI script, not in the web browser 1417 // requests), remember which ones are set so that we can clear them later. 1418 if (defined('BEHAT_TEST')) { 1419 if (!property_exists($CFG, 'behat_cli_added_config')) { 1420 $CFG->behat_cli_added_config = []; 1421 } 1422 $CFG->behat_cli_added_config[$name] = true; 1423 } 1424 } 1425 if ($name === 'siteidentifier') { 1426 cache_helper::update_site_identifier($value); 1427 } 1428 cache_helper::invalidate_by_definition('core', 'config', array(), 'core'); 1429 } else { 1430 // Plugin scope. 1431 if ($id = $DB->get_field('config_plugins', 'id', array('name' => $name, 'plugin' => $plugin))) { 1432 if ($value===null) { 1433 $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin)); 1434 } else { 1435 $DB->set_field('config_plugins', 'value', $value, array('id' => $id)); 1436 } 1437 } else { 1438 if ($value !== null) { 1439 $config = new stdClass(); 1440 $config->plugin = $plugin; 1441 $config->name = $name; 1442 $config->value = $value; 1443 $DB->insert_record('config_plugins', $config, false); 1444 } 1445 } 1446 cache_helper::invalidate_by_definition('core', 'config', array(), $plugin); 1447 } 1448 1449 return true; 1450 } 1451 1452 /** 1453 * Get configuration values from the global config table 1454 * or the config_plugins table. 1455 * 1456 * If called with one parameter, it will load all the config 1457 * variables for one plugin, and return them as an object. 1458 * 1459 * If called with 2 parameters it will return a string single 1460 * value or false if the value is not found. 1461 * 1462 * NOTE: this function is called from lib/db/upgrade.php 1463 * 1464 * @static string|false $siteidentifier The site identifier is not cached. We use this static cache so 1465 * that we need only fetch it once per request. 1466 * @param string $plugin full component name 1467 * @param string $name default null 1468 * @return mixed hash-like object or single value, return false no config found 1469 * @throws dml_exception 1470 */ 1471 function get_config($plugin, $name = null) { 1472 global $CFG, $DB; 1473 1474 static $siteidentifier = null; 1475 1476 if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) { 1477 $forced =& $CFG->config_php_settings; 1478 $iscore = true; 1479 $plugin = 'core'; 1480 } else { 1481 if (array_key_exists($plugin, $CFG->forced_plugin_settings)) { 1482 $forced =& $CFG->forced_plugin_settings[$plugin]; 1483 } else { 1484 $forced = array(); 1485 } 1486 $iscore = false; 1487 } 1488 1489 if ($siteidentifier === null) { 1490 try { 1491 // This may fail during installation. 1492 // If you have a look at {@link initialise_cfg()} you will see that this is how we detect the need to 1493 // install the database. 1494 $siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier')); 1495 } catch (dml_exception $ex) { 1496 // Set siteidentifier to false. We don't want to trip this continually. 1497 $siteidentifier = false; 1498 throw $ex; 1499 } 1500 } 1501 1502 if (!empty($name)) { 1503 if (array_key_exists($name, $forced)) { 1504 return (string)$forced[$name]; 1505 } else if ($name === 'siteidentifier' && $plugin == 'core') { 1506 return $siteidentifier; 1507 } 1508 } 1509 1510 $cache = cache::make('core', 'config'); 1511 $result = $cache->get($plugin); 1512 if ($result === false) { 1513 // The user is after a recordset. 1514 if (!$iscore) { 1515 $result = $DB->get_records_menu('config_plugins', array('plugin' => $plugin), '', 'name,value'); 1516 } else { 1517 // This part is not really used any more, but anyway... 1518 $result = $DB->get_records_menu('config', array(), '', 'name,value');; 1519 } 1520 $cache->set($plugin, $result); 1521 } 1522 1523 if (!empty($name)) { 1524 if (array_key_exists($name, $result)) { 1525 return $result[$name]; 1526 } 1527 return false; 1528 } 1529 1530 if ($plugin === 'core') { 1531 $result['siteidentifier'] = $siteidentifier; 1532 } 1533 1534 foreach ($forced as $key => $value) { 1535 if (is_null($value) or is_array($value) or is_object($value)) { 1536 // We do not want any extra mess here, just real settings that could be saved in db. 1537 unset($result[$key]); 1538 } else { 1539 // Convert to string as if it went through the DB. 1540 $result[$key] = (string)$value; 1541 } 1542 } 1543 1544 return (object)$result; 1545 } 1546 1547 /** 1548 * Removes a key from global configuration. 1549 * 1550 * NOTE: this function is called from lib/db/upgrade.php 1551 * 1552 * @param string $name the key to set 1553 * @param string $plugin (optional) the plugin scope 1554 * @return boolean whether the operation succeeded. 1555 */ 1556 function unset_config($name, $plugin=null) { 1557 global $CFG, $DB; 1558 1559 if (empty($plugin)) { 1560 unset($CFG->$name); 1561 $DB->delete_records('config', array('name' => $name)); 1562 cache_helper::invalidate_by_definition('core', 'config', array(), 'core'); 1563 } else { 1564 $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin)); 1565 cache_helper::invalidate_by_definition('core', 'config', array(), $plugin); 1566 } 1567 1568 return true; 1569 } 1570 1571 /** 1572 * Remove all the config variables for a given plugin. 1573 * 1574 * NOTE: this function is called from lib/db/upgrade.php 1575 * 1576 * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice'; 1577 * @return boolean whether the operation succeeded. 1578 */ 1579 function unset_all_config_for_plugin($plugin) { 1580 global $DB; 1581 // Delete from the obvious config_plugins first. 1582 $DB->delete_records('config_plugins', array('plugin' => $plugin)); 1583 // Next delete any suspect settings from config. 1584 $like = $DB->sql_like('name', '?', true, true, false, '|'); 1585 $params = array($DB->sql_like_escape($plugin.'_', '|') . '%'); 1586 $DB->delete_records_select('config', $like, $params); 1587 // Finally clear both the plugin cache and the core cache (suspect settings now removed from core). 1588 cache_helper::invalidate_by_definition('core', 'config', array(), array('core', $plugin)); 1589 1590 return true; 1591 } 1592 1593 /** 1594 * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability. 1595 * 1596 * All users are verified if they still have the necessary capability. 1597 * 1598 * @param string $value the value of the config setting. 1599 * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor. 1600 * @param bool $includeadmins include administrators. 1601 * @return array of user objects. 1602 */ 1603 function get_users_from_config($value, $capability, $includeadmins = true) { 1604 if (empty($value) or $value === '$@NONE@$') { 1605 return array(); 1606 } 1607 1608 // We have to make sure that users still have the necessary capability, 1609 // it should be faster to fetch them all first and then test if they are present 1610 // instead of validating them one-by-one. 1611 $users = get_users_by_capability(context_system::instance(), $capability); 1612 if ($includeadmins) { 1613 $admins = get_admins(); 1614 foreach ($admins as $admin) { 1615 $users[$admin->id] = $admin; 1616 } 1617 } 1618 1619 if ($value === '$@ALL@$') { 1620 return $users; 1621 } 1622 1623 $result = array(); // Result in correct order. 1624 $allowed = explode(',', $value); 1625 foreach ($allowed as $uid) { 1626 if (isset($users[$uid])) { 1627 $user = $users[$uid]; 1628 $result[$user->id] = $user; 1629 } 1630 } 1631 1632 return $result; 1633 } 1634 1635 1636 /** 1637 * Invalidates browser caches and cached data in temp. 1638 * 1639 * @return void 1640 */ 1641 function purge_all_caches() { 1642 purge_caches(); 1643 } 1644 1645 /** 1646 * Selectively invalidate different types of cache. 1647 * 1648 * Purges the cache areas specified. By default, this will purge all caches but can selectively purge specific 1649 * areas alone or in combination. 1650 * 1651 * @param bool[] $options Specific parts of the cache to purge. Valid options are: 1652 * 'muc' Purge MUC caches? 1653 * 'theme' Purge theme cache? 1654 * 'lang' Purge language string cache? 1655 * 'js' Purge javascript cache? 1656 * 'filter' Purge text filter cache? 1657 * 'other' Purge all other caches? 1658 */ 1659 function purge_caches($options = []) { 1660 $defaults = array_fill_keys(['muc', 'theme', 'lang', 'js', 'template', 'filter', 'other'], false); 1661 if (empty(array_filter($options))) { 1662 $options = array_fill_keys(array_keys($defaults), true); // Set all options to true. 1663 } else { 1664 $options = array_merge($defaults, array_intersect_key($options, $defaults)); // Override defaults with specified options. 1665 } 1666 if ($options['muc']) { 1667 cache_helper::purge_all(); 1668 } 1669 if ($options['theme']) { 1670 theme_reset_all_caches(); 1671 } 1672 if ($options['lang']) { 1673 get_string_manager()->reset_caches(); 1674 } 1675 if ($options['js']) { 1676 js_reset_all_caches(); 1677 } 1678 if ($options['template']) { 1679 template_reset_all_caches(); 1680 } 1681 if ($options['filter']) { 1682 reset_text_filters_cache(); 1683 } 1684 if ($options['other']) { 1685 purge_other_caches(); 1686 } 1687 } 1688 1689 /** 1690 * Purge all non-MUC caches not otherwise purged in purge_caches. 1691 * 1692 * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at 1693 * {@link phpunit_util::reset_dataroot()} 1694 */ 1695 function purge_other_caches() { 1696 global $DB, $CFG; 1697 core_text::reset_caches(); 1698 if (class_exists('core_plugin_manager')) { 1699 core_plugin_manager::reset_caches(); 1700 } 1701 1702 // Bump up cacherev field for all courses. 1703 try { 1704 increment_revision_number('course', 'cacherev', ''); 1705 } catch (moodle_exception $e) { 1706 // Ignore exception since this function is also called before upgrade script when field course.cacherev does not exist yet. 1707 } 1708 1709 $DB->reset_caches(); 1710 1711 // Purge all other caches: rss, simplepie, etc. 1712 clearstatcache(); 1713 remove_dir($CFG->cachedir.'', true); 1714 1715 // Make sure cache dir is writable, throws exception if not. 1716 make_cache_directory(''); 1717 1718 // This is the only place where we purge local caches, we are only adding files there. 1719 // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes. 1720 remove_dir($CFG->localcachedir, true); 1721 set_config('localcachedirpurged', time()); 1722 make_localcache_directory('', true); 1723 \core\task\manager::clear_static_caches(); 1724 } 1725 1726 /** 1727 * Get volatile flags 1728 * 1729 * @param string $type 1730 * @param int $changedsince default null 1731 * @return array records array 1732 */ 1733 function get_cache_flags($type, $changedsince = null) { 1734 global $DB; 1735 1736 $params = array('type' => $type, 'expiry' => time()); 1737 $sqlwhere = "flagtype = :type AND expiry >= :expiry"; 1738 if ($changedsince !== null) { 1739 $params['changedsince'] = $changedsince; 1740 $sqlwhere .= " AND timemodified > :changedsince"; 1741 } 1742 $cf = array(); 1743 if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) { 1744 foreach ($flags as $flag) { 1745 $cf[$flag->name] = $flag->value; 1746 } 1747 } 1748 return $cf; 1749 } 1750 1751 /** 1752 * Get volatile flags 1753 * 1754 * @param string $type 1755 * @param string $name 1756 * @param int $changedsince default null 1757 * @return string|false The cache flag value or false 1758 */ 1759 function get_cache_flag($type, $name, $changedsince=null) { 1760 global $DB; 1761 1762 $params = array('type' => $type, 'name' => $name, 'expiry' => time()); 1763 1764 $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry"; 1765 if ($changedsince !== null) { 1766 $params['changedsince'] = $changedsince; 1767 $sqlwhere .= " AND timemodified > :changedsince"; 1768 } 1769 1770 return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params); 1771 } 1772 1773 /** 1774 * Set a volatile flag 1775 * 1776 * @param string $type the "type" namespace for the key 1777 * @param string $name the key to set 1778 * @param string $value the value to set (without magic quotes) - null will remove the flag 1779 * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs 1780 * @return bool Always returns true 1781 */ 1782 function set_cache_flag($type, $name, $value, $expiry = null) { 1783 global $DB; 1784 1785 $timemodified = time(); 1786 if ($expiry === null || $expiry < $timemodified) { 1787 $expiry = $timemodified + 24 * 60 * 60; 1788 } else { 1789 $expiry = (int)$expiry; 1790 } 1791 1792 if ($value === null) { 1793 unset_cache_flag($type, $name); 1794 return true; 1795 } 1796 1797 if ($f = $DB->get_record('cache_flags', array('name' => $name, 'flagtype' => $type), '*', IGNORE_MULTIPLE)) { 1798 // This is a potential problem in DEBUG_DEVELOPER. 1799 if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) { 1800 return true; // No need to update. 1801 } 1802 $f->value = $value; 1803 $f->expiry = $expiry; 1804 $f->timemodified = $timemodified; 1805 $DB->update_record('cache_flags', $f); 1806 } else { 1807 $f = new stdClass(); 1808 $f->flagtype = $type; 1809 $f->name = $name; 1810 $f->value = $value; 1811 $f->expiry = $expiry; 1812 $f->timemodified = $timemodified; 1813 $DB->insert_record('cache_flags', $f); 1814 } 1815 return true; 1816 } 1817 1818 /** 1819 * Removes a single volatile flag 1820 * 1821 * @param string $type the "type" namespace for the key 1822 * @param string $name the key to set 1823 * @return bool 1824 */ 1825 function unset_cache_flag($type, $name) { 1826 global $DB; 1827 $DB->delete_records('cache_flags', array('name' => $name, 'flagtype' => $type)); 1828 return true; 1829 } 1830 1831 /** 1832 * Garbage-collect volatile flags 1833 * 1834 * @return bool Always returns true 1835 */ 1836 function gc_cache_flags() { 1837 global $DB; 1838 $DB->delete_records_select('cache_flags', 'expiry < ?', array(time())); 1839 return true; 1840 } 1841 1842 // USER PREFERENCE API. 1843 1844 /** 1845 * Refresh user preference cache. This is used most often for $USER 1846 * object that is stored in session, but it also helps with performance in cron script. 1847 * 1848 * Preferences for each user are loaded on first use on every page, then again after the timeout expires. 1849 * 1850 * @package core 1851 * @category preference 1852 * @access public 1853 * @param stdClass $user User object. Preferences are preloaded into 'preference' property 1854 * @param int $cachelifetime Cache life time on the current page (in seconds) 1855 * @throws coding_exception 1856 * @return null 1857 */ 1858 function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120) { 1859 global $DB; 1860 // Static cache, we need to check on each page load, not only every 2 minutes. 1861 static $loadedusers = array(); 1862 1863 if (!isset($user->id)) { 1864 throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field'); 1865 } 1866 1867 if (empty($user->id) or isguestuser($user->id)) { 1868 // No permanent storage for not-logged-in users and guest. 1869 if (!isset($user->preference)) { 1870 $user->preference = array(); 1871 } 1872 return; 1873 } 1874 1875 $timenow = time(); 1876 1877 if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) { 1878 // Already loaded at least once on this page. Are we up to date? 1879 if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) { 1880 // No need to reload - we are on the same page and we loaded prefs just a moment ago. 1881 return; 1882 1883 } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) { 1884 // No change since the lastcheck on this page. 1885 $user->preference['_lastloaded'] = $timenow; 1886 return; 1887 } 1888 } 1889 1890 // OK, so we have to reload all preferences. 1891 $loadedusers[$user->id] = true; 1892 $user->preference = $DB->get_records_menu('user_preferences', array('userid' => $user->id), '', 'name,value'); // All values. 1893 $user->preference['_lastloaded'] = $timenow; 1894 } 1895 1896 /** 1897 * Called from set/unset_user_preferences, so that the prefs can be correctly reloaded in different sessions. 1898 * 1899 * NOTE: internal function, do not call from other code. 1900 * 1901 * @package core 1902 * @access private 1903 * @param integer $userid the user whose prefs were changed. 1904 */ 1905 function mark_user_preferences_changed($userid) { 1906 global $CFG; 1907 1908 if (empty($userid) or isguestuser($userid)) { 1909 // No cache flags for guest and not-logged-in users. 1910 return; 1911 } 1912 1913 set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout); 1914 } 1915 1916 /** 1917 * Sets a preference for the specified user. 1918 * 1919 * If a $user object is submitted it's 'preference' property is used for the preferences cache. 1920 * 1921 * When additional validation/permission check is needed it is better to use {@see useredit_update_user_preference()} 1922 * 1923 * @package core 1924 * @category preference 1925 * @access public 1926 * @param string $name The key to set as preference for the specified user 1927 * @param string $value The value to set for the $name key in the specified user's 1928 * record, null means delete current value. 1929 * @param stdClass|int|null $user A moodle user object or id, null means current user 1930 * @throws coding_exception 1931 * @return bool Always true or exception 1932 */ 1933 function set_user_preference($name, $value, $user = null) { 1934 global $USER, $DB; 1935 1936 if (empty($name) or is_numeric($name) or $name === '_lastloaded') { 1937 throw new coding_exception('Invalid preference name in set_user_preference() call'); 1938 } 1939 1940 if (is_null($value)) { 1941 // Null means delete current. 1942 return unset_user_preference($name, $user); 1943 } else if (is_object($value)) { 1944 throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed'); 1945 } else if (is_array($value)) { 1946 throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed'); 1947 } 1948 // Value column maximum length is 1333 characters. 1949 $value = (string)$value; 1950 if (core_text::strlen($value) > 1333) { 1951 throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column'); 1952 } 1953 1954 if (is_null($user)) { 1955 $user = $USER; 1956 } else if (isset($user->id)) { 1957 // It is a valid object. 1958 } else if (is_numeric($user)) { 1959 $user = (object)array('id' => (int)$user); 1960 } else { 1961 throw new coding_exception('Invalid $user parameter in set_user_preference() call'); 1962 } 1963 1964 check_user_preferences_loaded($user); 1965 1966 if (empty($user->id) or isguestuser($user->id)) { 1967 // No permanent storage for not-logged-in users and guest. 1968 $user->preference[$name] = $value; 1969 return true; 1970 } 1971 1972 if ($preference = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => $name))) { 1973 if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) { 1974 // Preference already set to this value. 1975 return true; 1976 } 1977 $DB->set_field('user_preferences', 'value', $value, array('id' => $preference->id)); 1978 1979 } else { 1980 $preference = new stdClass(); 1981 $preference->userid = $user->id; 1982 $preference->name = $name; 1983 $preference->value = $value; 1984 $DB->insert_record('user_preferences', $preference); 1985 } 1986 1987 // Update value in cache. 1988 $user->preference[$name] = $value; 1989 // Update the $USER in case where we've not a direct reference to $USER. 1990 if ($user !== $USER && $user->id == $USER->id) { 1991 $USER->preference[$name] = $value; 1992 } 1993 1994 // Set reload flag for other sessions. 1995 mark_user_preferences_changed($user->id); 1996 1997 return true; 1998 } 1999 2000 /** 2001 * Sets a whole array of preferences for the current user 2002 * 2003 * If a $user object is submitted it's 'preference' property is used for the preferences cache. 2004 * 2005 * @package core 2006 * @category preference 2007 * @access public 2008 * @param array $prefarray An array of key/value pairs to be set 2009 * @param stdClass|int|null $user A moodle user object or id, null means current user 2010 * @return bool Always true or exception 2011 */ 2012 function set_user_preferences(array $prefarray, $user = null) { 2013 foreach ($prefarray as $name => $value) { 2014 set_user_preference($name, $value, $user); 2015 } 2016 return true; 2017 } 2018 2019 /** 2020 * Unsets a preference completely by deleting it from the database 2021 * 2022 * If a $user object is submitted it's 'preference' property is used for the preferences cache. 2023 * 2024 * @package core 2025 * @category preference 2026 * @access public 2027 * @param string $name The key to unset as preference for the specified user 2028 * @param stdClass|int|null $user A moodle user object or id, null means current user 2029 * @throws coding_exception 2030 * @return bool Always true or exception 2031 */ 2032 function unset_user_preference($name, $user = null) { 2033 global $USER, $DB; 2034 2035 if (empty($name) or is_numeric($name) or $name === '_lastloaded') { 2036 throw new coding_exception('Invalid preference name in unset_user_preference() call'); 2037 } 2038 2039 if (is_null($user)) { 2040 $user = $USER; 2041 } else if (isset($user->id)) { 2042 // It is a valid object. 2043 } else if (is_numeric($user)) { 2044 $user = (object)array('id' => (int)$user); 2045 } else { 2046 throw new coding_exception('Invalid $user parameter in unset_user_preference() call'); 2047 } 2048 2049 check_user_preferences_loaded($user); 2050 2051 if (empty($user->id) or isguestuser($user->id)) { 2052 // No permanent storage for not-logged-in user and guest. 2053 unset($user->preference[$name]); 2054 return true; 2055 } 2056 2057 // Delete from DB. 2058 $DB->delete_records('user_preferences', array('userid' => $user->id, 'name' => $name)); 2059 2060 // Delete the preference from cache. 2061 unset($user->preference[$name]); 2062 // Update the $USER in case where we've not a direct reference to $USER. 2063 if ($user !== $USER && $user->id == $USER->id) { 2064 unset($USER->preference[$name]); 2065 } 2066 2067 // Set reload flag for other sessions. 2068 mark_user_preferences_changed($user->id); 2069 2070 return true; 2071 } 2072 2073 /** 2074 * Used to fetch user preference(s) 2075 * 2076 * If no arguments are supplied this function will return 2077 * all of the current user preferences as an array. 2078 * 2079 * If a name is specified then this function 2080 * attempts to return that particular preference value. If 2081 * none is found, then the optional value $default is returned, 2082 * otherwise null. 2083 * 2084 * If a $user object is submitted it's 'preference' property is used for the preferences cache. 2085 * 2086 * @package core 2087 * @category preference 2088 * @access public 2089 * @param string $name Name of the key to use in finding a preference value 2090 * @param mixed|null $default Value to be returned if the $name key is not set in the user preferences 2091 * @param stdClass|int|null $user A moodle user object or id, null means current user 2092 * @throws coding_exception 2093 * @return string|mixed|null A string containing the value of a single preference. An 2094 * array with all of the preferences or null 2095 */ 2096 function get_user_preferences($name = null, $default = null, $user = null) { 2097 global $USER; 2098 2099 if (is_null($name)) { 2100 // All prefs. 2101 } else if (is_numeric($name) or $name === '_lastloaded') { 2102 throw new coding_exception('Invalid preference name in get_user_preferences() call'); 2103 } 2104 2105 if (is_null($user)) { 2106 $user = $USER; 2107 } else if (isset($user->id)) { 2108 // Is a valid object. 2109 } else if (is_numeric($user)) { 2110 if ($USER->id == $user) { 2111 $user = $USER; 2112 } else { 2113 $user = (object)array('id' => (int)$user); 2114 } 2115 } else { 2116 throw new coding_exception('Invalid $user parameter in get_user_preferences() call'); 2117 } 2118 2119 check_user_preferences_loaded($user); 2120 2121 if (empty($name)) { 2122 // All values. 2123 return $user->preference; 2124 } else if (isset($user->preference[$name])) { 2125 // The single string value. 2126 return $user->preference[$name]; 2127 } else { 2128 // Default value (null if not specified). 2129 return $default; 2130 } 2131 } 2132 2133 // FUNCTIONS FOR HANDLING TIME. 2134 2135 /** 2136 * Given Gregorian date parts in user time produce a GMT timestamp. 2137 * 2138 * @package core 2139 * @category time 2140 * @param int $year The year part to create timestamp of 2141 * @param int $month The month part to create timestamp of 2142 * @param int $day The day part to create timestamp of 2143 * @param int $hour The hour part to create timestamp of 2144 * @param int $minute The minute part to create timestamp of 2145 * @param int $second The second part to create timestamp of 2146 * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset. 2147 * if 99 then default user's timezone is used {@link http://docs.moodle.org/dev/Time_API#Timezone} 2148 * @param bool $applydst Toggle Daylight Saving Time, default true, will be 2149 * applied only if timezone is 99 or string. 2150 * @return int GMT timestamp 2151 */ 2152 function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) { 2153 $date = new DateTime('now', core_date::get_user_timezone_object($timezone)); 2154 $date->setDate((int)$year, (int)$month, (int)$day); 2155 $date->setTime((int)$hour, (int)$minute, (int)$second); 2156 2157 $time = $date->getTimestamp(); 2158 2159 if ($time === false) { 2160 throw new coding_exception('getTimestamp() returned false, please ensure you have passed correct values.'. 2161 ' This can fail if year is more than 2038 and OS is 32 bit windows'); 2162 } 2163 2164 // Moodle BC DST stuff. 2165 if (!$applydst) { 2166 $time += dst_offset_on($time, $timezone); 2167 } 2168 2169 return $time; 2170 2171 } 2172 2173 /** 2174 * Format a date/time (seconds) as weeks, days, hours etc as needed 2175 * 2176 * Given an amount of time in seconds, returns string 2177 * formatted nicely as years, days, hours etc as needed 2178 * 2179 * @package core 2180 * @category time 2181 * @uses MINSECS 2182 * @uses HOURSECS 2183 * @uses DAYSECS 2184 * @uses YEARSECS 2185 * @param int $totalsecs Time in seconds 2186 * @param stdClass $str Should be a time object 2187 * @return string A nicely formatted date/time string 2188 */ 2189 function format_time($totalsecs, $str = null) { 2190 2191 $totalsecs = abs($totalsecs); 2192 2193 if (!$str) { 2194 // Create the str structure the slow way. 2195 $str = new stdClass(); 2196 $str->day = get_string('day'); 2197 $str->days = get_string('days'); 2198 $str->hour = get_string('hour'); 2199 $str->hours = get_string('hours'); 2200 $str->min = get_string('min'); 2201 $str->mins = get_string('mins'); 2202 $str->sec = get_string('sec'); 2203 $str->secs = get_string('secs'); 2204 $str->year = get_string('year'); 2205 $str->years = get_string('years'); 2206 } 2207 2208 $years = floor($totalsecs/YEARSECS); 2209 $remainder = $totalsecs - ($years*YEARSECS); 2210 $days = floor($remainder/DAYSECS); 2211 $remainder = $totalsecs - ($days*DAYSECS); 2212 $hours = floor($remainder/HOURSECS); 2213 $remainder = $remainder - ($hours*HOURSECS); 2214 $mins = floor($remainder/MINSECS); 2215 $secs = $remainder - ($mins*MINSECS); 2216 2217 $ss = ($secs == 1) ? $str->sec : $str->secs; 2218 $sm = ($mins == 1) ? $str->min : $str->mins; 2219 $sh = ($hours == 1) ? $str->hour : $str->hours; 2220 $sd = ($days == 1) ? $str->day : $str->days; 2221 $sy = ($years == 1) ? $str->year : $str->years; 2222 2223 $oyears = ''; 2224 $odays = ''; 2225 $ohours = ''; 2226 $omins = ''; 2227 $osecs = ''; 2228 2229 if ($years) { 2230 $oyears = $years .' '. $sy; 2231 } 2232 if ($days) { 2233 $odays = $days .' '. $sd; 2234 } 2235 if ($hours) { 2236 $ohours = $hours .' '. $sh; 2237 } 2238 if ($mins) { 2239 $omins = $mins .' '. $sm; 2240 } 2241 if ($secs) { 2242 $osecs = $secs .' '. $ss; 2243 } 2244 2245 if ($years) { 2246 return trim($oyears .' '. $odays); 2247 } 2248 if ($days) { 2249 return trim($odays .' '. $ohours); 2250 } 2251 if ($hours) { 2252 return trim($ohours .' '. $omins); 2253 } 2254 if ($mins) { 2255 return trim($omins .' '. $osecs); 2256 } 2257 if ($secs) { 2258 return $osecs; 2259 } 2260 return get_string('now'); 2261 } 2262 2263 /** 2264 * Returns a formatted string that represents a date in user time. 2265 * 2266 * @package core 2267 * @category time 2268 * @param int $date the timestamp in UTC, as obtained from the database. 2269 * @param string $format strftime format. You should probably get this using 2270 * get_string('strftime...', 'langconfig'); 2271 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and 2272 * not 99 then daylight saving will not be added. 2273 * {@link http://docs.moodle.org/dev/Time_API#Timezone} 2274 * @param bool $fixday If true (default) then the leading zero from %d is removed. 2275 * If false then the leading zero is maintained. 2276 * @param bool $fixhour If true (default) then the leading zero from %I is removed. 2277 * @return string the formatted date/time. 2278 */ 2279 function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) { 2280 $calendartype = \core_calendar\type_factory::get_calendar_instance(); 2281 return $calendartype->timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour); 2282 } 2283 2284 /** 2285 * Returns a html "time" tag with both the exact user date with timezone information 2286 * as a datetime attribute in the W3C format, and the user readable date and time as text. 2287 * 2288 * @package core 2289 * @category time 2290 * @param int $date the timestamp in UTC, as obtained from the database. 2291 * @param string $format strftime format. You should probably get this using 2292 * get_string('strftime...', 'langconfig'); 2293 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and 2294 * not 99 then daylight saving will not be added. 2295 * {@link http://docs.moodle.org/dev/Time_API#Timezone} 2296 * @param bool $fixday If true (default) then the leading zero from %d is removed. 2297 * If false then the leading zero is maintained. 2298 * @param bool $fixhour If true (default) then the leading zero from %I is removed. 2299 * @return string the formatted date/time. 2300 */ 2301 function userdate_htmltime($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) { 2302 $userdatestr = userdate($date, $format, $timezone, $fixday, $fixhour); 2303 if (CLI_SCRIPT && !PHPUNIT_TEST) { 2304 return $userdatestr; 2305 } 2306 $machinedate = new DateTime(); 2307 $machinedate->setTimestamp(intval($date)); 2308 $machinedate->setTimezone(core_date::get_user_timezone_object()); 2309 2310 return html_writer::tag('time', $userdatestr, ['datetime' => $machinedate->format(DateTime::W3C)]); 2311 } 2312 2313 /** 2314 * Returns a formatted date ensuring it is UTF-8. 2315 * 2316 * If we are running under Windows convert to Windows encoding and then back to UTF-8 2317 * (because it's impossible to specify UTF-8 to fetch locale info in Win32). 2318 * 2319 * @param int $date the timestamp - since Moodle 2.9 this is a real UTC timestamp 2320 * @param string $format strftime format. 2321 * @param int|float|string $tz the user timezone 2322 * @return string the formatted date/time. 2323 * @since Moodle 2.3.3 2324 */ 2325 function date_format_string($date, $format, $tz = 99) { 2326 global $CFG; 2327 2328 $localewincharset = null; 2329 // Get the calendar type user is using. 2330 if ($CFG->ostype == 'WINDOWS') { 2331 $calendartype = \core_calendar\type_factory::get_calendar_instance(); 2332 $localewincharset = $calendartype->locale_win_charset(); 2333 } 2334 2335 if ($localewincharset) { 2336 $format = core_text::convert($format, 'utf-8', $localewincharset); 2337 } 2338 2339 date_default_timezone_set(core_date::get_user_timezone($tz)); 2340 $datestring = strftime($format, $date); 2341 core_date::set_default_server_timezone(); 2342 2343 if ($localewincharset) { 2344 $datestring = core_text::convert($datestring, $localewincharset, 'utf-8'); 2345 } 2346 2347 return $datestring; 2348 } 2349 2350 /** 2351 * Given a $time timestamp in GMT (seconds since epoch), 2352 * returns an array that represents the Gregorian date in user time 2353 * 2354 * @package core 2355 * @category time 2356 * @param int $time Timestamp in GMT 2357 * @param float|int|string $timezone user timezone 2358 * @return array An array that represents the date in user time 2359 */ 2360 function usergetdate($time, $timezone=99) { 2361 date_default_timezone_set(core_date::get_user_timezone($timezone)); 2362 $result = getdate($time); 2363 core_date::set_default_server_timezone(); 2364 2365 return $result; 2366 } 2367 2368 /** 2369 * Given a GMT timestamp (seconds since epoch), offsets it by 2370 * the timezone. eg 3pm in India is 3pm GMT - 7 * 3600 seconds 2371 * 2372 * NOTE: this function does not include DST properly, 2373 * you should use the PHP date stuff instead! 2374 * 2375 * @package core 2376 * @category time 2377 * @param int $date Timestamp in GMT 2378 * @param float|int|string $timezone user timezone 2379 * @return int 2380 */ 2381 function usertime($date, $timezone=99) { 2382 $userdate = new DateTime('@' . $date); 2383 $userdate->setTimezone(core_date::get_user_timezone_object($timezone)); 2384 $dst = dst_offset_on($date, $timezone); 2385 2386 return $date - $userdate->getOffset() + $dst; 2387 } 2388 2389 /** 2390 * Get a formatted string representation of an interval between two unix timestamps. 2391 * 2392 * E.g. 2393 * $intervalstring = get_time_interval_string(12345600, 12345660); 2394 * Will produce the string: 2395 * '0d 0h 1m' 2396 * 2397 * @param int $time1 unix timestamp 2398 * @param int $time2 unix timestamp 2399 * @param string $format string (can be lang string) containing format chars: https://www.php.net/manual/en/dateinterval.format.php. 2400 * @return string the formatted string describing the time difference, e.g. '10d 11h 45m'. 2401 */ 2402 function get_time_interval_string(int $time1, int $time2, string $format = ''): string { 2403 $dtdate = new DateTime(); 2404 $dtdate->setTimeStamp($time1); 2405 $dtdate2 = new DateTime(); 2406 $dtdate2->setTimeStamp($time2); 2407 $interval = $dtdate2->diff($dtdate); 2408 $format = empty($format) ? get_string('dateintervaldayshoursmins', 'langconfig') : $format; 2409 return $interval->format($format); 2410 } 2411 2412 /** 2413 * Given a time, return the GMT timestamp of the most recent midnight 2414 * for the current user. 2415 * 2416 * @package core 2417 * @category time 2418 * @param int $date Timestamp in GMT 2419 * @param float|int|string $timezone user timezone 2420 * @return int Returns a GMT timestamp 2421 */ 2422 function usergetmidnight($date, $timezone=99) { 2423 2424 $userdate = usergetdate($date, $timezone); 2425 2426 // Time of midnight of this user's day, in GMT. 2427 return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone); 2428 2429 } 2430 2431 /** 2432 * Returns a string that prints the user's timezone 2433 * 2434 * @package core 2435 * @category time 2436 * @param float|int|string $timezone user timezone 2437 * @return string 2438 */ 2439 function usertimezone($timezone=99) { 2440 $tz = core_date::get_user_timezone($timezone); 2441 return core_date::get_localised_timezone($tz); 2442 } 2443 2444 /** 2445 * Returns a float or a string which denotes the user's timezone 2446 * 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) 2447 * means that for this timezone there are also DST rules to be taken into account 2448 * Checks various settings and picks the most dominant of those which have a value 2449 * 2450 * @package core 2451 * @category time 2452 * @param float|int|string $tz timezone to calculate GMT time offset before 2453 * calculating user timezone, 99 is default user timezone 2454 * {@link http://docs.moodle.org/dev/Time_API#Timezone} 2455 * @return float|string 2456 */ 2457 function get_user_timezone($tz = 99) { 2458 global $USER, $CFG; 2459 2460 $timezones = array( 2461 $tz, 2462 isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99, 2463 isset($USER->timezone) ? $USER->timezone : 99, 2464 isset($CFG->timezone) ? $CFG->timezone : 99, 2465 ); 2466 2467 $tz = 99; 2468 2469 // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array. 2470 foreach ($timezones as $nextvalue) { 2471 if ((empty($tz) && !is_numeric($tz)) || $tz == 99) { 2472 $tz = $nextvalue; 2473 } 2474 } 2475 return is_numeric($tz) ? (float) $tz : $tz; 2476 } 2477 2478 /** 2479 * Calculates the Daylight Saving Offset for a given date/time (timestamp) 2480 * - Note: Daylight saving only works for string timezones and not for float. 2481 * 2482 * @package core 2483 * @category time 2484 * @param int $time must NOT be compensated at all, it has to be a pure timestamp 2485 * @param int|float|string $strtimezone user timezone 2486 * @return int 2487 */ 2488 function dst_offset_on($time, $strtimezone = null) { 2489 $tz = core_date::get_user_timezone($strtimezone); 2490 $date = new DateTime('@' . $time); 2491 $date->setTimezone(new DateTimeZone($tz)); 2492 if ($date->format('I') == '1') { 2493 if ($tz === 'Australia/Lord_Howe') { 2494 return 1800; 2495 } 2496 return 3600; 2497 } 2498 return 0; 2499 } 2500 2501 /** 2502 * Calculates when the day appears in specific month 2503 * 2504 * @package core 2505 * @category time 2506 * @param int $startday starting day of the month 2507 * @param int $weekday The day when week starts (normally taken from user preferences) 2508 * @param int $month The month whose day is sought 2509 * @param int $year The year of the month whose day is sought 2510 * @return int 2511 */ 2512 function find_day_in_month($startday, $weekday, $month, $year) { 2513 $calendartype = \core_calendar\type_factory::get_calendar_instance(); 2514 2515 $daysinmonth = days_in_month($month, $year); 2516 $daysinweek = count($calendartype->get_weekdays()); 2517 2518 if ($weekday == -1) { 2519 // Don't care about weekday, so return: 2520 // abs($startday) if $startday != -1 2521 // $daysinmonth otherwise. 2522 return ($startday == -1) ? $daysinmonth : abs($startday); 2523 } 2524 2525 // From now on we 're looking for a specific weekday. 2526 // Give "end of month" its actual value, since we know it. 2527 if ($startday == -1) { 2528 $startday = -1 * $daysinmonth; 2529 } 2530 2531 // Starting from day $startday, the sign is the direction. 2532 if ($startday < 1) { 2533 $startday = abs($startday); 2534 $lastmonthweekday = dayofweek($daysinmonth, $month, $year); 2535 2536 // This is the last such weekday of the month. 2537 $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday; 2538 if ($lastinmonth > $daysinmonth) { 2539 $lastinmonth -= $daysinweek; 2540 } 2541 2542 // Find the first such weekday <= $startday. 2543 while ($lastinmonth > $startday) { 2544 $lastinmonth -= $daysinweek; 2545 } 2546 2547 return $lastinmonth; 2548 } else { 2549 $indexweekday = dayofweek($startday, $month, $year); 2550 2551 $diff = $weekday - $indexweekday; 2552 if ($diff < 0) { 2553 $diff += $daysinweek; 2554 } 2555 2556 // This is the first such weekday of the month equal to or after $startday. 2557 $firstfromindex = $startday + $diff; 2558 2559 return $firstfromindex; 2560 } 2561 } 2562 2563 /** 2564 * Calculate the number of days in a given month 2565 * 2566 * @package core 2567 * @category time 2568 * @param int $month The month whose day count is sought 2569 * @param int $year The year of the month whose day count is sought 2570 * @return int 2571 */ 2572 function days_in_month($month, $year) { 2573 $calendartype = \core_calendar\type_factory::get_calendar_instance(); 2574 return $calendartype->get_num_days_in_month($year, $month); 2575 } 2576 2577 /** 2578 * Calculate the position in the week of a specific calendar day 2579 * 2580 * @package core 2581 * @category time 2582 * @param int $day The day of the date whose position in the week is sought 2583 * @param int $month The month of the date whose position in the week is sought 2584 * @param int $year The year of the date whose position in the week is sought 2585 * @return int 2586 */ 2587 function dayofweek($day, $month, $year) { 2588 $calendartype = \core_calendar\type_factory::get_calendar_instance(); 2589 return $calendartype->get_weekday($year, $month, $day); 2590 } 2591 2592 // USER AUTHENTICATION AND LOGIN. 2593 2594 /** 2595 * Returns full login url. 2596 * 2597 * Any form submissions for authentication to this URL must include username, 2598 * password as well as a logintoken generated by \core\session\manager::get_login_token(). 2599 * 2600 * @return string login url 2601 */ 2602 function get_login_url() { 2603 global $CFG; 2604 2605 return "$CFG->wwwroot/login/index.php"; 2606 } 2607 2608 /** 2609 * This function checks that the current user is logged in and has the 2610 * required privileges 2611 * 2612 * This function checks that the current user is logged in, and optionally 2613 * whether they are allowed to be in a particular course and view a particular 2614 * course module. 2615 * If they are not logged in, then it redirects them to the site login unless 2616 * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which 2617 * case they are automatically logged in as guests. 2618 * If $courseid is given and the user is not enrolled in that course then the 2619 * user is redirected to the course enrolment page. 2620 * If $cm is given and the course module is hidden and the user is not a teacher 2621 * in the course then the user is redirected to the course home page. 2622 * 2623 * When $cm parameter specified, this function sets page layout to 'module'. 2624 * You need to change it manually later if some other layout needed. 2625 * 2626 * @package core_access 2627 * @category access 2628 * 2629 * @param mixed $courseorid id of the course or course object 2630 * @param bool $autologinguest default true 2631 * @param object $cm course module object 2632 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to 2633 * true. Used to avoid (=false) some scripts (file.php...) to set that variable, 2634 * in order to keep redirects working properly. MDL-14495 2635 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions 2636 * @return mixed Void, exit, and die depending on path 2637 * @throws coding_exception 2638 * @throws require_login_exception 2639 * @throws moodle_exception 2640 */ 2641 function require_login($courseorid = null, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) { 2642 global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT; 2643 2644 // Must not redirect when byteserving already started. 2645 if (!empty($_SERVER['HTTP_RANGE'])) { 2646 $preventredirect = true; 2647 } 2648 2649 if (AJAX_SCRIPT) { 2650 // We cannot redirect for AJAX scripts either. 2651 $preventredirect = true; 2652 } 2653 2654 // Setup global $COURSE, themes, language and locale. 2655 if (!empty($courseorid)) { 2656 if (is_object($courseorid)) { 2657 $course = $courseorid; 2658 } else if ($courseorid == SITEID) { 2659 $course = clone($SITE); 2660 } else { 2661 $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST); 2662 } 2663 if ($cm) { 2664 if ($cm->course != $course->id) { 2665 throw new coding_exception('course and cm parameters in require_login() call do not match!!'); 2666 } 2667 // Make sure we have a $cm from get_fast_modinfo as this contains activity access details. 2668 if (!($cm instanceof cm_info)) { 2669 // Note: nearly all pages call get_fast_modinfo anyway and it does not make any 2670 // db queries so this is not really a performance concern, however it is obviously 2671 // better if you use get_fast_modinfo to get the cm before calling this. 2672 $modinfo = get_fast_modinfo($course); 2673 $cm = $modinfo->get_cm($cm->id); 2674 } 2675 } 2676 } else { 2677 // Do not touch global $COURSE via $PAGE->set_course(), 2678 // the reasons is we need to be able to call require_login() at any time!! 2679 $course = $SITE; 2680 if ($cm) { 2681 throw new coding_exception('cm parameter in require_login() requires valid course parameter!'); 2682 } 2683 } 2684 2685 // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false. 2686 // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future 2687 // risk leading the user back to the AJAX request URL. 2688 if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) { 2689 $setwantsurltome = false; 2690 } 2691 2692 // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour. 2693 if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !empty($CFG->dbsessions)) { 2694 if ($preventredirect) { 2695 throw new require_login_session_timeout_exception(); 2696 } else { 2697 if ($setwantsurltome) { 2698 $SESSION->wantsurl = qualified_me(); 2699 } 2700 redirect(get_login_url()); 2701 } 2702 } 2703 2704 // If the user is not even logged in yet then make sure they are. 2705 if (!isloggedin()) { 2706 if ($autologinguest and !empty($CFG->guestloginbutton) and !empty($CFG->autologinguests)) { 2707 if (!$guest = get_complete_user_data('id', $CFG->siteguest)) { 2708 // Misconfigured site guest, just redirect to login page. 2709 redirect(get_login_url()); 2710 exit; // Never reached. 2711 } 2712 $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang; 2713 complete_user_login($guest); 2714 $USER->autologinguest = true; 2715 $SESSION->lang = $lang; 2716 } else { 2717 // NOTE: $USER->site check was obsoleted by session test cookie, $USER->confirmed test is in login/index.php. 2718 if ($preventredirect) { 2719 throw new require_login_exception('You are not logged in'); 2720 } 2721 2722 if ($setwantsurltome) { 2723 $SESSION->wantsurl = qualified_me(); 2724 } 2725 2726 $referer = get_local_referer(false); 2727 if (!empty($referer)) { 2728 $SESSION->fromurl = $referer; 2729 } 2730 2731 // Give auth plugins an opportunity to authenticate or redirect to an external login page 2732 $authsequence = get_enabled_auth_plugins(); // Auths, in sequence. 2733 foreach($authsequence as $authname) { 2734 $authplugin = get_auth_plugin($authname); 2735 $authplugin->pre_loginpage_hook(); 2736 if (isloggedin()) { 2737 if ($cm) { 2738 $modinfo = get_fast_modinfo($course); 2739 $cm = $modinfo->get_cm($cm->id); 2740 } 2741 set_access_log_user(); 2742 break; 2743 } 2744 } 2745 2746 // If we're still not logged in then go to the login page 2747 if (!isloggedin()) { 2748 redirect(get_login_url()); 2749 exit; // Never reached. 2750 } 2751 } 2752 } 2753 2754 // Loginas as redirection if needed. 2755 if ($course->id != SITEID and \core\session\manager::is_loggedinas()) { 2756 if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) { 2757 if ($USER->loginascontext->instanceid != $course->id) { 2758 print_error('loginasonecourse', '', $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid); 2759 } 2760 } 2761 } 2762 2763 // Check whether the user should be changing password (but only if it is REALLY them). 2764 if (get_user_preferences('auth_forcepasswordchange') && !\core\session\manager::is_loggedinas()) { 2765 $userauth = get_auth_plugin($USER->auth); 2766 if ($userauth->can_change_password() and !$preventredirect) { 2767 if ($setwantsurltome) { 2768 $SESSION->wantsurl = qualified_me(); 2769 } 2770 if ($changeurl = $userauth->change_password_url()) { 2771 // Use plugin custom url. 2772 redirect($changeurl); 2773 } else { 2774 // Use moodle internal method. 2775 redirect($CFG->wwwroot .'/login/change_password.php'); 2776 } 2777 } else if ($userauth->can_change_password()) { 2778 throw new moodle_exception('forcepasswordchangenotice'); 2779 } else { 2780 throw new moodle_exception('nopasswordchangeforced', 'auth'); 2781 } 2782 } 2783 2784 // Check that the user account is properly set up. If we can't redirect to 2785 // edit their profile and this is not a WS request, perform just the lax check. 2786 // It will allow them to use filepicker on the profile edit page. 2787 2788 if ($preventredirect && !WS_SERVER) { 2789 $usernotfullysetup = user_not_fully_set_up($USER, false); 2790 } else { 2791 $usernotfullysetup = user_not_fully_set_up($USER, true); 2792 } 2793 2794 if ($usernotfullysetup) { 2795 if ($preventredirect) { 2796 throw new moodle_exception('usernotfullysetup'); 2797 } 2798 if ($setwantsurltome) { 2799 $SESSION->wantsurl = qualified_me(); 2800 } 2801 redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&course='. SITEID); 2802 } 2803 2804 // Make sure the USER has a sesskey set up. Used for CSRF protection. 2805 sesskey(); 2806 2807 if (\core\session\manager::is_loggedinas()) { 2808 // During a "logged in as" session we should force all content to be cleaned because the 2809 // logged in user will be viewing potentially malicious user generated content. 2810 // See MDL-63786 for more details. 2811 $CFG->forceclean = true; 2812 } 2813 2814 $afterlogins = get_plugins_with_function('after_require_login', 'lib.php'); 2815 2816 // Do not bother admins with any formalities, except for activities pending deletion. 2817 if (is_siteadmin() && !($cm && $cm->deletioninprogress)) { 2818 // Set the global $COURSE. 2819 if ($cm) { 2820 $PAGE->set_cm($cm, $course); 2821 $PAGE->set_pagelayout('incourse'); 2822 } else if (!empty($courseorid)) { 2823 $PAGE->set_course($course); 2824 } 2825 // Set accesstime or the user will appear offline which messes up messaging. 2826 // Do not update access time for webservice or ajax requests. 2827 if (!WS_SERVER && !AJAX_SCRIPT) { 2828 user_accesstime_log($course->id); 2829 } 2830 2831 foreach ($afterlogins as $plugintype => $plugins) { 2832 foreach ($plugins as $pluginfunction) { 2833 $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); 2834 } 2835 } 2836 return; 2837 } 2838 2839 // Scripts have a chance to declare that $USER->policyagreed should not be checked. 2840 // This is mostly for places where users are actually accepting the policies, to avoid the redirect loop. 2841 if (!defined('NO_SITEPOLICY_CHECK')) { 2842 define('NO_SITEPOLICY_CHECK', false); 2843 } 2844 2845 // Check that the user has agreed to a site policy if there is one - do not test in case of admins. 2846 // Do not test if the script explicitly asked for skipping the site policies check. 2847 if (!$USER->policyagreed && !is_siteadmin() && !NO_SITEPOLICY_CHECK) { 2848 $manager = new \core_privacy\local\sitepolicy\manager(); 2849 if ($policyurl = $manager->get_redirect_url(isguestuser())) { 2850 if ($preventredirect) { 2851 throw new moodle_exception('sitepolicynotagreed', 'error', '', $policyurl->out()); 2852 } 2853 if ($setwantsurltome) { 2854 $SESSION->wantsurl = qualified_me(); 2855 } 2856 redirect($policyurl); 2857 } 2858 } 2859 2860 // Fetch the system context, the course context, and prefetch its child contexts. 2861 $sysctx = context_system::instance(); 2862 $coursecontext = context_course::instance($course->id, MUST_EXIST); 2863 if ($cm) { 2864 $cmcontext = context_module::instance($cm->id, MUST_EXIST); 2865 } else { 2866 $cmcontext = null; 2867 } 2868 2869 // If the site is currently under maintenance, then print a message. 2870 if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:maintenanceaccess', $sysctx)) { 2871 if ($preventredirect) { 2872 throw new require_login_exception('Maintenance in progress'); 2873 } 2874 $PAGE->set_context(null); 2875 print_maintenance_message(); 2876 } 2877 2878 // Make sure the course itself is not hidden. 2879 if ($course->id == SITEID) { 2880 // Frontpage can not be hidden. 2881 } else { 2882 if (is_role_switched($course->id)) { 2883 // When switching roles ignore the hidden flag - user had to be in course to do the switch. 2884 } else { 2885 if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) { 2886 // Originally there was also test of parent category visibility, BUT is was very slow in complex queries 2887 // involving "my courses" now it is also possible to simply hide all courses user is not enrolled in :-). 2888 if ($preventredirect) { 2889 throw new require_login_exception('Course is hidden'); 2890 } 2891 $PAGE->set_context(null); 2892 // We need to override the navigation URL as the course won't have been added to the navigation and thus 2893 // the navigation will mess up when trying to find it. 2894 navigation_node::override_active_url(new moodle_url('/')); 2895 notice(get_string('coursehidden'), $CFG->wwwroot .'/'); 2896 } 2897 } 2898 } 2899 2900 // Is the user enrolled? 2901 if ($course->id == SITEID) { 2902 // Everybody is enrolled on the frontpage. 2903 } else { 2904 if (\core\session\manager::is_loggedinas()) { 2905 // Make sure the REAL person can access this course first. 2906 $realuser = \core\session\manager::get_realuser(); 2907 if (!is_enrolled($coursecontext, $realuser->id, '', true) and 2908 !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) { 2909 if ($preventredirect) { 2910 throw new require_login_exception('Invalid course login-as access'); 2911 } 2912 $PAGE->set_context(null); 2913 echo $OUTPUT->header(); 2914 notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/'); 2915 } 2916 } 2917 2918 $access = false; 2919 2920 if (is_role_switched($course->id)) { 2921 // Ok, user had to be inside this course before the switch. 2922 $access = true; 2923 2924 } else if (is_viewing($coursecontext, $USER)) { 2925 // Ok, no need to mess with enrol. 2926 $access = true; 2927 2928 } else { 2929 if (isset($USER->enrol['enrolled'][$course->id])) { 2930 if ($USER->enrol['enrolled'][$course->id] > time()) { 2931 $access = true; 2932 if (isset($USER->enrol['tempguest'][$course->id])) { 2933 unset($USER->enrol['tempguest'][$course->id]); 2934 remove_temp_course_roles($coursecontext); 2935 } 2936 } else { 2937 // Expired. 2938 unset($USER->enrol['enrolled'][$course->id]); 2939 } 2940 } 2941 if (isset($USER->enrol['tempguest'][$course->id])) { 2942 if ($USER->enrol['tempguest'][$course->id] == 0) { 2943 $access = true; 2944 } else if ($USER->enrol['tempguest'][$course->id] > time()) { 2945 $access = true; 2946 } else { 2947 // Expired. 2948 unset($USER->enrol['tempguest'][$course->id]); 2949 remove_temp_course_roles($coursecontext); 2950 } 2951 } 2952 2953 if (!$access) { 2954 // Cache not ok. 2955 $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id); 2956 if ($until !== false) { 2957 // Active participants may always access, a timestamp in the future, 0 (always) or false. 2958 if ($until == 0) { 2959 $until = ENROL_MAX_TIMESTAMP; 2960 } 2961 $USER->enrol['enrolled'][$course->id] = $until; 2962 $access = true; 2963 2964 } else if (core_course_category::can_view_course_info($course)) { 2965 $params = array('courseid' => $course->id, 'status' => ENROL_INSTANCE_ENABLED); 2966 $instances = $DB->get_records('enrol', $params, 'sortorder, id ASC'); 2967 $enrols = enrol_get_plugins(true); 2968 // First ask all enabled enrol instances in course if they want to auto enrol user. 2969 foreach ($instances as $instance) { 2970 if (!isset($enrols[$instance->enrol])) { 2971 continue; 2972 } 2973 // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false. 2974 $until = $enrols[$instance->enrol]->try_autoenrol($instance); 2975 if ($until !== false) { 2976 if ($until == 0) { 2977 $until = ENROL_MAX_TIMESTAMP; 2978 } 2979 $USER->enrol['enrolled'][$course->id] = $until; 2980 $access = true; 2981 break; 2982 } 2983 } 2984 // If not enrolled yet try to gain temporary guest access. 2985 if (!$access) { 2986 foreach ($instances as $instance) { 2987 if (!isset($enrols[$instance->enrol])) { 2988 continue; 2989 } 2990 // Get a duration for the guest access, a timestamp in the future or false. 2991 $until = $enrols[$instance->enrol]->try_guestaccess($instance); 2992 if ($until !== false and $until > time()) { 2993 $USER->enrol['tempguest'][$course->id] = $until; 2994 $access = true; 2995 break; 2996 } 2997 } 2998 } 2999 } else { 3000 // User is not enrolled and is not allowed to browse courses here. 3001 if ($preventredirect) { 3002 throw new require_login_exception('Course is not available'); 3003 } 3004 $PAGE->set_context(null); 3005 // We need to override the navigation URL as the course won't have been added to the navigation and thus 3006 // the navigation will mess up when trying to find it. 3007 navigation_node::override_active_url(new moodle_url('/')); 3008 notice(get_string('coursehidden'), $CFG->wwwroot .'/'); 3009 } 3010 } 3011 } 3012 3013 if (!$access) { 3014 if ($preventredirect) { 3015 throw new require_login_exception('Not enrolled'); 3016 } 3017 if ($setwantsurltome) { 3018 $SESSION->wantsurl = qualified_me(); 3019 } 3020 redirect($CFG->wwwroot .'/enrol/index.php?id='. $course->id); 3021 } 3022 } 3023 3024 // Check whether the activity has been scheduled for deletion. If so, then deny access, even for admins. 3025 if ($cm && $cm->deletioninprogress) { 3026 if ($preventredirect) { 3027 throw new moodle_exception('activityisscheduledfordeletion'); 3028 } 3029 require_once($CFG->dirroot . '/course/lib.php'); 3030 redirect(course_get_url($course), get_string('activityisscheduledfordeletion', 'error')); 3031 } 3032 3033 // Check visibility of activity to current user; includes visible flag, conditional availability, etc. 3034 if ($cm && !$cm->uservisible) { 3035 if ($preventredirect) { 3036 throw new require_login_exception('Activity is hidden'); 3037 } 3038 // Get the error message that activity is not available and why (if explanation can be shown to the user). 3039 $PAGE->set_course($course); 3040 $renderer = $PAGE->get_renderer('course'); 3041 $message = $renderer->course_section_cm_unavailable_error_message($cm); 3042 redirect(course_get_url($course), $message, null, \core\output\notification::NOTIFY_ERROR); 3043 } 3044 3045 // Set the global $COURSE. 3046 if ($cm) { 3047 $PAGE->set_cm($cm, $course); 3048 $PAGE->set_pagelayout('incourse'); 3049 } else if (!empty($courseorid)) { 3050 $PAGE->set_course($course); 3051 } 3052 3053 foreach ($afterlogins as $plugintype => $plugins) { 3054 foreach ($plugins as $pluginfunction) { 3055 $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); 3056 } 3057 } 3058 3059 // Finally access granted, update lastaccess times. 3060 // Do not update access time for webservice or ajax requests. 3061 if (!WS_SERVER && !AJAX_SCRIPT) { 3062 user_accesstime_log($course->id); 3063 } 3064 } 3065 3066 /** 3067 * A convenience function for where we must be logged in as admin 3068 * @return void 3069 */ 3070 function require_admin() { 3071 require_login(null, false); 3072 require_capability('moodle/site:config', context_system::instance()); 3073 } 3074 3075 /** 3076 * This function just makes sure a user is logged out. 3077 * 3078 * @package core_access 3079 * @category access 3080 */ 3081 function require_logout() { 3082 global $USER, $DB; 3083 3084 if (!isloggedin()) { 3085 // This should not happen often, no need for hooks or events here. 3086 \core\session\manager::terminate_current(); 3087 return; 3088 } 3089 3090 // Execute hooks before action. 3091 $authplugins = array(); 3092 $authsequence = get_enabled_auth_plugins(); 3093 foreach ($authsequence as $authname) { 3094 $authplugins[$authname] = get_auth_plugin($authname); 3095 $authplugins[$authname]->prelogout_hook(); 3096 } 3097 3098 // Store info that gets removed during logout. 3099 $sid = session_id(); 3100 $event = \core\event\user_loggedout::create( 3101 array( 3102 'userid' => $USER->id, 3103 'objectid' => $USER->id, 3104 'other' => array('sessionid' => $sid), 3105 ) 3106 ); 3107 if ($session = $DB->get_record('sessions', array('sid'=>$sid))) { 3108 $event->add_record_snapshot('sessions', $session); 3109 } 3110 3111 // Clone of $USER object to be used by auth plugins. 3112 $user = fullclone($USER); 3113 3114 // Delete session record and drop $_SESSION content. 3115 \core\session\manager::terminate_current(); 3116 3117 // Trigger event AFTER action. 3118 $event->trigger(); 3119 3120 // Hook to execute auth plugins redirection after event trigger. 3121 foreach ($authplugins as $authplugin) { 3122 $authplugin->postlogout_hook($user); 3123 } 3124 } 3125 3126 /** 3127 * Weaker version of require_login() 3128 * 3129 * This is a weaker version of {@link require_login()} which only requires login 3130 * when called from within a course rather than the site page, unless 3131 * the forcelogin option is turned on. 3132 * @see require_login() 3133 * 3134 * @package core_access 3135 * @category access 3136 * 3137 * @param mixed $courseorid The course object or id in question 3138 * @param bool $autologinguest Allow autologin guests if that is wanted 3139 * @param object $cm Course activity module if known 3140 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to 3141 * true. Used to avoid (=false) some scripts (file.php...) to set that variable, 3142 * in order to keep redirects working properly. MDL-14495 3143 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions 3144 * @return void 3145 * @throws coding_exception 3146 */ 3147 function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) { 3148 global $CFG, $PAGE, $SITE; 3149 $issite = ((is_object($courseorid) and $courseorid->id == SITEID) 3150 or (!is_object($courseorid) and $courseorid == SITEID)); 3151 if ($issite && !empty($cm) && !($cm instanceof cm_info)) { 3152 // Note: nearly all pages call get_fast_modinfo anyway and it does not make any 3153 // db queries so this is not really a performance concern, however it is obviously 3154 // better if you use get_fast_modinfo to get the cm before calling this. 3155 if (is_object($courseorid)) { 3156 $course = $courseorid; 3157 } else { 3158 $course = clone($SITE); 3159 } 3160 $modinfo = get_fast_modinfo($course); 3161 $cm = $modinfo->get_cm($cm->id); 3162 } 3163 if (!empty($CFG->forcelogin)) { 3164 // Login required for both SITE and courses. 3165 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); 3166 3167 } else if ($issite && !empty($cm) and !$cm->uservisible) { 3168 // Always login for hidden activities. 3169 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); 3170 3171 } else if (isloggedin() && !isguestuser()) { 3172 // User is already logged in. Make sure the login is complete (user is fully setup, policies agreed). 3173 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); 3174 3175 } else if ($issite) { 3176 // Login for SITE not required. 3177 // We still need to instatiate PAGE vars properly so that things that rely on it like navigation function correctly. 3178 if (!empty($courseorid)) { 3179 if (is_object($courseorid)) { 3180 $course = $courseorid; 3181 } else { 3182 $course = clone $SITE; 3183 } 3184 if ($cm) { 3185 if ($cm->course != $course->id) { 3186 throw new coding_exception('course and cm parameters in require_course_login() call do not match!!'); 3187 } 3188 $PAGE->set_cm($cm, $course); 3189 $PAGE->set_pagelayout('incourse'); 3190 } else { 3191 $PAGE->set_course($course); 3192 } 3193 } else { 3194 // If $PAGE->course, and hence $PAGE->context, have not already been set up properly, set them up now. 3195 $PAGE->set_course($PAGE->course); 3196 } 3197 // Do not update access time for webservice or ajax requests. 3198 if (!WS_SERVER && !AJAX_SCRIPT) { 3199 user_accesstime_log(SITEID); 3200 } 3201 return; 3202 3203 } else { 3204 // Course login always required. 3205 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); 3206 } 3207 } 3208 3209 /** 3210 * Validates a user key, checking if the key exists, is not expired and the remote ip is correct. 3211 * 3212 * @param string $keyvalue the key value 3213 * @param string $script unique script identifier 3214 * @param int $instance instance id 3215 * @return stdClass the key entry in the user_private_key table 3216 * @since Moodle 3.2 3217 * @throws moodle_exception 3218 */ 3219 function validate_user_key($keyvalue, $script, $instance) { 3220 global $DB; 3221 3222 if (!$key = $DB->get_record('user_private_key', array('script' => $script, 'value' => $keyvalue, 'instance' => $instance))) { 3223 print_error('invalidkey'); 3224 } 3225 3226 if (!empty($key->validuntil) and $key->validuntil < time()) { 3227 print_error('expiredkey'); 3228 } 3229 3230 if ($key->iprestriction) { 3231 $remoteaddr = getremoteaddr(null); 3232 if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) { 3233 print_error('ipmismatch'); 3234 } 3235 } 3236 return $key; 3237 } 3238 3239 /** 3240 * Require key login. Function terminates with error if key not found or incorrect. 3241 * 3242 * @uses NO_MOODLE_COOKIES 3243 * @uses PARAM_ALPHANUM 3244 * @param string $script unique script identifier 3245 * @param int $instance optional instance id 3246 * @param string $keyvalue The key. If not supplied, this will be fetched from the current session. 3247 * @return int Instance ID 3248 */ 3249 function require_user_key_login($script, $instance = null, $keyvalue = null) { 3250 global $DB; 3251 3252 if (!NO_MOODLE_COOKIES) { 3253 print_error('sessioncookiesdisable'); 3254 } 3255 3256 // Extra safety. 3257 \core\session\manager::write_close(); 3258 3259 if (null === $keyvalue) { 3260 $keyvalue = required_param('key', PARAM_ALPHANUM); 3261 } 3262 3263 $key = validate_user_key($keyvalue, $script, $instance); 3264 3265 if (!$user = $DB->get_record('user', array('id' => $key->userid))) { 3266 print_error('invaliduserid'); 3267 } 3268 3269 core_user::require_active_user($user, true, true); 3270 3271 // Emulate normal session. 3272 enrol_check_plugins($user); 3273 \core\session\manager::set_user($user); 3274 3275 // Note we are not using normal login. 3276 if (!defined('USER_KEY_LOGIN')) { 3277 define('USER_KEY_LOGIN', true); 3278 } 3279 3280 // Return instance id - it might be empty. 3281 return $key->instance; 3282 } 3283 3284 /** 3285 * Creates a new private user access key. 3286 * 3287 * @param string $script unique target identifier 3288 * @param int $userid 3289 * @param int $instance optional instance id 3290 * @param string $iprestriction optional ip restricted access 3291 * @param int $validuntil key valid only until given data 3292 * @return string access key value 3293 */ 3294 function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) { 3295 global $DB; 3296 3297 $key = new stdClass(); 3298 $key->script = $script; 3299 $key->userid = $userid; 3300 $key->instance = $instance; 3301 $key->iprestriction = $iprestriction; 3302 $key->validuntil = $validuntil; 3303 $key->timecreated = time(); 3304 3305 // Something long and unique. 3306 $key->value = md5($userid.'_'.time().random_string(40)); 3307 while ($DB->record_exists('user_private_key', array('value' => $key->value))) { 3308 // Must be unique. 3309 $key->value = md5($userid.'_'.time().random_string(40)); 3310 } 3311 $DB->insert_record('user_private_key', $key); 3312 return $key->value; 3313 } 3314 3315 /** 3316 * Delete the user's new private user access keys for a particular script. 3317 * 3318 * @param string $script unique target identifier 3319 * @param int $userid 3320 * @return void 3321 */ 3322 function delete_user_key($script, $userid) { 3323 global $DB; 3324 $DB->delete_records('user_private_key', array('script' => $script, 'userid' => $userid)); 3325 } 3326 3327 /** 3328 * Gets a private user access key (and creates one if one doesn't exist). 3329 * 3330 * @param string $script unique target identifier 3331 * @param int $userid 3332 * @param int $instance optional instance id 3333 * @param string $iprestriction optional ip restricted access 3334 * @param int $validuntil key valid only until given date 3335 * @return string access key value 3336 */ 3337 function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) { 3338 global $DB; 3339 3340 if ($key = $DB->get_record('user_private_key', array('script' => $script, 'userid' => $userid, 3341 'instance' => $instance, 'iprestriction' => $iprestriction, 3342 'validuntil' => $validuntil))) { 3343 return $key->value; 3344 } else { 3345 return create_user_key($script, $userid, $instance, $iprestriction, $validuntil); 3346 } 3347 } 3348 3349 3350 /** 3351 * Modify the user table by setting the currently logged in user's last login to now. 3352 * 3353 * @return bool Always returns true 3354 */ 3355 function update_user_login_times() { 3356 global $USER, $DB; 3357 3358 if (isguestuser()) { 3359 // Do not update guest access times/ips for performance. 3360 return true; 3361 } 3362 3363 $now = time(); 3364 3365 $user = new stdClass(); 3366 $user->id = $USER->id; 3367 3368 // Make sure all users that logged in have some firstaccess. 3369 if ($USER->firstaccess == 0) { 3370 $USER->firstaccess = $user->firstaccess = $now; 3371 } 3372 3373 // Store the previous current as lastlogin. 3374 $USER->lastlogin = $user->lastlogin = $USER->currentlogin; 3375 3376 $USER->currentlogin = $user->currentlogin = $now; 3377 3378 // Function user_accesstime_log() may not update immediately, better do it here. 3379 $USER->lastaccess = $user->lastaccess = $now; 3380 $USER->lastip = $user->lastip = getremoteaddr(); 3381 3382 // Note: do not call user_update_user() here because this is part of the login process, 3383 // the login event means that these fields were updated. 3384 $DB->update_record('user', $user); 3385 return true; 3386 } 3387 3388 /** 3389 * Determines if a user has completed setting up their account. 3390 * 3391 * The lax mode (with $strict = false) has been introduced for special cases 3392 * only where we want to skip certain checks intentionally. This is valid in 3393 * certain mnet or ajax scenarios when the user cannot / should not be 3394 * redirected to edit their profile. In most cases, you should perform the 3395 * strict check. 3396 * 3397 * @param stdClass $user A {@link $USER} object to test for the existence of a valid name and email 3398 * @param bool $strict Be more strict and assert id and custom profile fields set, too 3399 * @return bool 3400 */ 3401 function user_not_fully_set_up($user, $strict = true) { 3402 global $CFG; 3403 require_once($CFG->dirroot.'/user/profile/lib.php'); 3404 3405 if (isguestuser($user)) { 3406 return false; 3407 } 3408 3409 if (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user)) { 3410 return true; 3411 } 3412 3413 if ($strict) { 3414 if (empty($user->id)) { 3415 // Strict mode can be used with existing accounts only. 3416 return true; 3417 } 3418 if (!profile_has_required_custom_fields_set($user->id)) { 3419 return true; 3420 } 3421 } 3422 3423 return false; 3424 } 3425 3426 /** 3427 * Check whether the user has exceeded the bounce threshold 3428 * 3429 * @param stdClass $user A {@link $USER} object 3430 * @return bool true => User has exceeded bounce threshold 3431 */ 3432 function over_bounce_threshold($user) { 3433 global $CFG, $DB; 3434 3435 if (empty($CFG->handlebounces)) { 3436 return false; 3437 } 3438 3439 if (empty($user->id)) { 3440 // No real (DB) user, nothing to do here. 3441 return false; 3442 } 3443 3444 // Set sensible defaults. 3445 if (empty($CFG->minbounces)) { 3446 $CFG->minbounces = 10; 3447 } 3448 if (empty($CFG->bounceratio)) { 3449 $CFG->bounceratio = .20; 3450 } 3451 $bouncecount = 0; 3452 $sendcount = 0; 3453 if ($bounce = $DB->get_record('user_preferences', array ('userid' => $user->id, 'name' => 'email_bounce_count'))) { 3454 $bouncecount = $bounce->value; 3455 } 3456 if ($send = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) { 3457 $sendcount = $send->value; 3458 } 3459 return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio); 3460 } 3461 3462 /** 3463 * Used to increment or reset email sent count 3464 * 3465 * @param stdClass $user object containing an id 3466 * @param bool $reset will reset the count to 0 3467 * @return void 3468 */ 3469 function set_send_count($user, $reset=false) { 3470 global $DB; 3471 3472 if (empty($user->id)) { 3473 // No real (DB) user, nothing to do here. 3474 return; 3475 } 3476 3477 if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) { 3478 $pref->value = (!empty($reset)) ? 0 : $pref->value+1; 3479 $DB->update_record('user_preferences', $pref); 3480 } else if (!empty($reset)) { 3481 // If it's not there and we're resetting, don't bother. Make a new one. 3482 $pref = new stdClass(); 3483 $pref->name = 'email_send_count'; 3484 $pref->value = 1; 3485 $pref->userid = $user->id; 3486 $DB->insert_record('user_preferences', $pref, false); 3487 } 3488 } 3489 3490 /** 3491 * Increment or reset user's email bounce count 3492 * 3493 * @param stdClass $user object containing an id 3494 * @param bool $reset will reset the count to 0 3495 */ 3496 function set_bounce_count($user, $reset=false) { 3497 global $DB; 3498 3499 if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_bounce_count'))) { 3500 $pref->value = (!empty($reset)) ? 0 : $pref->value+1; 3501 $DB->update_record('user_preferences', $pref); 3502 } else if (!empty($reset)) { 3503 // If it's not there and we're resetting, don't bother. Make a new one. 3504 $pref = new stdClass(); 3505 $pref->name = 'email_bounce_count'; 3506 $pref->value = 1; 3507 $pref->userid = $user->id; 3508 $DB->insert_record('user_preferences', $pref, false); 3509 } 3510 } 3511 3512 /** 3513 * Determines if the logged in user is currently moving an activity 3514 * 3515 * @param int $courseid The id of the course being tested 3516 * @return bool 3517 */ 3518 function ismoving($courseid) { 3519 global $USER; 3520 3521 if (!empty($USER->activitycopy)) { 3522 return ($USER->activitycopycourse == $courseid); 3523 } 3524 return false; 3525 } 3526 3527 /** 3528 * Returns a persons full name 3529 * 3530 * Given an object containing all of the users name values, this function returns a string with the full name of the person. 3531 * The result may depend on system settings or language. 'override' will force the alternativefullnameformat to be used. In 3532 * English, fullname as well as alternativefullnameformat is set to 'firstname lastname' by default. But you could have 3533 * fullname set to 'firstname lastname' and alternativefullnameformat set to 'firstname middlename alternatename lastname'. 3534 * 3535 * @param stdClass $user A {@link $USER} object to get full name of. 3536 * @param bool $override If true then the alternativefullnameformat format rather than fullnamedisplay format will be used. 3537 * @return string 3538 */ 3539 function fullname($user, $override=false) { 3540 global $CFG, $SESSION; 3541 3542 if (!isset($user->firstname) and !isset($user->lastname)) { 3543 return ''; 3544 } 3545 3546 // Get all of the name fields. 3547 $allnames = get_all_user_name_fields(); 3548 if ($CFG->debugdeveloper) { 3549 foreach ($allnames as $allname) { 3550 if (!property_exists($user, $allname)) { 3551 // If all the user name fields are not set in the user object, then notify the programmer that it needs to be fixed. 3552 debugging('You need to update your sql to include additional name fields in the user object.', DEBUG_DEVELOPER); 3553 // Message has been sent, no point in sending the message multiple times. 3554 break; 3555 } 3556 } 3557 } 3558 3559 if (!$override) { 3560 if (!empty($CFG->forcefirstname)) { 3561 $user->firstname = $CFG->forcefirstname; 3562 } 3563 if (!empty($CFG->forcelastname)) { 3564 $user->lastname = $CFG->forcelastname; 3565 } 3566 } 3567 3568 if (!empty($SESSION->fullnamedisplay)) { 3569 $CFG->fullnamedisplay = $SESSION->fullnamedisplay; 3570 } 3571 3572 $template = null; 3573 // If the fullnamedisplay setting is available, set the template to that. 3574 if (isset($CFG->fullnamedisplay)) { 3575 $template = $CFG->fullnamedisplay; 3576 } 3577 // If the template is empty, or set to language, return the language string. 3578 if ((empty($template) || $template == 'language') && !$override) { 3579 return get_string('fullnamedisplay', null, $user); 3580 } 3581 3582 // Check to see if we are displaying according to the alternative full name format. 3583 if ($override) { 3584 if (empty($CFG->alternativefullnameformat) || $CFG->alternativefullnameformat == 'language') { 3585 // Default to show just the user names according to the fullnamedisplay string. 3586 return get_string('fullnamedisplay', null, $user); 3587 } else { 3588 // If the override is true, then change the template to use the complete name. 3589 $template = $CFG->alternativefullnameformat; 3590 } 3591 } 3592 3593 $requirednames = array(); 3594 // With each name, see if it is in the display name template, and add it to the required names array if it is. 3595 foreach ($allnames as $allname) { 3596 if (strpos($template, $allname) !== false) { 3597 $requirednames[] = $allname; 3598 } 3599 } 3600 3601 $displayname = $template; 3602 // Switch in the actual data into the template. 3603 foreach ($requirednames as $altname) { 3604 if (isset($user->$altname)) { 3605 // Using empty() on the below if statement causes breakages. 3606 if ((string)$user->$altname == '') { 3607 $displayname = str_replace($altname, 'EMPTY', $displayname); 3608 } else { 3609 $displayname = str_replace($altname, $user->$altname, $displayname); 3610 } 3611 } else { 3612 $displayname = str_replace($altname, 'EMPTY', $displayname); 3613 } 3614 } 3615 // Tidy up any misc. characters (Not perfect, but gets most characters). 3616 // Don't remove the "u" at the end of the first expression unless you want garbled characters when combining hiragana or 3617 // katakana and parenthesis. 3618 $patterns = array(); 3619 // This regular expression replacement is to fix problems such as 'James () Kirk' Where 'Tiberius' (middlename) has not been 3620 // filled in by a user. 3621 // The special characters are Japanese brackets that are common enough to make allowances for them (not covered by :punct:). 3622 $patterns[] = '/[[:punct:]「」]*EMPTY[[:punct:]「」]*/u'; 3623 // This regular expression is to remove any double spaces in the display name. 3624 $patterns[] = '/\s{2,}/u'; 3625 foreach ($patterns as $pattern) { 3626 $displayname = preg_replace($pattern, ' ', $displayname); 3627 } 3628 3629 // Trimming $displayname will help the next check to ensure that we don't have a display name with spaces. 3630 $displayname = trim($displayname); 3631 if (empty($displayname)) { 3632 // Going with just the first name if no alternate fields are filled out. May be changed later depending on what 3633 // people in general feel is a good setting to fall back on. 3634 $displayname = $user->firstname; 3635 } 3636 return $displayname; 3637 } 3638 3639 /** 3640 * A centralised location for the all name fields. Returns an array / sql string snippet. 3641 * 3642 * @param bool $returnsql True for an sql select field snippet. 3643 * @param string $tableprefix table query prefix to use in front of each field. 3644 * @param string $prefix prefix added to the name fields e.g. authorfirstname. 3645 * @param string $fieldprefix sql field prefix e.g. id AS userid. 3646 * @param bool $order moves firstname and lastname to the top of the array / start of the string. 3647 * @return array|string All name fields. 3648 */ 3649 function get_all_user_name_fields($returnsql = false, $tableprefix = null, $prefix = null, $fieldprefix = null, $order = false) { 3650 // This array is provided in this order because when called by fullname() (above) if firstname is before 3651 // firstnamephonetic str_replace() will change the wrong placeholder. 3652 $alternatenames = array('firstnamephonetic' => 'firstnamephonetic', 3653 'lastnamephonetic' => 'lastnamephonetic', 3654 'middlename' => 'middlename', 3655 'alternatename' => 'alternatename', 3656 'firstname' => 'firstname', 3657 'lastname' => 'lastname'); 3658 3659 // Let's add a prefix to the array of user name fields if provided. 3660 if ($prefix) { 3661 foreach ($alternatenames as $key => $altname) { 3662 $alternatenames[$key] = $prefix . $altname; 3663 } 3664 } 3665 3666 // If we want the end result to have firstname and lastname at the front / top of the result. 3667 if ($order) { 3668 // Move the last two elements (firstname, lastname) off the array and put them at the top. 3669 for ($i = 0; $i < 2; $i++) { 3670 // Get the last element. 3671 $lastelement = end($alternatenames); 3672 // Remove it from the array. 3673 unset($alternatenames[$lastelement]); 3674 // Put the element back on the top of the array. 3675 $alternatenames = array_merge(array($lastelement => $lastelement), $alternatenames); 3676 } 3677 } 3678 3679 // Create an sql field snippet if requested. 3680 if ($returnsql) { 3681 if ($tableprefix) { 3682 if ($fieldprefix) { 3683 foreach ($alternatenames as $key => $altname) { 3684 $alternatenames[$key] = $tableprefix . '.' . $altname . ' AS ' . $fieldprefix . $altname; 3685 } 3686 } else { 3687 foreach ($alternatenames as $key => $altname) { 3688 $alternatenames[$key] = $tableprefix . '.' . $altname; 3689 } 3690 } 3691 } 3692 $alternatenames = implode(',', $alternatenames); 3693 } 3694 return $alternatenames; 3695 } 3696 3697 /** 3698 * Reduces lines of duplicated code for getting user name fields. 3699 * 3700 * See also {@link user_picture::unalias()} 3701 * 3702 * @param object $addtoobject Object to add user name fields to. 3703 * @param object $secondobject Object that contains user name field information. 3704 * @param string $prefix prefix to be added to all fields (including $additionalfields) e.g. authorfirstname. 3705 * @param array $additionalfields Additional fields to be matched with data in the second object. 3706 * The key can be set to the user table field name. 3707 * @return object User name fields. 3708 */ 3709 function username_load_fields_from_object($addtoobject, $secondobject, $prefix = null, $additionalfields = null) { 3710 $fields = get_all_user_name_fields(false, null, $prefix); 3711 if ($additionalfields) { 3712 // Additional fields can specify their own 'alias' such as 'id' => 'userid'. This checks to see if 3713 // the key is a number and then sets the key to the array value. 3714 foreach ($additionalfields as $key => $value) { 3715 if (is_numeric($key)) { 3716 $additionalfields[$value] = $prefix . $value; 3717 unset($additionalfields[$key]); 3718 } else { 3719 $additionalfields[$key] = $prefix . $value; 3720 } 3721 } 3722 $fields = array_merge($fields, $additionalfields); 3723 } 3724 foreach ($fields as $key => $field) { 3725 // Important that we have all of the user name fields present in the object that we are sending back. 3726 $addtoobject->$key = ''; 3727 if (isset($secondobject->$field)) { 3728 $addtoobject->$key = $secondobject->$field; 3729 } 3730 } 3731 return $addtoobject; 3732 } 3733 3734 /** 3735 * Returns an array of values in order of occurance in a provided string. 3736 * The key in the result is the character postion in the string. 3737 * 3738 * @param array $values Values to be found in the string format 3739 * @param string $stringformat The string which may contain values being searched for. 3740 * @return array An array of values in order according to placement in the string format. 3741 */ 3742 function order_in_string($values, $stringformat) { 3743 $valuearray = array(); 3744 foreach ($values as $value) { 3745 $pattern = "/$value\b/"; 3746 // Using preg_match as strpos() may match values that are similar e.g. firstname and firstnamephonetic. 3747 if (preg_match($pattern, $stringformat)) { 3748 $replacement = "thing"; 3749 // Replace the value with something more unique to ensure we get the right position when using strpos(). 3750 $newformat = preg_replace($pattern, $replacement, $stringformat); 3751 $position = strpos($newformat, $replacement); 3752 $valuearray[$position] = $value; 3753 } 3754 } 3755 ksort($valuearray); 3756 return $valuearray; 3757 } 3758 3759 /** 3760 * Checks if current user is shown any extra fields when listing users. 3761 * 3762 * @param object $context Context 3763 * @param array $already Array of fields that we're going to show anyway 3764 * so don't bother listing them 3765 * @return array Array of field names from user table, not including anything 3766 * listed in $already 3767 */ 3768 function get_extra_user_fields($context, $already = array()) { 3769 global $CFG; 3770 3771 // Only users with permission get the extra fields. 3772 if (!has_capability('moodle/site:viewuseridentity', $context)) { 3773 return array(); 3774 } 3775 3776 // Split showuseridentity on comma (filter needed in case the showuseridentity is empty). 3777 $extra = array_filter(explode(',', $CFG->showuseridentity)); 3778 3779 foreach ($extra as $key => $field) { 3780 if (in_array($field, $already)) { 3781 unset($extra[$key]); 3782 } 3783 } 3784 3785 // If the identity fields are also among hidden fields, make sure the user can see them. 3786 $hiddenfields = array_filter(explode(',', $CFG->hiddenuserfields)); 3787 $hiddenidentifiers = array_intersect($extra, $hiddenfields); 3788 3789 if ($hiddenidentifiers) { 3790 if ($context->get_course_context(false)) { 3791 // We are somewhere inside a course. 3792 $canviewhiddenuserfields = has_capability('moodle/course:viewhiddenuserfields', $context); 3793 3794 } else { 3795 // We are not inside a course. 3796 $canviewhiddenuserfields = has_capability('moodle/user:viewhiddendetails', $context); 3797 } 3798 3799 if (!$canviewhiddenuserfields) { 3800 // Remove hidden identifiers from the list. 3801 $extra = array_diff($extra, $hiddenidentifiers); 3802 } 3803 } 3804 3805 // Re-index the entries. 3806 $extra = array_values($extra); 3807 3808 return $extra; 3809 } 3810 3811 /** 3812 * If the current user is to be shown extra user fields when listing or 3813 * selecting users, returns a string suitable for including in an SQL select 3814 * clause to retrieve those fields. 3815 * 3816 * @param context $context Context 3817 * @param string $alias Alias of user table, e.g. 'u' (default none) 3818 * @param string $prefix Prefix for field names using AS, e.g. 'u_' (default none) 3819 * @param array $already Array of fields that we're going to include anyway so don't list them (default none) 3820 * @return string Partial SQL select clause, beginning with comma, for example ',u.idnumber,u.department' unless it is blank 3821 */ 3822 function get_extra_user_fields_sql($context, $alias='', $prefix='', $already = array()) { 3823 $fields = get_extra_user_fields($context, $already); 3824 $result = ''; 3825 // Add punctuation for alias. 3826 if ($alias !== '') { 3827 $alias .= '.'; 3828 } 3829 foreach ($fields as $field) { 3830 $result .= ', ' . $alias . $field; 3831 if ($prefix) { 3832 $result .= ' AS ' . $prefix . $field; 3833 } 3834 } 3835 return $result; 3836 } 3837 3838 /** 3839 * Returns the display name of a field in the user table. Works for most fields that are commonly displayed to users. 3840 * @param string $field Field name, e.g. 'phone1' 3841 * @return string Text description taken from language file, e.g. 'Phone number' 3842 */ 3843 function get_user_field_name($field) { 3844 // Some fields have language strings which are not the same as field name. 3845 switch ($field) { 3846 case 'url' : { 3847 return get_string('webpage'); 3848 } 3849 case 'icq' : { 3850 return get_string('icqnumber'); 3851 } 3852 case 'skype' : { 3853 return get_string('skypeid'); 3854 } 3855 case 'aim' : { 3856 return get_string('aimid'); 3857 } 3858 case 'yahoo' : { 3859 return get_string('yahooid'); 3860 } 3861 case 'msn' : { 3862 return get_string('msnid'); 3863 } 3864 case 'picture' : { 3865 return get_string('pictureofuser'); 3866 } 3867 } 3868 // Otherwise just use the same lang string. 3869 return get_string($field); 3870 } 3871 3872 /** 3873 * Returns whether a given authentication plugin exists. 3874 * 3875 * @param string $auth Form of authentication to check for. Defaults to the global setting in {@link $CFG}. 3876 * @return boolean Whether the plugin is available. 3877 */ 3878 function exists_auth_plugin($auth) { 3879 global $CFG; 3880 3881 if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) { 3882 return is_readable("{$CFG->dirroot}/auth/$auth/auth.php"); 3883 } 3884 return false; 3885 } 3886 3887 /** 3888 * Checks if a given plugin is in the list of enabled authentication plugins. 3889 * 3890 * @param string $auth Authentication plugin. 3891 * @return boolean Whether the plugin is enabled. 3892 */ 3893 function is_enabled_auth($auth) { 3894 if (empty($auth)) { 3895 return false; 3896 } 3897 3898 $enabled = get_enabled_auth_plugins(); 3899 3900 return in_array($auth, $enabled); 3901 } 3902 3903 /** 3904 * Returns an authentication plugin instance. 3905 * 3906 * @param string $auth name of authentication plugin 3907 * @return auth_plugin_base An instance of the required authentication plugin. 3908 */ 3909 function get_auth_plugin($auth) { 3910 global $CFG; 3911 3912 // Check the plugin exists first. 3913 if (! exists_auth_plugin($auth)) { 3914 print_error('authpluginnotfound', 'debug', '', $auth); 3915 } 3916 3917 // Return auth plugin instance. 3918 require_once("{$CFG->dirroot}/auth/$auth/auth.php"); 3919 $class = "auth_plugin_$auth"; 3920 return new $class; 3921 } 3922 3923 /** 3924 * Returns array of active auth plugins. 3925 * 3926 * @param bool $fix fix $CFG->auth if needed. Only set if logged in as admin. 3927 * @return array 3928 */ 3929 function get_enabled_auth_plugins($fix=false) { 3930 global $CFG; 3931 3932 $default = array('manual', 'nologin'); 3933 3934 if (empty($CFG->auth)) { 3935 $auths = array(); 3936 } else { 3937 $auths = explode(',', $CFG->auth); 3938 } 3939 3940 $auths = array_unique($auths); 3941 $oldauthconfig = implode(',', $auths); 3942 foreach ($auths as $k => $authname) { 3943 if (in_array($authname, $default)) { 3944 // The manual and nologin plugin never need to be stored. 3945 unset($auths[$k]); 3946 } else if (!exists_auth_plugin($authname)) { 3947 debugging(get_string('authpluginnotfound', 'debug', $authname)); 3948 unset($auths[$k]); 3949 } 3950 } 3951 3952 // Ideally only explicit interaction from a human admin should trigger a 3953 // change in auth config, see MDL-70424 for details. 3954 if ($fix) { 3955 $newconfig = implode(',', $auths); 3956 if (!isset($CFG->auth) or $newconfig != $CFG->auth) { 3957 set_config('auth', $newconfig); 3958 } 3959 } 3960 3961 return (array_merge($default, $auths)); 3962 } 3963 3964 /** 3965 * Returns true if an internal authentication method is being used. 3966 * if method not specified then, global default is assumed 3967 * 3968 * @param string $auth Form of authentication required 3969 * @return bool 3970 */ 3971 function is_internal_auth($auth) { 3972 // Throws error if bad $auth. 3973 $authplugin = get_auth_plugin($auth); 3974 return $authplugin->is_internal(); 3975 } 3976 3977 /** 3978 * Returns true if the user is a 'restored' one. 3979 * 3980 * Used in the login process to inform the user and allow him/her to reset the password 3981 * 3982 * @param string $username username to be checked 3983 * @return bool 3984 */ 3985 function is_restored_user($username) { 3986 global $CFG, $DB; 3987 3988 return $DB->record_exists('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'password' => 'restored')); 3989 } 3990 3991 /** 3992 * Returns an array of user fields 3993 * 3994 * @return array User field/column names 3995 */ 3996 function get_user_fieldnames() { 3997 global $DB; 3998 3999 $fieldarray = $DB->get_columns('user'); 4000 unset($fieldarray['id']); 4001 $fieldarray = array_keys($fieldarray); 4002 4003 return $fieldarray; 4004 } 4005 4006 /** 4007 * Creates a bare-bones user record 4008 * 4009 * @todo Outline auth types and provide code example 4010 * 4011 * @param string $username New user's username to add to record 4012 * @param string $password New user's password to add to record 4013 * @param string $auth Form of authentication required 4014 * @return stdClass A complete user object 4015 */ 4016 function create_user_record($username, $password, $auth = 'manual') { 4017 global $CFG, $DB; 4018 require_once($CFG->dirroot.'/user/profile/lib.php'); 4019 require_once($CFG->dirroot.'/user/lib.php'); 4020 4021 // Just in case check text case. 4022 $username = trim(core_text::strtolower($username)); 4023 4024 $authplugin = get_auth_plugin($auth); 4025 $customfields = $authplugin->get_custom_user_profile_fields(); 4026 $newuser = new stdClass(); 4027 if ($newinfo = $authplugin->get_userinfo($username)) { 4028 $newinfo = truncate_userinfo($newinfo); 4029 foreach ($newinfo as $key => $value) { 4030 if (in_array($key, $authplugin->userfields) || (in_array($key, $customfields))) { 4031 $newuser->$key = $value; 4032 } 4033 } 4034 } 4035 4036 if (!empty($newuser->email)) { 4037 if (email_is_not_allowed($newuser->email)) { 4038 unset($newuser->email); 4039 } 4040 } 4041 4042 if (!isset($newuser->city)) { 4043 $newuser->city = ''; 4044 } 4045 4046 $newuser->auth = $auth; 4047 $newuser->username = $username; 4048 4049 // Fix for MDL-8480 4050 // user CFG lang for user if $newuser->lang is empty 4051 // or $user->lang is not an installed language. 4052 if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) { 4053 $newuser->lang = $CFG->lang; 4054 } 4055 $newuser->confirmed = 1; 4056 $newuser->lastip = getremoteaddr(); 4057 $newuser->timecreated = time(); 4058 $newuser->timemodified = $newuser->timecreated; 4059 $newuser->mnethostid = $CFG->mnet_localhost_id; 4060 4061 $newuser->id = user_create_user($newuser, false, false); 4062 4063 // Save user profile data. 4064 profile_save_data($newuser); 4065 4066 $user = get_complete_user_data('id', $newuser->id); 4067 if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})) { 4068 set_user_preference('auth_forcepasswordchange', 1, $user); 4069 } 4070 // Set the password. 4071 update_internal_user_password($user, $password); 4072 4073 // Trigger event. 4074 \core\event\user_created::create_from_userid($newuser->id)->trigger(); 4075 4076 return $user; 4077 } 4078 4079 /** 4080 * Will update a local user record from an external source (MNET users can not be updated using this method!). 4081 * 4082 * @param string $username user's username to update the record 4083 * @return stdClass A complete user object 4084 */ 4085 function update_user_record($username) { 4086 global $DB, $CFG; 4087 // Just in case check text case. 4088 $username = trim(core_text::strtolower($username)); 4089 4090 $oldinfo = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id), '*', MUST_EXIST); 4091 return update_user_record_by_id($oldinfo->id); 4092 } 4093 4094 /** 4095 * Will update a local user record from an external source (MNET users can not be updated using this method!). 4096 * 4097 * @param int $id user id 4098 * @return stdClass A complete user object 4099 */ 4100 function update_user_record_by_id($id) { 4101 global $DB, $CFG; 4102 require_once($CFG->dirroot."/user/profile/lib.php"); 4103 require_once($CFG->dirroot.'/user/lib.php'); 4104 4105 $params = array('mnethostid' => $CFG->mnet_localhost_id, 'id' => $id, 'deleted' => 0); 4106 $oldinfo = $DB->get_record('user', $params, '*', MUST_EXIST); 4107 4108 $newuser = array(); 4109 $userauth = get_auth_plugin($oldinfo->auth); 4110 4111 if ($newinfo = $userauth->get_userinfo($oldinfo->username)) { 4112 $newinfo = truncate_userinfo($newinfo); 4113 $customfields = $userauth->get_custom_user_profile_fields(); 4114 4115 foreach ($newinfo as $key => $value) { 4116 $iscustom = in_array($key, $customfields); 4117 if (!$iscustom) { 4118 $key = strtolower($key); 4119 } 4120 if ((!property_exists($oldinfo, $key) && !$iscustom) or $key === 'username' or $key === 'id' 4121 or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') { 4122 // Unknown or must not be changed. 4123 continue; 4124 } 4125 if (empty($userauth->config->{'field_updatelocal_' . $key}) || empty($userauth->config->{'field_lock_' . $key})) { 4126 continue; 4127 } 4128 $confval = $userauth->config->{'field_updatelocal_' . $key}; 4129 $lockval = $userauth->config->{'field_lock_' . $key}; 4130 if ($confval === 'onlogin') { 4131 // MDL-4207 Don't overwrite modified user profile values with 4132 // empty LDAP values when 'unlocked if empty' is set. The purpose 4133 // of the setting 'unlocked if empty' is to allow the user to fill 4134 // in a value for the selected field _if LDAP is giving 4135 // nothing_ for this field. Thus it makes sense to let this value 4136 // stand in until LDAP is giving a value for this field. 4137 if (!(empty($value) && $lockval === 'unlockedifempty')) { 4138 if ($iscustom || (in_array($key, $userauth->userfields) && 4139 ((string)$oldinfo->$key !== (string)$value))) { 4140 $newuser[$key] = (string)$value; 4141 } 4142 } 4143 } 4144 } 4145 if ($newuser) { 4146 $newuser['id'] = $oldinfo->id; 4147 $newuser['timemodified'] = time(); 4148 user_update_user((object) $newuser, false, false); 4149 4150 // Save user profile data. 4151 profile_save_data((object) $newuser); 4152 4153 // Trigger event. 4154 \core\event\user_updated::create_from_userid($newuser['id'])->trigger(); 4155 } 4156 } 4157 4158 return get_complete_user_data('id', $oldinfo->id); 4159 } 4160 4161 /** 4162 * Will truncate userinfo as it comes from auth_get_userinfo (from external auth) which may have large fields. 4163 * 4164 * @param array $info Array of user properties to truncate if needed 4165 * @return array The now truncated information that was passed in 4166 */ 4167 function truncate_userinfo(array $info) { 4168 // Define the limits. 4169 $limit = array( 4170 'username' => 100, 4171 'idnumber' => 255, 4172 'firstname' => 100, 4173 'lastname' => 100, 4174 'email' => 100, 4175 'icq' => 15, 4176 'phone1' => 20, 4177 'phone2' => 20, 4178 'institution' => 255, 4179 'department' => 255, 4180 'address' => 255, 4181 'city' => 120, 4182 'country' => 2, 4183 'url' => 255, 4184 ); 4185 4186 // Apply where needed. 4187 foreach (array_keys($info) as $key) { 4188 if (!empty($limit[$key])) { 4189 $info[$key] = trim(core_text::substr($info[$key], 0, $limit[$key])); 4190 } 4191 } 4192 4193 return $info; 4194 } 4195 4196 /** 4197 * Marks user deleted in internal user database and notifies the auth plugin. 4198 * Also unenrols user from all roles and does other cleanup. 4199 * 4200 * Any plugin that needs to purge user data should register the 'user_deleted' event. 4201 * 4202 * @param stdClass $user full user object before delete 4203 * @return boolean success 4204 * @throws coding_exception if invalid $user parameter detected 4205 */ 4206 function delete_user(stdClass $user) { 4207 global $CFG, $DB, $SESSION; 4208 require_once($CFG->libdir.'/grouplib.php'); 4209 require_once($CFG->libdir.'/gradelib.php'); 4210 require_once($CFG->dirroot.'/message/lib.php'); 4211 require_once($CFG->dirroot.'/user/lib.php'); 4212 4213 // Make sure nobody sends bogus record type as parameter. 4214 if (!property_exists($user, 'id') or !property_exists($user, 'username')) { 4215 throw new coding_exception('Invalid $user parameter in delete_user() detected'); 4216 } 4217 4218 // Better not trust the parameter and fetch the latest info this will be very expensive anyway. 4219 if (!$user = $DB->get_record('user', array('id' => $user->id))) { 4220 debugging('Attempt to delete unknown user account.'); 4221 return false; 4222 } 4223 4224 // There must be always exactly one guest record, originally the guest account was identified by username only, 4225 // now we use $CFG->siteguest for performance reasons. 4226 if ($user->username === 'guest' or isguestuser($user)) { 4227 debugging('Guest user account can not be deleted.'); 4228 return false; 4229 } 4230 4231 // Admin can be theoretically from different auth plugin, but we want to prevent deletion of internal accoutns only, 4232 // if anything goes wrong ppl may force somebody to be admin via config.php setting $CFG->siteadmins. 4233 if ($user->auth === 'manual' and is_siteadmin($user)) { 4234 debugging('Local administrator accounts can not be deleted.'); 4235 return false; 4236 } 4237 4238 // Allow plugins to use this user object before we completely delete it. 4239 if ($pluginsfunction = get_plugins_with_function('pre_user_delete')) { 4240 foreach ($pluginsfunction as $plugintype => $plugins) { 4241 foreach ($plugins as $pluginfunction) { 4242 $pluginfunction($user); 4243 } 4244 } 4245 } 4246 4247 // Keep user record before updating it, as we have to pass this to user_deleted event. 4248 $olduser = clone $user; 4249 4250 // Keep a copy of user context, we need it for event. 4251 $usercontext = context_user::instance($user->id); 4252 4253 // Delete all grades - backup is kept in grade_grades_history table. 4254 grade_user_delete($user->id); 4255 4256 // TODO: remove from cohorts using standard API here. 4257 4258 // Remove user tags. 4259 core_tag_tag::remove_all_item_tags('core', 'user', $user->id); 4260 4261 // Unconditionally unenrol from all courses. 4262 enrol_user_delete($user); 4263 4264 // Unenrol from all roles in all contexts. 4265 // This might be slow but it is really needed - modules might do some extra cleanup! 4266 role_unassign_all(array('userid' => $user->id)); 4267 4268 // Notify the competency subsystem. 4269 \core_competency\api::hook_user_deleted($user->id); 4270 4271 // Now do a brute force cleanup. 4272 4273 // Delete all user events and subscription events. 4274 $DB->delete_records_select('event', 'userid = :userid AND subscriptionid IS NOT NULL', ['userid' => $user->id]); 4275 4276 // Now, delete all calendar subscription from the user. 4277 $DB->delete_records('event_subscriptions', ['userid' => $user->id]); 4278 4279 // Remove from all cohorts. 4280 $DB->delete_records('cohort_members', array('userid' => $user->id)); 4281 4282 // Remove from all groups. 4283 $DB->delete_records('groups_members', array('userid' => $user->id)); 4284 4285 // Brute force unenrol from all courses. 4286 $DB->delete_records('user_enrolments', array('userid' => $user->id)); 4287 4288 // Purge user preferences. 4289 $DB->delete_records('user_preferences', array('userid' => $user->id)); 4290 4291 // Purge user extra profile info. 4292 $DB->delete_records('user_info_data', array('userid' => $user->id)); 4293 4294 // Purge log of previous password hashes. 4295 $DB->delete_records('user_password_history', array('userid' => $user->id)); 4296 4297 // Last course access not necessary either. 4298 $DB->delete_records('user_lastaccess', array('userid' => $user->id)); 4299 // Remove all user tokens. 4300 $DB->delete_records('external_tokens', array('userid' => $user->id)); 4301 4302 // Unauthorise the user for all services. 4303 $DB->delete_records('external_services_users', array('userid' => $user->id)); 4304 4305 // Remove users private keys. 4306 $DB->delete_records('user_private_key', array('userid' => $user->id)); 4307 4308 // Remove users customised pages. 4309 $DB->delete_records('my_pages', array('userid' => $user->id, 'private' => 1)); 4310 4311 // Delete user from $SESSION->bulk_users. 4312 if (isset($SESSION->bulk_users[$user->id])) { 4313 unset($SESSION->bulk_users[$user->id]); 4314 } 4315 4316 // Force logout - may fail if file based sessions used, sorry. 4317 \core\session\manager::kill_user_sessions($user->id); 4318 4319 // Generate username from email address, or a fake email. 4320 $delemail = !empty($user->email) ? $user->email : $user->username . '.' . $user->id . '@unknownemail.invalid'; 4321 4322 $deltime = time(); 4323 $deltimelength = core_text::strlen((string) $deltime); 4324 4325 // Max username length is 100 chars. Select up to limit - (length of current time + 1 [period character]) from users email. 4326 $delname = clean_param($delemail, PARAM_USERNAME); 4327 $delname = core_text::substr($delname, 0, 100 - ($deltimelength + 1)) . ".{$deltime}"; 4328 4329 // Workaround for bulk deletes of users with the same email address. 4330 while ($DB->record_exists('user', array('username' => $delname))) { // No need to use mnethostid here. 4331 $delname++; 4332 } 4333 4334 // Mark internal user record as "deleted". 4335 $updateuser = new stdClass(); 4336 $updateuser->id = $user->id; 4337 $updateuser->deleted = 1; 4338 $updateuser->username = $delname; // Remember it just in case. 4339 $updateuser->email = md5($user->username);// Store hash of username, useful importing/restoring users. 4340 $updateuser->idnumber = ''; // Clear this field to free it up. 4341 $updateuser->picture = 0; 4342 $updateuser->timemodified = $deltime; 4343 4344 // Don't trigger update event, as user is being deleted. 4345 user_update_user($updateuser, false, false); 4346 4347 // Delete all content associated with the user context, but not the context itself. 4348 $usercontext->delete_content(); 4349 4350 // Delete any search data. 4351 \core_search\manager::context_deleted($usercontext); 4352 4353 // Any plugin that needs to cleanup should register this event. 4354 // Trigger event. 4355 $event = \core\event\user_deleted::create( 4356 array( 4357 'objectid' => $user->id, 4358 'relateduserid' => $user->id, 4359 'context' => $usercontext, 4360 'other' => array( 4361 'username' => $user->username, 4362 'email' => $user->email, 4363 'idnumber' => $user->idnumber, 4364 'picture' => $user->picture, 4365 'mnethostid' => $user->mnethostid 4366 ) 4367 ) 4368 ); 4369 $event->add_record_snapshot('user', $olduser); 4370 $event->trigger(); 4371 4372 // We will update the user's timemodified, as it will be passed to the user_deleted event, which 4373 // should know about this updated property persisted to the user's table. 4374 $user->timemodified = $updateuser->timemodified; 4375 4376 // Notify auth plugin - do not block the delete even when plugin fails. 4377 $authplugin = get_auth_plugin($user->auth); 4378 $authplugin->user_delete($user); 4379 4380 return true; 4381 } 4382 4383 /** 4384 * Retrieve the guest user object. 4385 * 4386 * @return stdClass A {@link $USER} object 4387 */ 4388 function guest_user() { 4389 global $CFG, $DB; 4390 4391 if ($newuser = $DB->get_record('user', array('id' => $CFG->siteguest))) { 4392 $newuser->confirmed = 1; 4393 $newuser->lang = $CFG->lang; 4394 $newuser->lastip = getremoteaddr(); 4395 } 4396 4397 return $newuser; 4398 } 4399 4400 /** 4401 * Authenticates a user against the chosen authentication mechanism 4402 * 4403 * Given a username and password, this function looks them 4404 * up using the currently selected authentication mechanism, 4405 * and if the authentication is successful, it returns a 4406 * valid $user object from the 'user' table. 4407 * 4408 * Uses auth_ functions from the currently active auth module 4409 * 4410 * After authenticate_user_login() returns success, you will need to 4411 * log that the user has logged in, and call complete_user_login() to set 4412 * the session up. 4413 * 4414 * Note: this function works only with non-mnet accounts! 4415 * 4416 * @param string $username User's username (or also email if $CFG->authloginviaemail enabled) 4417 * @param string $password User's password 4418 * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO 4419 * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists) 4420 * @param mixed logintoken If this is set to a string it is validated against the login token for the session. 4421 * @return stdClass|false A {@link $USER} object or false if error 4422 */ 4423 function authenticate_user_login($username, $password, $ignorelockout=false, &$failurereason=null, $logintoken=false) { 4424 global $CFG, $DB, $PAGE; 4425 require_once("$CFG->libdir/authlib.php"); 4426 4427 if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) { 4428 // we have found the user 4429 4430 } else if (!empty($CFG->authloginviaemail)) { 4431 if ($email = clean_param($username, PARAM_EMAIL)) { 4432 $select = "mnethostid = :mnethostid AND LOWER(email) = LOWER(:email) AND deleted = 0"; 4433 $params = array('mnethostid' => $CFG->mnet_localhost_id, 'email' => $email); 4434 $users = $DB->get_records_select('user', $select, $params, 'id', 'id', 0, 2); 4435 if (count($users) === 1) { 4436 // Use email for login only if unique. 4437 $user = reset($users); 4438 $user = get_complete_user_data('id', $user->id); 4439 $username = $user->username; 4440 } 4441 unset($users); 4442 } 4443 } 4444 4445 // Make sure this request came from the login form. 4446 if (!\core\session\manager::validate_login_token($logintoken)) { 4447 $failurereason = AUTH_LOGIN_FAILED; 4448 4449 // Trigger login failed event (specifying the ID of the found user, if available). 4450 \core\event\user_login_failed::create([ 4451 'userid' => ($user->id ?? 0), 4452 'other' => [ 4453 'username' => $username, 4454 'reason' => $failurereason, 4455 ], 4456 ])->trigger(); 4457 4458 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Invalid Login Token: $username ".$_SERVER['HTTP_USER_AGENT']); 4459 return false; 4460 } 4461 4462 $authsenabled = get_enabled_auth_plugins(); 4463 4464 if ($user) { 4465 // Use manual if auth not set. 4466 $auth = empty($user->auth) ? 'manual' : $user->auth; 4467 4468 if (in_array($user->auth, $authsenabled)) { 4469 $authplugin = get_auth_plugin($user->auth); 4470 $authplugin->pre_user_login_hook($user); 4471 } 4472 4473 if (!empty($user->suspended)) { 4474 $failurereason = AUTH_LOGIN_SUSPENDED; 4475 4476 // Trigger login failed event. 4477 $event = \core\event\user_login_failed::create(array('userid' => $user->id, 4478 'other' => array('username' => $username, 'reason' => $failurereason))); 4479 $event->trigger(); 4480 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']); 4481 return false; 4482 } 4483 if ($auth=='nologin' or !is_enabled_auth($auth)) { 4484 // Legacy way to suspend user. 4485 $failurereason = AUTH_LOGIN_SUSPENDED; 4486 4487 // Trigger login failed event. 4488 $event = \core\event\user_login_failed::create(array('userid' => $user->id, 4489 'other' => array('username' => $username, 'reason' => $failurereason))); 4490 $event->trigger(); 4491 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Disabled Login: $username ".$_SERVER['HTTP_USER_AGENT']); 4492 return false; 4493 } 4494 $auths = array($auth); 4495 4496 } else { 4497 // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user(). 4498 if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'deleted' => 1))) { 4499 $failurereason = AUTH_LOGIN_NOUSER; 4500 4501 // Trigger login failed event. 4502 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, 4503 'reason' => $failurereason))); 4504 $event->trigger(); 4505 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Deleted Login: $username ".$_SERVER['HTTP_USER_AGENT']); 4506 return false; 4507 } 4508 4509 // User does not exist. 4510 $auths = $authsenabled; 4511 $user = new stdClass(); 4512 $user->id = 0; 4513 } 4514 4515 if ($ignorelockout) { 4516 // Some other mechanism protects against brute force password guessing, for example login form might include reCAPTCHA 4517 // or this function is called from a SSO script. 4518 } else if ($user->id) { 4519 // Verify login lockout after other ways that may prevent user login. 4520 if (login_is_lockedout($user)) { 4521 $failurereason = AUTH_LOGIN_LOCKOUT; 4522 4523 // Trigger login failed event. 4524 $event = \core\event\user_login_failed::create(array('userid' => $user->id, 4525 'other' => array('username' => $username, 'reason' => $failurereason))); 4526 $event->trigger(); 4527 4528 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Login lockout: $username ".$_SERVER['HTTP_USER_AGENT']); 4529 return false; 4530 } 4531 } else { 4532 // We can not lockout non-existing accounts. 4533 } 4534 4535 foreach ($auths as $auth) { 4536 $authplugin = get_auth_plugin($auth); 4537 4538 // On auth fail fall through to the next plugin. 4539 if (!$authplugin->user_login($username, $password)) { 4540 continue; 4541 } 4542 4543 // Before performing login actions, check if user still passes password policy, if admin setting is enabled. 4544 if (!empty($CFG->passwordpolicycheckonlogin)) { 4545 $errmsg = ''; 4546 $passed = check_password_policy($password, $errmsg, $user); 4547 if (!$passed) { 4548 // First trigger event for failure. 4549 $failedevent = \core\event\user_password_policy_failed::create_from_user($user); 4550 $failedevent->trigger(); 4551 4552 // If able to change password, set flag and move on. 4553 if ($authplugin->can_change_password()) { 4554 // Check if we are on internal change password page, or service is external, don't show notification. 4555 $internalchangeurl = new moodle_url('/login/change_password.php'); 4556 if (!($PAGE->has_set_url() && $internalchangeurl->compare($PAGE->url)) && $authplugin->is_internal()) { 4557 \core\notification::error(get_string('passwordpolicynomatch', '', $errmsg)); 4558 } 4559 set_user_preference('auth_forcepasswordchange', 1, $user); 4560 } else if ($authplugin->can_reset_password()) { 4561 // Else force a reset if possible. 4562 \core\notification::error(get_string('forcepasswordresetnotice', '', $errmsg)); 4563 redirect(new moodle_url('/login/forgot_password.php')); 4564 } else { 4565 $notifymsg = get_string('forcepasswordresetfailurenotice', '', $errmsg); 4566 // If support page is set, add link for help. 4567 if (!empty($CFG->supportpage)) { 4568 $link = \html_writer::link($CFG->supportpage, $CFG->supportpage); 4569 $link = \html_writer::tag('p', $link); 4570 $notifymsg .= $link; 4571 } 4572 4573 // If no change or reset is possible, add a notification for user. 4574 \core\notification::error($notifymsg); 4575 } 4576 } 4577 } 4578 4579 // Successful authentication. 4580 if ($user->id) { 4581 // User already exists in database. 4582 if (empty($user->auth)) { 4583 // For some reason auth isn't set yet. 4584 $DB->set_field('user', 'auth', $auth, array('id' => $user->id)); 4585 $user->auth = $auth; 4586 } 4587 4588 // If the existing hash is using an out-of-date algorithm (or the legacy md5 algorithm), then we should update to 4589 // the current hash algorithm while we have access to the user's password. 4590 update_internal_user_password($user, $password); 4591 4592 if ($authplugin->is_synchronised_with_external()) { 4593 // Update user record from external DB. 4594 $user = update_user_record_by_id($user->id); 4595 } 4596 } else { 4597 // The user is authenticated but user creation may be disabled. 4598 if (!empty($CFG->authpreventaccountcreation)) { 4599 $failurereason = AUTH_LOGIN_UNAUTHORISED; 4600 4601 // Trigger login failed event. 4602 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, 4603 'reason' => $failurereason))); 4604 $event->trigger(); 4605 4606 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Unknown user, can not create new accounts: $username ". 4607 $_SERVER['HTTP_USER_AGENT']); 4608 return false; 4609 } else { 4610 $user = create_user_record($username, $password, $auth); 4611 } 4612 } 4613 4614 $authplugin->sync_roles($user); 4615 4616 foreach ($authsenabled as $hau) { 4617 $hauth = get_auth_plugin($hau); 4618 $hauth->user_authenticated_hook($user, $username, $password); 4619 } 4620 4621 if (empty($user->id)) { 4622 $failurereason = AUTH_LOGIN_NOUSER; 4623 // Trigger login failed event. 4624 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, 4625 'reason' => $failurereason))); 4626 $event->trigger(); 4627 return false; 4628 } 4629 4630 if (!empty($user->suspended)) { 4631 // Just in case some auth plugin suspended account. 4632 $failurereason = AUTH_LOGIN_SUSPENDED; 4633 // Trigger login failed event. 4634 $event = \core\event\user_login_failed::create(array('userid' => $user->id, 4635 'other' => array('username' => $username, 'reason' => $failurereason))); 4636 $event->trigger(); 4637 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']); 4638 return false; 4639 } 4640 4641 login_attempt_valid($user); 4642 $failurereason = AUTH_LOGIN_OK; 4643 return $user; 4644 } 4645 4646 // Failed if all the plugins have failed. 4647 if (debugging('', DEBUG_ALL)) { 4648 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Failed Login: $username ".$_SERVER['HTTP_USER_AGENT']); 4649 } 4650 4651 if ($user->id) { 4652 login_attempt_failed($user); 4653 $failurereason = AUTH_LOGIN_FAILED; 4654 // Trigger login failed event. 4655 $event = \core\event\user_login_failed::create(array('userid' => $user->id, 4656 'other' => array('username' => $username, 'reason' => $failurereason))); 4657 $event->trigger(); 4658 } else { 4659 $failurereason = AUTH_LOGIN_NOUSER; 4660 // Trigger login failed event. 4661 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, 4662 'reason' => $failurereason))); 4663 $event->trigger(); 4664 } 4665 4666 return false; 4667 } 4668 4669 /** 4670 * Call to complete the user login process after authenticate_user_login() 4671 * has succeeded. It will setup the $USER variable and other required bits 4672 * and pieces. 4673 * 4674 * NOTE: 4675 * - It will NOT log anything -- up to the caller to decide what to log. 4676 * - this function does not set any cookies any more! 4677 * 4678 * @param stdClass $user 4679 * @return stdClass A {@link $USER} object - BC only, do not use 4680 */ 4681 function complete_user_login($user) { 4682 global $CFG, $DB, $USER, $SESSION; 4683 4684 \core\session\manager::login_user($user); 4685 4686 // Reload preferences from DB. 4687 unset($USER->preference); 4688 check_user_preferences_loaded($USER); 4689 4690 // Update login times. 4691 update_user_login_times(); 4692 4693 // Extra session prefs init. 4694 set_login_session_preferences(); 4695 4696 // Trigger login event. 4697 $event = \core\event\user_loggedin::create( 4698 array( 4699 'userid' => $USER->id, 4700 'objectid' => $USER->id, 4701 'other' => array('username' => $USER->username), 4702 ) 4703 ); 4704 $event->trigger(); 4705 4706 // Queue migrating the messaging data, if we need to. 4707 if (!get_user_preferences('core_message_migrate_data', false, $USER->id)) { 4708 // Check if there are any legacy messages to migrate. 4709 if (\core_message\helper::legacy_messages_exist($USER->id)) { 4710 \core_message\task\migrate_message_data::queue_task($USER->id); 4711 } else { 4712 set_user_preference('core_message_migrate_data', true, $USER->id); 4713 } 4714 } 4715 4716 if (isguestuser()) { 4717 // No need to continue when user is THE guest. 4718 return $USER; 4719 } 4720 4721 if (CLI_SCRIPT) { 4722 // We can redirect to password change URL only in browser. 4723 return $USER; 4724 } 4725 4726 // Select password change url. 4727 $userauth = get_auth_plugin($USER->auth); 4728 4729 // Check whether the user should be changing password. 4730 if (get_user_preferences('auth_forcepasswordchange', false)) { 4731 if ($userauth->can_change_password()) { 4732 if ($changeurl = $userauth->change_password_url()) { 4733 redirect($changeurl); 4734 } else { 4735 require_once($CFG->dirroot . '/login/lib.php'); 4736 $SESSION->wantsurl = core_login_get_return_url(); 4737 redirect($CFG->wwwroot.'/login/change_password.php'); 4738 } 4739 } else { 4740 print_error('nopasswordchangeforced', 'auth'); 4741 } 4742 } 4743 return $USER; 4744 } 4745 4746 /** 4747 * Check a password hash to see if it was hashed using the legacy hash algorithm (md5). 4748 * 4749 * @param string $password String to check. 4750 * @return boolean True if the $password matches the format of an md5 sum. 4751 */ 4752 function password_is_legacy_hash($password) { 4753 return (bool) preg_match('/^[0-9a-f]{32}$/', $password); 4754 } 4755 4756 /** 4757 * Compare password against hash stored in user object to determine if it is valid. 4758 * 4759 * If necessary it also updates the stored hash to the current format. 4760 * 4761 * @param stdClass $user (Password property may be updated). 4762 * @param string $password Plain text password. 4763 * @return bool True if password is valid. 4764 */ 4765 function validate_internal_user_password($user, $password) { 4766 global $CFG; 4767 4768 if ($user->password === AUTH_PASSWORD_NOT_CACHED) { 4769 // Internal password is not used at all, it can not validate. 4770 return false; 4771 } 4772 4773 // If hash isn't a legacy (md5) hash, validate using the library function. 4774 if (!password_is_legacy_hash($user->password)) { 4775 return password_verify($password, $user->password); 4776 } 4777 4778 // Otherwise we need to check for a legacy (md5) hash instead. If the hash 4779 // is valid we can then update it to the new algorithm. 4780 4781 $sitesalt = isset($CFG->passwordsaltmain) ? $CFG->passwordsaltmain : ''; 4782 $validated = false; 4783 4784 if ($user->password === md5($password.$sitesalt) 4785 or $user->password === md5($password) 4786 or $user->password === md5(addslashes($password).$sitesalt) 4787 or $user->password === md5(addslashes($password))) { 4788 // Note: we are intentionally using the addslashes() here because we 4789 // need to accept old password hashes of passwords with magic quotes. 4790 $validated = true; 4791 4792 } else { 4793 for ($i=1; $i<=20; $i++) { // 20 alternative salts should be enough, right? 4794 $alt = 'passwordsaltalt'.$i; 4795 if (!empty($CFG->$alt)) { 4796 if ($user->password === md5($password.$CFG->$alt) or $user->password === md5(addslashes($password).$CFG->$alt)) { 4797 $validated = true; 4798 break; 4799 } 4800 } 4801 } 4802 } 4803 4804 if ($validated) { 4805 // If the password matches the existing md5 hash, update to the 4806 // current hash algorithm while we have access to the user's password. 4807 update_internal_user_password($user, $password); 4808 } 4809 4810 return $validated; 4811 } 4812 4813 /** 4814 * Calculate hash for a plain text password. 4815 * 4816 * @param string $password Plain text password to be hashed. 4817 * @param bool $fasthash If true, use a low cost factor when generating the hash 4818 * This is much faster to generate but makes the hash 4819 * less secure. It is used when lots of hashes need to 4820 * be generated quickly. 4821 * @return string The hashed password. 4822 * 4823 * @throws moodle_exception If a problem occurs while generating the hash. 4824 */ 4825 function hash_internal_user_password($password, $fasthash = false) { 4826 global $CFG; 4827 4828 // Set the cost factor to 4 for fast hashing, otherwise use default cost. 4829 $options = ($fasthash) ? array('cost' => 4) : array(); 4830 4831 $generatedhash = password_hash($password, PASSWORD_DEFAULT, $options); 4832 4833 if ($generatedhash === false || $generatedhash === null) { 4834 throw new moodle_exception('Failed to generate password hash.'); 4835 } 4836 4837 return $generatedhash; 4838 } 4839 4840 /** 4841 * Update password hash in user object (if necessary). 4842 * 4843 * The password is updated if: 4844 * 1. The password has changed (the hash of $user->password is different 4845 * to the hash of $password). 4846 * 2. The existing hash is using an out-of-date algorithm (or the legacy 4847 * md5 algorithm). 4848 * 4849 * Updating the password will modify the $user object and the database 4850 * record to use the current hashing algorithm. 4851 * It will remove Web Services user tokens too. 4852 * 4853 * @param stdClass $user User object (password property may be updated). 4854 * @param string $password Plain text password. 4855 * @param bool $fasthash If true, use a low cost factor when generating the hash 4856 * This is much faster to generate but makes the hash 4857 * less secure. It is used when lots of hashes need to 4858 * be generated quickly. 4859 * @return bool Always returns true. 4860 */ 4861 function update_internal_user_password($user, $password, $fasthash = false) { 4862 global $CFG, $DB; 4863 4864 // Figure out what the hashed password should be. 4865 if (!isset($user->auth)) { 4866 debugging('User record in update_internal_user_password() must include field auth', 4867 DEBUG_DEVELOPER); 4868 $user->auth = $DB->get_field('user', 'auth', array('id' => $user->id)); 4869 } 4870 $authplugin = get_auth_plugin($user->auth); 4871 if ($authplugin->prevent_local_passwords()) { 4872 $hashedpassword = AUTH_PASSWORD_NOT_CACHED; 4873 } else { 4874 $hashedpassword = hash_internal_user_password($password, $fasthash); 4875 } 4876 4877 $algorithmchanged = false; 4878 4879 if ($hashedpassword === AUTH_PASSWORD_NOT_CACHED) { 4880 // Password is not cached, update it if not set to AUTH_PASSWORD_NOT_CACHED. 4881 $passwordchanged = ($user->password !== $hashedpassword); 4882 4883 } else if (isset($user->password)) { 4884 // If verification fails then it means the password has changed. 4885 $passwordchanged = !password_verify($password, $user->password); 4886 $algorithmchanged = password_needs_rehash($user->password, PASSWORD_DEFAULT); 4887 } else { 4888 // While creating new user, password in unset in $user object, to avoid 4889 // saving it with user_create() 4890 $passwordchanged = true; 4891 } 4892 4893 if ($passwordchanged || $algorithmchanged) { 4894 $DB->set_field('user', 'password', $hashedpassword, array('id' => $user->id)); 4895 $user->password = $hashedpassword; 4896 4897 // Trigger event. 4898 $user = $DB->get_record('user', array('id' => $user->id)); 4899 \core\event\user_password_updated::create_from_user($user)->trigger(); 4900 4901 // Remove WS user tokens. 4902 if (!empty($CFG->passwordchangetokendeletion)) { 4903 require_once($CFG->dirroot.'/webservice/lib.php'); 4904 webservice::delete_user_ws_tokens($user->id); 4905 } 4906 } 4907 4908 return true; 4909 } 4910 4911 /** 4912 * Get a complete user record, which includes all the info in the user record. 4913 * 4914 * Intended for setting as $USER session variable 4915 * 4916 * @param string $field The user field to be checked for a given value. 4917 * @param string $value The value to match for $field. 4918 * @param int $mnethostid 4919 * @param bool $throwexception If true, it will throw an exception when there's no record found or when there are multiple records 4920 * found. Otherwise, it will just return false. 4921 * @return mixed False, or A {@link $USER} object. 4922 */ 4923 function get_complete_user_data($field, $value, $mnethostid = null, $throwexception = false) { 4924 global $CFG, $DB; 4925 4926 if (!$field || !$value) { 4927 return false; 4928 } 4929 4930 // Change the field to lowercase. 4931 $field = core_text::strtolower($field); 4932 4933 // List of case insensitive fields. 4934 $caseinsensitivefields = ['email']; 4935 4936 // Username input is forced to lowercase and should be case sensitive. 4937 if ($field == 'username') { 4938 $value = core_text::strtolower($value); 4939 } 4940 4941 // Build the WHERE clause for an SQL query. 4942 $params = array('fieldval' => $value); 4943 4944 // Do a case-insensitive query, if necessary. These are generally very expensive. The performance can be improved on some DBs 4945 // such as MySQL by pre-filtering users with accent-insensitive subselect. 4946 if (in_array($field, $caseinsensitivefields)) { 4947 $fieldselect = $DB->sql_equal($field, ':fieldval', false); 4948 $idsubselect = $DB->sql_equal($field, ':fieldval2', false, false); 4949 $params['fieldval2'] = $value; 4950 } else { 4951 $fieldselect = "$field = :fieldval"; 4952 $idsubselect = ''; 4953 } 4954 $constraints = "$fieldselect AND deleted <> 1"; 4955 4956 // If we are loading user data based on anything other than id, 4957 // we must also restrict our search based on mnet host. 4958 if ($field != 'id') { 4959 if (empty($mnethostid)) { 4960 // If empty, we restrict to local users. 4961 $mnethostid = $CFG->mnet_localhost_id; 4962 } 4963 } 4964 if (!empty($mnethostid)) { 4965 $params['mnethostid'] = $mnethostid; 4966 $constraints .= " AND mnethostid = :mnethostid"; 4967 } 4968 4969 if ($idsubselect) { 4970 $constraints .= " AND id IN (SELECT id FROM {user} WHERE {$idsubselect})"; 4971 } 4972 4973 // Get all the basic user data. 4974 try { 4975 // Make sure that there's only a single record that matches our query. 4976 // For example, when fetching by email, multiple records might match the query as there's no guarantee that email addresses 4977 // are unique. Therefore we can't reliably tell whether the user profile data that we're fetching is the correct one. 4978 $user = $DB->get_record_select('user', $constraints, $params, '*', MUST_EXIST); 4979 } catch (dml_exception $exception) { 4980 if ($throwexception) { 4981 throw $exception; 4982 } else { 4983 // Return false when no records or multiple records were found. 4984 return false; 4985 } 4986 } 4987 4988 // Get various settings and preferences. 4989 4990 // Preload preference cache. 4991 check_user_preferences_loaded($user); 4992 4993 // Load course enrolment related stuff. 4994 $user->lastcourseaccess = array(); // During last session. 4995 $user->currentcourseaccess = array(); // During current session. 4996 if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid' => $user->id))) { 4997 foreach ($lastaccesses as $lastaccess) { 4998 $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess; 4999 } 5000 } 5001 5002 $sql = "SELECT g.id, g.courseid 5003 FROM {groups} g, {groups_members} gm 5004 WHERE gm.groupid=g.id AND gm.userid=?"; 5005 5006 // This is a special hack to speedup calendar display. 5007 $user->groupmember = array(); 5008 if (!isguestuser($user)) { 5009 if ($groups = $DB->get_records_sql($sql, array($user->id))) { 5010 foreach ($groups as $group) { 5011 if (!array_key_exists($group->courseid, $user->groupmember)) { 5012 $user->groupmember[$group->courseid] = array(); 5013 } 5014 $user->groupmember[$group->courseid][$group->id] = $group->id; 5015 } 5016 } 5017 } 5018 5019 // Add cohort theme. 5020 if (!empty($CFG->allowcohortthemes)) { 5021 require_once($CFG->dirroot . '/cohort/lib.php'); 5022 if ($cohorttheme = cohort_get_user_cohort_theme($user->id)) { 5023 $user->cohorttheme = $cohorttheme; 5024 } 5025 } 5026 5027 // Add the custom profile fields to the user record. 5028 $user->profile = array(); 5029 if (!isguestuser($user)) { 5030 require_once($CFG->dirroot.'/user/profile/lib.php'); 5031 profile_load_custom_fields($user); 5032 } 5033 5034 // Rewrite some variables if necessary. 5035 if (!empty($user->description)) { 5036 // No need to cart all of it around. 5037 $user->description = true; 5038 } 5039 if (isguestuser($user)) { 5040 // Guest language always same as site. 5041 $user->lang = $CFG->lang; 5042 // Name always in current language. 5043 $user->firstname = get_string('guestuser'); 5044 $user->lastname = ' '; 5045 } 5046 5047 return $user; 5048 } 5049 5050 /** 5051 * Validate a password against the configured password policy 5052 * 5053 * @param string $password the password to be checked against the password policy 5054 * @param string $errmsg the error message to display when the password doesn't comply with the policy. 5055 * @param stdClass $user the user object to perform password validation against. Defaults to null if not provided. 5056 * 5057 * @return bool true if the password is valid according to the policy. false otherwise. 5058 */ 5059 function check_password_policy($password, &$errmsg, $user = null) { 5060 global $CFG; 5061 5062 if (!empty($CFG->passwordpolicy)) { 5063 $errmsg = ''; 5064 if (core_text::strlen($password) < $CFG->minpasswordlength) { 5065 $errmsg .= '<div>'. get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength) .'</div>'; 5066 } 5067 if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) { 5068 $errmsg .= '<div>'. get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits) .'</div>'; 5069 } 5070 if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) { 5071 $errmsg .= '<div>'. get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower) .'</div>'; 5072 } 5073 if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) { 5074 $errmsg .= '<div>'. get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper) .'</div>'; 5075 } 5076 if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) { 5077 $errmsg .= '<div>'. get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum) .'</div>'; 5078 } 5079 if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) { 5080 $errmsg .= '<div>'. get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars) .'</div>'; 5081 } 5082 5083 // Fire any additional password policy functions from plugins. 5084 // Plugin functions should output an error message string or empty string for success. 5085 $pluginsfunction = get_plugins_with_function('check_password_policy'); 5086 foreach ($pluginsfunction as $plugintype => $plugins) { 5087 foreach ($plugins as $pluginfunction) { 5088 $pluginerr = $pluginfunction($password, $user); 5089 if ($pluginerr) { 5090 $errmsg .= '<div>'. $pluginerr .'</div>'; 5091 } 5092 } 5093 } 5094 } 5095 5096 if ($errmsg == '') { 5097 return true; 5098 } else { 5099 return false; 5100 } 5101 } 5102 5103 5104 /** 5105 * When logging in, this function is run to set certain preferences for the current SESSION. 5106 */ 5107 function set_login_session_preferences() { 5108 global $SESSION; 5109 5110 $SESSION->justloggedin = true; 5111 5112 unset($SESSION->lang); 5113 unset($SESSION->forcelang); 5114 unset($SESSION->load_navigation_admin); 5115 } 5116 5117 5118 /** 5119 * Delete a course, including all related data from the database, and any associated files. 5120 * 5121 * @param mixed $courseorid The id of the course or course object to delete. 5122 * @param bool $showfeedback Whether to display notifications of each action the function performs. 5123 * @return bool true if all the removals succeeded. false if there were any failures. If this 5124 * method returns false, some of the removals will probably have succeeded, and others 5125 * failed, but you have no way of knowing which. 5126 */ 5127 function delete_course($courseorid, $showfeedback = true) { 5128 global $DB; 5129 5130 if (is_object($courseorid)) { 5131 $courseid = $courseorid->id; 5132 $course = $courseorid; 5133 } else { 5134 $courseid = $courseorid; 5135 if (!$course = $DB->get_record('course', array('id' => $courseid))) { 5136 return false; 5137 } 5138 } 5139 $context = context_course::instance($courseid); 5140 5141 // Frontpage course can not be deleted!! 5142 if ($courseid == SITEID) { 5143 return false; 5144 } 5145 5146 // Allow plugins to use this course before we completely delete it. 5147 if ($pluginsfunction = get_plugins_with_function('pre_course_delete')) { 5148 foreach ($pluginsfunction as $plugintype => $plugins) { 5149 foreach ($plugins as $pluginfunction) { 5150 $pluginfunction($course); 5151 } 5152 } 5153 } 5154 5155 // Tell the search manager we are about to delete a course. This prevents us sending updates 5156 // for each individual context being deleted. 5157 \core_search\manager::course_deleting_start($courseid); 5158 5159 $handler = core_course\customfield\course_handler::create(); 5160 $handler->delete_instance($courseid); 5161 5162 // Make the course completely empty. 5163 remove_course_contents($courseid, $showfeedback); 5164 5165 // Delete the course and related context instance. 5166 context_helper::delete_instance(CONTEXT_COURSE, $courseid); 5167 5168 $DB->delete_records("course", array("id" => $courseid)); 5169 $DB->delete_records("course_format_options", array("courseid" => $courseid)); 5170 5171 // Reset all course related caches here. 5172 if (class_exists('format_base', false)) { 5173 format_base::reset_course_cache($courseid); 5174 } 5175 5176 // Tell search that we have deleted the course so it can delete course data from the index. 5177 \core_search\manager::course_deleting_finish($courseid); 5178 5179 // Trigger a course deleted event. 5180 $event = \core\event\course_deleted::create(array( 5181 'objectid' => $course->id, 5182 'context' => $context, 5183 'other' => array( 5184 'shortname' => $course->shortname, 5185 'fullname' => $course->fullname, 5186 'idnumber' => $course->idnumber 5187 ) 5188 )); 5189 $event->add_record_snapshot('course', $course); 5190 $event->trigger(); 5191 5192 return true; 5193 } 5194 5195 /** 5196 * Clear a course out completely, deleting all content but don't delete the course itself. 5197 * 5198 * This function does not verify any permissions. 5199 * 5200 * Please note this function also deletes all user enrolments, 5201 * enrolment instances and role assignments by default. 5202 * 5203 * $options: 5204 * - 'keep_roles_and_enrolments' - false by default 5205 * - 'keep_groups_and_groupings' - false by default 5206 * 5207 * @param int $courseid The id of the course that is being deleted 5208 * @param bool $showfeedback Whether to display notifications of each action the function performs. 5209 * @param array $options extra options 5210 * @return bool true if all the removals succeeded. false if there were any failures. If this 5211 * method returns false, some of the removals will probably have succeeded, and others 5212 * failed, but you have no way of knowing which. 5213 */ 5214 function remove_course_contents($courseid, $showfeedback = true, array $options = null) { 5215 global $CFG, $DB, $OUTPUT; 5216 5217 require_once($CFG->libdir.'/badgeslib.php'); 5218 require_once($CFG->libdir.'/completionlib.php'); 5219 require_once($CFG->libdir.'/questionlib.php'); 5220 require_once($CFG->libdir.'/gradelib.php'); 5221 require_once($CFG->dirroot.'/group/lib.php'); 5222 require_once($CFG->dirroot.'/comment/lib.php'); 5223 require_once($CFG->dirroot.'/rating/lib.php'); 5224 require_once($CFG->dirroot.'/notes/lib.php'); 5225 5226 // Handle course badges. 5227 badges_handle_course_deletion($courseid); 5228 5229 // NOTE: these concatenated strings are suboptimal, but it is just extra info... 5230 $strdeleted = get_string('deleted').' - '; 5231 5232 // Some crazy wishlist of stuff we should skip during purging of course content. 5233 $options = (array)$options; 5234 5235 $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); 5236 $coursecontext = context_course::instance($courseid); 5237 $fs = get_file_storage(); 5238 5239 // Delete course completion information, this has to be done before grades and enrols. 5240 $cc = new completion_info($course); 5241 $cc->clear_criteria(); 5242 if ($showfeedback) { 5243 echo $OUTPUT->notification($strdeleted.get_string('completion', 'completion'), 'notifysuccess'); 5244 } 5245 5246 // Remove all data from gradebook - this needs to be done before course modules 5247 // because while deleting this information, the system may need to reference 5248 // the course modules that own the grades. 5249 remove_course_grades($courseid, $showfeedback); 5250 remove_grade_letters($coursecontext, $showfeedback); 5251 5252 // Delete course blocks in any all child contexts, 5253 // they may depend on modules so delete them first. 5254 $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2. 5255 foreach ($childcontexts as $childcontext) { 5256 blocks_delete_all_for_context($childcontext->id); 5257 } 5258 unset($childcontexts); 5259 blocks_delete_all_for_context($coursecontext->id); 5260 if ($showfeedback) { 5261 echo $OUTPUT->notification($strdeleted.get_string('type_block_plural', 'plugin'), 'notifysuccess'); 5262 } 5263 5264 $DB->set_field('course_modules', 'deletioninprogress', '1', ['course' => $courseid]); 5265 rebuild_course_cache($courseid, true); 5266 5267 // Get the list of all modules that are properly installed. 5268 $allmodules = $DB->get_records_menu('modules', array(), '', 'name, id'); 5269 5270 // Delete every instance of every module, 5271 // this has to be done before deleting of course level stuff. 5272 $locations = core_component::get_plugin_list('mod'); 5273 foreach ($locations as $modname => $moddir) { 5274 if ($modname === 'NEWMODULE') { 5275 continue; 5276 } 5277 if (array_key_exists($modname, $allmodules)) { 5278 $sql = "SELECT cm.*, m.id AS modinstance, m.name, '$modname' AS modname 5279 FROM {".$modname."} m 5280 LEFT JOIN {course_modules} cm ON cm.instance = m.id AND cm.module = :moduleid 5281 WHERE m.course = :courseid"; 5282 $instances = $DB->get_records_sql($sql, array('courseid' => $course->id, 5283 'modulename' => $modname, 'moduleid' => $allmodules[$modname])); 5284 5285 include_once("$moddir/lib.php"); // Shows php warning only if plugin defective. 5286 $moddelete = $modname .'_delete_instance'; // Delete everything connected to an instance. 5287 5288 if ($instances) { 5289 foreach ($instances as $cm) { 5290 if ($cm->id) { 5291 // Delete activity context questions and question categories. 5292 question_delete_activity($cm); 5293 // Notify the competency subsystem. 5294 \core_competency\api::hook_course_module_deleted($cm); 5295 } 5296 if (function_exists($moddelete)) { 5297 // This purges all module data in related tables, extra user prefs, settings, etc. 5298 $moddelete($cm->modinstance); 5299 } else { 5300 // NOTE: we should not allow installation of modules with missing delete support! 5301 debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!"); 5302 $DB->delete_records($modname, array('id' => $cm->modinstance)); 5303 } 5304 5305 if ($cm->id) { 5306 // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition. 5307 context_helper::delete_instance(CONTEXT_MODULE, $cm->id); 5308 $DB->delete_records('course_modules_completion', ['coursemoduleid' => $cm->id]); 5309 $DB->delete_records('course_modules', array('id' => $cm->id)); 5310 rebuild_course_cache($cm->course, true); 5311 } 5312 } 5313 } 5314 if ($instances and $showfeedback) { 5315 echo $OUTPUT->notification($strdeleted.get_string('pluginname', $modname), 'notifysuccess'); 5316 } 5317 } else { 5318 // Ooops, this module is not properly installed, force-delete it in the next block. 5319 } 5320 } 5321 5322 // We have tried to delete everything the nice way - now let's force-delete any remaining module data. 5323 5324 // Delete completion defaults. 5325 $DB->delete_records("course_completion_defaults", array("course" => $courseid)); 5326 5327 // Remove all data from availability and completion tables that is associated 5328 // with course-modules belonging to this course. Note this is done even if the 5329 // features are not enabled now, in case they were enabled previously. 5330 $DB->delete_records_subquery('course_modules_completion', 'coursemoduleid', 'id', 5331 'SELECT id from {course_modules} WHERE course = ?', [$courseid]); 5332 5333 // Remove course-module data that has not been removed in modules' _delete_instance callbacks. 5334 $cms = $DB->get_records('course_modules', array('course' => $course->id)); 5335 $allmodulesbyid = array_flip($allmodules); 5336 foreach ($cms as $cm) { 5337 if (array_key_exists($cm->module, $allmodulesbyid)) { 5338 try { 5339 $DB->delete_records($allmodulesbyid[$cm->module], array('id' => $cm->instance)); 5340 } catch (Exception $e) { 5341 // Ignore weird or missing table problems. 5342 } 5343 } 5344 context_helper::delete_instance(CONTEXT_MODULE, $cm->id); 5345 $DB->delete_records('course_modules', array('id' => $cm->id)); 5346 rebuild_course_cache($cm->course, true); 5347 } 5348 5349 if ($showfeedback) { 5350 echo $OUTPUT->notification($strdeleted.get_string('type_mod_plural', 'plugin'), 'notifysuccess'); 5351 } 5352 5353 // Delete questions and question categories. 5354 question_delete_course($course); 5355 if ($showfeedback) { 5356 echo $OUTPUT->notification($strdeleted.get_string('questions', 'question'), 'notifysuccess'); 5357 } 5358 5359 // Delete content bank contents. 5360 $cb = new \core_contentbank\contentbank(); 5361 $cbdeleted = $cb->delete_contents($coursecontext); 5362 if ($showfeedback && $cbdeleted) { 5363 echo $OUTPUT->notification($strdeleted.get_string('contentbank', 'contentbank'), 'notifysuccess'); 5364 } 5365 5366 // Make sure there are no subcontexts left - all valid blocks and modules should be already gone. 5367 $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2. 5368 foreach ($childcontexts as $childcontext) { 5369 $childcontext->delete(); 5370 } 5371 unset($childcontexts); 5372 5373 // Remove roles and enrolments by default. 5374 if (empty($options['keep_roles_and_enrolments'])) { 5375 // This hack is used in restore when deleting contents of existing course. 5376 // During restore, we should remove only enrolment related data that the user performing the restore has a 5377 // permission to remove. 5378 $userid = $options['userid'] ?? null; 5379 enrol_course_delete($course, $userid); 5380 role_unassign_all(array('contextid' => $coursecontext->id, 'component' => ''), true); 5381 if ($showfeedback) { 5382 echo $OUTPUT->notification($strdeleted.get_string('type_enrol_plural', 'plugin'), 'notifysuccess'); 5383 } 5384 } 5385 5386 // Delete any groups, removing members and grouping/course links first. 5387 if (empty($options['keep_groups_and_groupings'])) { 5388 groups_delete_groupings($course->id, $showfeedback); 5389 groups_delete_groups($course->id, $showfeedback); 5390 } 5391 5392 // Filters be gone! 5393 filter_delete_all_for_context($coursecontext->id); 5394 5395 // Notes, you shall not pass! 5396 note_delete_all($course->id); 5397 5398 // Die comments! 5399 comment::delete_comments($coursecontext->id); 5400 5401 // Ratings are history too. 5402 $delopt = new stdclass(); 5403 $delopt->contextid = $coursecontext->id; 5404 $rm = new rating_manager(); 5405 $rm->delete_ratings($delopt); 5406 5407 // Delete course tags. 5408 core_tag_tag::remove_all_item_tags('core', 'course', $course->id); 5409 5410 // Notify the competency subsystem. 5411 \core_competency\api::hook_course_deleted($course); 5412 5413 // Delete calendar events. 5414 $DB->delete_records('event', array('courseid' => $course->id)); 5415 $fs->delete_area_files($coursecontext->id, 'calendar'); 5416 5417 // Delete all related records in other core tables that may have a courseid 5418 // This array stores the tables that need to be cleared, as 5419 // table_name => column_name that contains the course id. 5420 $tablestoclear = array( 5421 'backup_courses' => 'courseid', // Scheduled backup stuff. 5422 'user_lastaccess' => 'courseid', // User access info. 5423 ); 5424 foreach ($tablestoclear as $table => $col) { 5425 $DB->delete_records($table, array($col => $course->id)); 5426 } 5427 5428 // Delete all course backup files. 5429 $fs->delete_area_files($coursecontext->id, 'backup'); 5430 5431 // Cleanup course record - remove links to deleted stuff. 5432 $oldcourse = new stdClass(); 5433 $oldcourse->id = $course->id; 5434 $oldcourse->summary = ''; 5435 $oldcourse->cacherev = 0; 5436 $oldcourse->legacyfiles = 0; 5437 if (!empty($options['keep_groups_and_groupings'])) { 5438 $oldcourse->defaultgroupingid = 0; 5439 } 5440 $DB->update_record('course', $oldcourse); 5441 5442 // Delete course sections. 5443 $DB->delete_records('course_sections', array('course' => $course->id)); 5444 5445 // Delete legacy, section and any other course files. 5446 $fs->delete_area_files($coursecontext->id, 'course'); // Files from summary and section. 5447 5448 // Delete all remaining stuff linked to context such as files, comments, ratings, etc. 5449 if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) { 5450 // Easy, do not delete the context itself... 5451 $coursecontext->delete_content(); 5452 } else { 5453 // Hack alert!!!! 5454 // We can not drop all context stuff because it would bork enrolments and roles, 5455 // there might be also files used by enrol plugins... 5456 } 5457 5458 // Delete legacy files - just in case some files are still left there after conversion to new file api, 5459 // also some non-standard unsupported plugins may try to store something there. 5460 fulldelete($CFG->dataroot.'/'.$course->id); 5461 5462 // Delete from cache to reduce the cache size especially makes sense in case of bulk course deletion. 5463 $cachemodinfo = cache::make('core', 'coursemodinfo'); 5464 $cachemodinfo->delete($courseid); 5465 5466 // Trigger a course content deleted event. 5467 $event = \core\event\course_content_deleted::create(array( 5468 'objectid' => $course->id, 5469 'context' => $coursecontext, 5470 'other' => array('shortname' => $course->shortname, 5471 'fullname' => $course->fullname, 5472 'options' => $options) // Passing this for legacy reasons. 5473 )); 5474 $event->add_record_snapshot('course', $course); 5475 $event->trigger(); 5476 5477 return true; 5478 } 5479 5480 /** 5481 * Change dates in module - used from course reset. 5482 * 5483 * @param string $modname forum, assignment, etc 5484 * @param array $fields array of date fields from mod table 5485 * @param int $timeshift time difference 5486 * @param int $courseid 5487 * @param int $modid (Optional) passed if specific mod instance in course needs to be updated. 5488 * @return bool success 5489 */ 5490 function shift_course_mod_dates($modname, $fields, $timeshift, $courseid, $modid = 0) { 5491 global $CFG, $DB; 5492 include_once($CFG->dirroot.'/mod/'.$modname.'/lib.php'); 5493 5494 $return = true; 5495 $params = array($timeshift, $courseid); 5496 foreach ($fields as $field) { 5497 $updatesql = "UPDATE {".$modname."} 5498 SET $field = $field + ? 5499 WHERE course=? AND $field<>0"; 5500 if ($modid) { 5501 $updatesql .= ' AND id=?'; 5502 $params[] = $modid; 5503 } 5504 $return = $DB->execute($updatesql, $params) && $return; 5505 } 5506 5507 return $return; 5508 } 5509 5510 /** 5511 * This function will empty a course of user data. 5512 * It will retain the activities and the structure of the course. 5513 * 5514 * @param object $data an object containing all the settings including courseid (without magic quotes) 5515 * @return array status array of array component, item, error 5516 */ 5517 function reset_course_userdata($data) { 5518 global $CFG, $DB; 5519 require_once($CFG->libdir.'/gradelib.php'); 5520 require_once($CFG->libdir.'/completionlib.php'); 5521 require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php'); 5522 require_once($CFG->dirroot.'/group/lib.php'); 5523 5524 $data->courseid = $data->id; 5525 $context = context_course::instance($data->courseid); 5526 5527 $eventparams = array( 5528 'context' => $context, 5529 'courseid' => $data->id, 5530 'other' => array( 5531 'reset_options' => (array) $data 5532 ) 5533 ); 5534 $event = \core\event\course_reset_started::create($eventparams); 5535 $event->trigger(); 5536 5537 // Calculate the time shift of dates. 5538 if (!empty($data->reset_start_date)) { 5539 // Time part of course startdate should be zero. 5540 $data->timeshift = $data->reset_start_date - usergetmidnight($data->reset_start_date_old); 5541 } else { 5542 $data->timeshift = 0; 5543 } 5544 5545 // Result array: component, item, error. 5546 $status = array(); 5547 5548 // Start the resetting. 5549 $componentstr = get_string('general'); 5550 5551 // Move the course start time. 5552 if (!empty($data->reset_start_date) and $data->timeshift) { 5553 // Change course start data. 5554 $DB->set_field('course', 'startdate', $data->reset_start_date, array('id' => $data->courseid)); 5555 // Update all course and group events - do not move activity events. 5556 $updatesql = "UPDATE {event} 5557 SET timestart = timestart + ? 5558 WHERE courseid=? AND instance=0"; 5559 $DB->execute($updatesql, array($data->timeshift, $data->courseid)); 5560 5561 // Update any date activity restrictions. 5562 if ($CFG->enableavailability) { 5563 \availability_date\condition::update_all_dates($data->courseid, $data->timeshift); 5564 } 5565 5566 // Update completion expected dates. 5567 if ($CFG->enablecompletion) { 5568 $modinfo = get_fast_modinfo($data->courseid); 5569 $changed = false; 5570 foreach ($modinfo->get_cms() as $cm) { 5571 if ($cm->completion && !empty($cm->completionexpected)) { 5572 $DB->set_field('course_modules', 'completionexpected', $cm->completionexpected + $data->timeshift, 5573 array('id' => $cm->id)); 5574 $changed = true; 5575 } 5576 } 5577 5578 // Clear course cache if changes made. 5579 if ($changed) { 5580 rebuild_course_cache($data->courseid, true); 5581 } 5582 5583 // Update course date completion criteria. 5584 \completion_criteria_date::update_date($data->courseid, $data->timeshift); 5585 } 5586 5587 $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false); 5588 } 5589 5590 if (!empty($data->reset_end_date)) { 5591 // If the user set a end date value respect it. 5592 $DB->set_field('course', 'enddate', $data->reset_end_date, array('id' => $data->courseid)); 5593 } else if ($data->timeshift > 0 && $data->reset_end_date_old) { 5594 // If there is a time shift apply it to the end date as well. 5595 $enddate = $data->reset_end_date_old + $data->timeshift; 5596 $DB->set_field('course', 'enddate', $enddate, array('id' => $data->courseid)); 5597 } 5598 5599 if (!empty($data->reset_events)) { 5600 $DB->delete_records('event', array('courseid' => $data->courseid)); 5601 $status[] = array('component' => $componentstr, 'item' => get_string('deleteevents', 'calendar'), 'error' => false); 5602 } 5603 5604 if (!empty($data->reset_notes)) { 5605 require_once($CFG->dirroot.'/notes/lib.php'); 5606 note_delete_all($data->courseid); 5607 $status[] = array('component' => $componentstr, 'item' => get_string('deletenotes', 'notes'), 'error' => false); 5608 } 5609 5610 if (!empty($data->delete_blog_associations)) { 5611 require_once($CFG->dirroot.'/blog/lib.php'); 5612 blog_remove_associations_for_course($data->courseid); 5613 $status[] = array('component' => $componentstr, 'item' => get_string('deleteblogassociations', 'blog'), 'error' => false); 5614 } 5615 5616 if (!empty($data->reset_completion)) { 5617 // Delete course and activity completion information. 5618 $course = $DB->get_record('course', array('id' => $data->courseid)); 5619 $cc = new completion_info($course); 5620 $cc->delete_all_completion_data(); 5621 $status[] = array('component' => $componentstr, 5622 'item' => get_string('deletecompletiondata', 'completion'), 'error' => false); 5623 } 5624 5625 if (!empty($data->reset_competency_ratings)) { 5626 \core_competency\api::hook_course_reset_competency_ratings($data->courseid); 5627 $status[] = array('component' => $componentstr, 5628 'item' => get_string('deletecompetencyratings', 'core_competency'), 'error' => false); 5629 } 5630 5631 $componentstr = get_string('roles'); 5632 5633 if (!empty($data->reset_roles_overrides)) { 5634 $children = $context->get_child_contexts(); 5635 foreach ($children as $child) { 5636 $child->delete_capabilities(); 5637 } 5638 $context->delete_capabilities(); 5639 $status[] = array('component' => $componentstr, 'item' => get_string('deletecourseoverrides', 'role'), 'error' => false); 5640 } 5641 5642 if (!empty($data->reset_roles_local)) { 5643 $children = $context->get_child_contexts(); 5644 foreach ($children as $child) { 5645 role_unassign_all(array('contextid' => $child->id)); 5646 } 5647 $status[] = array('component' => $componentstr, 'item' => get_string('deletelocalroles', 'role'), 'error' => false); 5648 } 5649 5650 // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc. 5651 $data->unenrolled = array(); 5652 if (!empty($data->unenrol_users)) { 5653 $plugins = enrol_get_plugins(true); 5654 $instances = enrol_get_instances($data->courseid, true); 5655 foreach ($instances as $key => $instance) { 5656 if (!isset($plugins[$instance->enrol])) { 5657 unset($instances[$key]); 5658 continue; 5659 } 5660 } 5661 5662 $usersroles = enrol_get_course_users_roles($data->courseid); 5663 foreach ($data->unenrol_users as $withroleid) { 5664 if ($withroleid) { 5665 $sql = "SELECT ue.* 5666 FROM {user_enrolments} ue 5667 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid) 5668 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid) 5669 JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)"; 5670 $params = array('courseid' => $data->courseid, 'roleid' => $withroleid, 'courselevel' => CONTEXT_COURSE); 5671 5672 } else { 5673 // Without any role assigned at course context. 5674 $sql = "SELECT ue.* 5675 FROM {user_enrolments} ue 5676 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid) 5677 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid) 5678 LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid) 5679 WHERE ra.id IS null"; 5680 $params = array('courseid' => $data->courseid, 'courselevel' => CONTEXT_COURSE); 5681 } 5682 5683 $rs = $DB->get_recordset_sql($sql, $params); 5684 foreach ($rs as $ue) { 5685 if (!isset($instances[$ue->enrolid])) { 5686 continue; 5687 } 5688 $instance = $instances[$ue->enrolid]; 5689 $plugin = $plugins[$instance->enrol]; 5690 if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) { 5691 continue; 5692 } 5693 5694 if ($withroleid && count($usersroles[$ue->userid]) > 1) { 5695 // If we don't remove all roles and user has more than one role, just remove this role. 5696 role_unassign($withroleid, $ue->userid, $context->id); 5697 5698 unset($usersroles[$ue->userid][$withroleid]); 5699 } else { 5700 // If we remove all roles or user has only one role, unenrol user from course. 5701 $plugin->unenrol_user($instance, $ue->userid); 5702 } 5703 $data->unenrolled[$ue->userid] = $ue->userid; 5704 } 5705 $rs->close(); 5706 } 5707 } 5708 if (!empty($data->unenrolled)) { 5709 $status[] = array( 5710 'component' => $componentstr, 5711 'item' => get_string('unenrol', 'enrol').' ('.count($data->unenrolled).')', 5712 'error' => false 5713 ); 5714 } 5715 5716 $componentstr = get_string('groups'); 5717 5718 // Remove all group members. 5719 if (!empty($data->reset_groups_members)) { 5720 groups_delete_group_members($data->courseid); 5721 $status[] = array('component' => $componentstr, 'item' => get_string('removegroupsmembers', 'group'), 'error' => false); 5722 } 5723 5724 // Remove all groups. 5725 if (!empty($data->reset_groups_remove)) { 5726 groups_delete_groups($data->courseid, false); 5727 $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroups', 'group'), 'error' => false); 5728 } 5729 5730 // Remove all grouping members. 5731 if (!empty($data->reset_groupings_members)) { 5732 groups_delete_groupings_groups($data->courseid, false); 5733 $status[] = array('component' => $componentstr, 'item' => get_string('removegroupingsmembers', 'group'), 'error' => false); 5734 } 5735 5736 // Remove all groupings. 5737 if (!empty($data->reset_groupings_remove)) { 5738 groups_delete_groupings($data->courseid, false); 5739 $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroupings', 'group'), 'error' => false); 5740 } 5741 5742 // Look in every instance of every module for data to delete. 5743 $unsupportedmods = array(); 5744 if ($allmods = $DB->get_records('modules') ) { 5745 foreach ($allmods as $mod) { 5746 $modname = $mod->name; 5747 $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php'; 5748 $moddeleteuserdata = $modname.'_reset_userdata'; // Function to delete user data. 5749 if (file_exists($modfile)) { 5750 if (!$DB->count_records($modname, array('course' => $data->courseid))) { 5751 continue; // Skip mods with no instances. 5752 } 5753 include_once($modfile); 5754 if (function_exists($moddeleteuserdata)) { 5755 $modstatus = $moddeleteuserdata($data); 5756 if (is_array($modstatus)) { 5757 $status = array_merge($status, $modstatus); 5758 } else { 5759 debugging('Module '.$modname.' returned incorrect staus - must be an array!'); 5760 } 5761 } else { 5762 $unsupportedmods[] = $mod; 5763 } 5764 } else { 5765 debugging('Missing lib.php in '.$modname.' module!'); 5766 } 5767 // Update calendar events for all modules. 5768 course_module_bulk_update_calendar_events($modname, $data->courseid); 5769 } 5770 } 5771 5772 // Mention unsupported mods. 5773 if (!empty($unsupportedmods)) { 5774 foreach ($unsupportedmods as $mod) { 5775 $status[] = array( 5776 'component' => get_string('modulenameplural', $mod->name), 5777 'item' => '', 5778 'error' => get_string('resetnotimplemented') 5779 ); 5780 } 5781 } 5782 5783 $componentstr = get_string('gradebook', 'grades'); 5784 // Reset gradebook,. 5785 if (!empty($data->reset_gradebook_items)) { 5786 remove_course_grades($data->courseid, false); 5787 grade_grab_course_grades($data->courseid); 5788 grade_regrade_final_grades($data->courseid); 5789 $status[] = array('component' => $componentstr, 'item' => get_string('removeallcourseitems', 'grades'), 'error' => false); 5790 5791 } else if (!empty($data->reset_gradebook_grades)) { 5792 grade_course_reset($data->courseid); 5793 $status[] = array('component' => $componentstr, 'item' => get_string('removeallcoursegrades', 'grades'), 'error' => false); 5794 } 5795 // Reset comments. 5796 if (!empty($data->reset_comments)) { 5797 require_once($CFG->dirroot.'/comment/lib.php'); 5798 comment::reset_course_page_comments($context); 5799 } 5800 5801 $event = \core\event\course_reset_ended::create($eventparams); 5802 $event->trigger(); 5803 5804 return $status; 5805 } 5806 5807 /** 5808 * Generate an email processing address. 5809 * 5810 * @param int $modid 5811 * @param string $modargs 5812 * @return string Returns email processing address 5813 */ 5814 function generate_email_processing_address($modid, $modargs) { 5815 global $CFG; 5816 5817 $header = $CFG->mailprefix . substr(base64_encode(pack('C', $modid)), 0, 2).$modargs; 5818 return $header . substr(md5($header.get_site_identifier()), 0, 16).'@'.$CFG->maildomain; 5819 } 5820 5821 /** 5822 * ? 5823 * 5824 * @todo Finish documenting this function 5825 * 5826 * @param string $modargs 5827 * @param string $body Currently unused 5828 */ 5829 function moodle_process_email($modargs, $body) { 5830 global $DB; 5831 5832 // The first char should be an unencoded letter. We'll take this as an action. 5833 switch ($modargs[0]) { 5834 case 'B': { // Bounce. 5835 list(, $userid) = unpack('V', base64_decode(substr($modargs, 1, 8))); 5836 if ($user = $DB->get_record("user", array('id' => $userid), "id,email")) { 5837 // Check the half md5 of their email. 5838 $md5check = substr(md5($user->email), 0, 16); 5839 if ($md5check == substr($modargs, -16)) { 5840 set_bounce_count($user); 5841 } 5842 // Else maybe they've already changed it? 5843 } 5844 } 5845 break; 5846 // Maybe more later? 5847 } 5848 } 5849 5850 // CORRESPONDENCE. 5851 5852 /** 5853 * Get mailer instance, enable buffering, flush buffer or disable buffering. 5854 * 5855 * @param string $action 'get', 'buffer', 'close' or 'flush' 5856 * @return moodle_phpmailer|null mailer instance if 'get' used or nothing 5857 */ 5858 function get_mailer($action='get') { 5859 global $CFG; 5860 5861 /** @var moodle_phpmailer $mailer */ 5862 static $mailer = null; 5863 static $counter = 0; 5864 5865 if (!isset($CFG->smtpmaxbulk)) { 5866 $CFG->smtpmaxbulk = 1; 5867 } 5868 5869 if ($action == 'get') { 5870 $prevkeepalive = false; 5871 5872 if (isset($mailer) and $mailer->Mailer == 'smtp') { 5873 if ($counter < $CFG->smtpmaxbulk and !$mailer->isError()) { 5874 $counter++; 5875 // Reset the mailer. 5876 $mailer->Priority = 3; 5877 $mailer->CharSet = 'UTF-8'; // Our default. 5878 $mailer->ContentType = "text/plain"; 5879 $mailer->Encoding = "8bit"; 5880 $mailer->From = "root@localhost"; 5881 $mailer->FromName = "Root User"; 5882 $mailer->Sender = ""; 5883 $mailer->Subject = ""; 5884 $mailer->Body = ""; 5885 $mailer->AltBody = ""; 5886 $mailer->ConfirmReadingTo = ""; 5887 5888 $mailer->clearAllRecipients(); 5889 $mailer->clearReplyTos(); 5890 $mailer->clearAttachments(); 5891 $mailer->clearCustomHeaders(); 5892 return $mailer; 5893 } 5894 5895 $prevkeepalive = $mailer->SMTPKeepAlive; 5896 get_mailer('flush'); 5897 } 5898 5899 require_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php'); 5900 $mailer = new moodle_phpmailer(); 5901 5902 $counter = 1; 5903 5904 if ($CFG->smtphosts == 'qmail') { 5905 // Use Qmail system. 5906 $mailer->isQmail(); 5907 5908 } else if (empty($CFG->smtphosts)) { 5909 // Use PHP mail() = sendmail. 5910 $mailer->isMail(); 5911 5912 } else { 5913 // Use SMTP directly. 5914 $mailer->isSMTP(); 5915 if (!empty($CFG->debugsmtp) && (!empty($CFG->debugdeveloper))) { 5916 $mailer->SMTPDebug = 3; 5917 } 5918 // Specify main and backup servers. 5919 $mailer->Host = $CFG->smtphosts; 5920 // Specify secure connection protocol. 5921 $mailer->SMTPSecure = $CFG->smtpsecure; 5922 // Use previous keepalive. 5923 $mailer->SMTPKeepAlive = $prevkeepalive; 5924 5925 if ($CFG->smtpuser) { 5926 // Use SMTP authentication. 5927 $mailer->SMTPAuth = true; 5928 $mailer->Username = $CFG->smtpuser; 5929 $mailer->Password = $CFG->smtppass; 5930 } 5931 } 5932 5933 return $mailer; 5934 } 5935 5936 $nothing = null; 5937 5938 // Keep smtp session open after sending. 5939 if ($action == 'buffer') { 5940 if (!empty($CFG->smtpmaxbulk)) { 5941 get_mailer('flush'); 5942 $m = get_mailer(); 5943 if ($m->Mailer == 'smtp') { 5944 $m->SMTPKeepAlive = true; 5945 } 5946 } 5947 return $nothing; 5948 } 5949 5950 // Close smtp session, but continue buffering. 5951 if ($action == 'flush') { 5952 if (isset($mailer) and $mailer->Mailer == 'smtp') { 5953 if (!empty($mailer->SMTPDebug)) { 5954 echo '<pre>'."\n"; 5955 } 5956 $mailer->SmtpClose(); 5957 if (!empty($mailer->SMTPDebug)) { 5958 echo '</pre>'; 5959 } 5960 } 5961 return $nothing; 5962 } 5963 5964 // Close smtp session, do not buffer anymore. 5965 if ($action == 'close') { 5966 if (isset($mailer) and $mailer->Mailer == 'smtp') { 5967 get_mailer('flush'); 5968 $mailer->SMTPKeepAlive = false; 5969 } 5970 $mailer = null; // Better force new instance. 5971 return $nothing; 5972 } 5973 } 5974 5975 /** 5976 * A helper function to test for email diversion 5977 * 5978 * @param string $email 5979 * @return bool Returns true if the email should be diverted 5980 */ 5981 function email_should_be_diverted($email) { 5982 global $CFG; 5983 5984 if (empty($CFG->divertallemailsto)) { 5985 return false; 5986 } 5987 5988 if (empty($CFG->divertallemailsexcept)) { 5989 return true; 5990 } 5991 5992 $patterns = array_map('trim', explode(',', $CFG->divertallemailsexcept)); 5993 foreach ($patterns as $pattern) { 5994 if (preg_match("/$pattern/", $email)) { 5995 return false; 5996 } 5997 } 5998 5999 return true; 6000 } 6001 6002 /** 6003 * Generate a unique email Message-ID using the moodle domain and install path 6004 * 6005 * @param string $localpart An optional unique message id prefix. 6006 * @return string The formatted ID ready for appending to the email headers. 6007 */ 6008 function generate_email_messageid($localpart = null) { 6009 global $CFG; 6010 6011 $urlinfo = parse_url($CFG->wwwroot); 6012 $base = '@' . $urlinfo['host']; 6013 6014 // If multiple moodles are on the same domain we want to tell them 6015 // apart so we add the install path to the local part. This means 6016 // that the id local part should never contain a / character so 6017 // we can correctly parse the id to reassemble the wwwroot. 6018 if (isset($urlinfo['path'])) { 6019 $base = $urlinfo['path'] . $base; 6020 } 6021 6022 if (empty($localpart)) { 6023 $localpart = uniqid('', true); 6024 } 6025 6026 // Because we may have an option /installpath suffix to the local part 6027 // of the id we need to escape any / chars which are in the $localpart. 6028 $localpart = str_replace('/', '%2F', $localpart); 6029 6030 return '<' . $localpart . $base . '>'; 6031 } 6032 6033 /** 6034 * Send an email to a specified user 6035 * 6036 * @param stdClass $user A {@link $USER} object 6037 * @param stdClass $from A {@link $USER} object 6038 * @param string $subject plain text subject line of the email 6039 * @param string $messagetext plain text version of the message 6040 * @param string $messagehtml complete html version of the message (optional) 6041 * @param string $attachment a file on the filesystem, either relative to $CFG->dataroot or a full path to a file in one of 6042 * the following directories: $CFG->cachedir, $CFG->dataroot, $CFG->dirroot, $CFG->localcachedir, $CFG->tempdir 6043 * @param string $attachname the name of the file (extension indicates MIME) 6044 * @param bool $usetrueaddress determines whether $from email address should 6045 * be sent out. Will be overruled by user profile setting for maildisplay 6046 * @param string $replyto Email address to reply to 6047 * @param string $replytoname Name of reply to recipient 6048 * @param int $wordwrapwidth custom word wrap width, default 79 6049 * @return bool Returns true if mail was sent OK and false if there was an error. 6050 */ 6051 function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '', $attachment = '', $attachname = '', 6052 $usetrueaddress = true, $replyto = '', $replytoname = '', $wordwrapwidth = 79) { 6053 6054 global $CFG, $PAGE, $SITE; 6055 6056 if (empty($user) or empty($user->id)) { 6057 debugging('Can not send email to null user', DEBUG_DEVELOPER); 6058 return false; 6059 } 6060 6061 if (empty($user->email)) { 6062 debugging('Can not send email to user without email: '.$user->id, DEBUG_DEVELOPER); 6063 return false; 6064 } 6065 6066 if (!empty($user->deleted)) { 6067 debugging('Can not send email to deleted user: '.$user->id, DEBUG_DEVELOPER); 6068 return false; 6069 } 6070 6071 if (defined('BEHAT_SITE_RUNNING')) { 6072 // Fake email sending in behat. 6073 return true; 6074 } 6075 6076 if (!empty($CFG->noemailever)) { 6077 // Hidden setting for development sites, set in config.php if needed. 6078 debugging('Not sending email due to $CFG->noemailever config setting', DEBUG_NORMAL); 6079 return true; 6080 } 6081 6082 if (email_should_be_diverted($user->email)) { 6083 $subject = "[DIVERTED {$user->email}] $subject"; 6084 $user = clone($user); 6085 $user->email = $CFG->divertallemailsto; 6086 } 6087 6088 // Skip mail to suspended users. 6089 if ((isset($user->auth) && $user->auth=='nologin') or (isset($user->suspended) && $user->suspended)) { 6090 return true; 6091 } 6092 6093 if (!validate_email($user->email)) { 6094 // We can not send emails to invalid addresses - it might create security issue or confuse the mailer. 6095 debugging("email_to_user: User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending."); 6096 return false; 6097 } 6098 6099 if (over_bounce_threshold($user)) { 6100 debugging("email_to_user: User $user->id (".fullname($user).") is over bounce threshold! Not sending."); 6101 return false; 6102 } 6103 6104 // TLD .invalid is specifically reserved for invalid domain names. 6105 // For More information, see {@link http://tools.ietf.org/html/rfc2606#section-2}. 6106 if (substr($user->email, -8) == '.invalid') { 6107 debugging("email_to_user: User $user->id (".fullname($user).") email domain ($user->email) is invalid! Not sending."); 6108 return true; // This is not an error. 6109 } 6110 6111 // If the user is a remote mnet user, parse the email text for URL to the 6112 // wwwroot and modify the url to direct the user's browser to login at their 6113 // home site (identity provider - idp) before hitting the link itself. 6114 if (is_mnet_remote_user($user)) { 6115 require_once($CFG->dirroot.'/mnet/lib.php'); 6116 6117 $jumpurl = mnet_get_idp_jump_url($user); 6118 $callback = partial('mnet_sso_apply_indirection', $jumpurl); 6119 6120 $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%", 6121 $callback, 6122 $messagetext); 6123 $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%", 6124 $callback, 6125 $messagehtml); 6126 } 6127 $mail = get_mailer(); 6128 6129 if (!empty($mail->SMTPDebug)) { 6130 echo '<pre>' . "\n"; 6131 } 6132 6133 $temprecipients = array(); 6134 $tempreplyto = array(); 6135 6136 // Make sure that we fall back onto some reasonable no-reply address. 6137 $noreplyaddressdefault = 'noreply@' . get_host_from_url($CFG->wwwroot); 6138 $noreplyaddress = empty($CFG->noreplyaddress) ? $noreplyaddressdefault : $CFG->noreplyaddress; 6139 6140 if (!validate_email($noreplyaddress)) { 6141 debugging('email_to_user: Invalid noreply-email '.s($noreplyaddress)); 6142 $noreplyaddress = $noreplyaddressdefault; 6143 } 6144 6145 // Make up an email address for handling bounces. 6146 if (!empty($CFG->handlebounces)) { 6147 $modargs = 'B'.base64_encode(pack('V', $user->id)).substr(md5($user->email), 0, 16); 6148 $mail->Sender = generate_email_processing_address(0, $modargs); 6149 } else { 6150 $mail->Sender = $noreplyaddress; 6151 } 6152 6153 // Make sure that the explicit replyto is valid, fall back to the implicit one. 6154 if (!empty($replyto) && !validate_email($replyto)) { 6155 debugging('email_to_user: Invalid replyto-email '.s($replyto)); 6156 $replyto = $noreplyaddress; 6157 } 6158 6159 if (is_string($from)) { // So we can pass whatever we want if there is need. 6160 $mail->From = $noreplyaddress; 6161 $mail->FromName = $from; 6162 // Check if using the true address is true, and the email is in the list of allowed domains for sending email, 6163 // and that the senders email setting is either displayed to everyone, or display to only other users that are enrolled 6164 // in a course with the sender. 6165 } else if ($usetrueaddress && can_send_from_real_email_address($from, $user)) { 6166 if (!validate_email($from->email)) { 6167 debugging('email_to_user: Invalid from-email '.s($from->email).' - not sending'); 6168 // Better not to use $noreplyaddress in this case. 6169 return false; 6170 } 6171 $mail->From = $from->email; 6172 $fromdetails = new stdClass(); 6173 $fromdetails->name = fullname($from); 6174 $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot); 6175 $fromdetails->siteshortname = format_string($SITE->shortname); 6176 $fromstring = $fromdetails->name; 6177 if ($CFG->emailfromvia == EMAIL_VIA_ALWAYS) { 6178 $fromstring = get_string('emailvia', 'core', $fromdetails); 6179 } 6180 $mail->FromName = $fromstring; 6181 if (empty($replyto)) { 6182 $tempreplyto[] = array($from->email, fullname($from)); 6183 } 6184 } else { 6185 $mail->From = $noreplyaddress; 6186 $fromdetails = new stdClass(); 6187 $fromdetails->name = fullname($from); 6188 $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot); 6189 $fromdetails->siteshortname = format_string($SITE->shortname); 6190 $fromstring = $fromdetails->name; 6191 if ($CFG->emailfromvia != EMAIL_VIA_NEVER) { 6192 $fromstring = get_string('emailvia', 'core', $fromdetails); 6193 } 6194 $mail->FromName = $fromstring; 6195 if (empty($replyto)) { 6196 $tempreplyto[] = array($noreplyaddress, get_string('noreplyname')); 6197 } 6198 } 6199 6200 if (!empty($replyto)) { 6201 $tempreplyto[] = array($replyto, $replytoname); 6202 } 6203 6204 $temprecipients[] = array($user->email, fullname($user)); 6205 6206 // Set word wrap. 6207 $mail->WordWrap = $wordwrapwidth; 6208 6209 if (!empty($from->customheaders)) { 6210 // Add custom headers. 6211 if (is_array($from->customheaders)) { 6212 foreach ($from->customheaders as $customheader) { 6213 $mail->addCustomHeader($customheader); 6214 } 6215 } else { 6216 $mail->addCustomHeader($from->customheaders); 6217 } 6218 } 6219 6220 // If the X-PHP-Originating-Script email header is on then also add an additional 6221 // header with details of where exactly in moodle the email was triggered from, 6222 // either a call to message_send() or to email_to_user(). 6223 if (ini_get('mail.add_x_header')) { 6224 6225 $stack = debug_backtrace(false); 6226 $origin = $stack[0]; 6227 6228 foreach ($stack as $depth => $call) { 6229 if ($call['function'] == 'message_send') { 6230 $origin = $call; 6231 } 6232 } 6233 6234 $originheader = $CFG->wwwroot . ' => ' . gethostname() . ':' 6235 . str_replace($CFG->dirroot . '/', '', $origin['file']) . ':' . $origin['line']; 6236 $mail->addCustomHeader('X-Moodle-Originating-Script: ' . $originheader); 6237 } 6238 6239 if (!empty($from->priority)) { 6240 $mail->Priority = $from->priority; 6241 } 6242 6243 $renderer = $PAGE->get_renderer('core'); 6244 $context = array( 6245 'sitefullname' => $SITE->fullname, 6246 'siteshortname' => $SITE->shortname, 6247 'sitewwwroot' => $CFG->wwwroot, 6248 'subject' => $subject, 6249 'prefix' => $CFG->emailsubjectprefix, 6250 'to' => $user->email, 6251 'toname' => fullname($user), 6252 'from' => $mail->From, 6253 'fromname' => $mail->FromName, 6254 ); 6255 if (!empty($tempreplyto[0])) { 6256 $context['replyto'] = $tempreplyto[0][0]; 6257 $context['replytoname'] = $tempreplyto[0][1]; 6258 } 6259 if ($user->id > 0) { 6260 $context['touserid'] = $user->id; 6261 $context['tousername'] = $user->username; 6262 } 6263 6264 if (!empty($user->mailformat) && $user->mailformat == 1) { 6265 // Only process html templates if the user preferences allow html email. 6266 6267 if (!$messagehtml) { 6268 // If no html has been given, BUT there is an html wrapping template then 6269 // auto convert the text to html and then wrap it. 6270 $messagehtml = trim(text_to_html($messagetext)); 6271 } 6272 $context['body'] = $messagehtml; 6273 $messagehtml = $renderer->render_from_template('core/email_html', $context); 6274 } 6275 6276 $context['body'] = html_to_text(nl2br($messagetext)); 6277 $mail->Subject = $renderer->render_from_template('core/email_subject', $context); 6278 $mail->FromName = $renderer->render_from_template('core/email_fromname', $context); 6279 $messagetext = $renderer->render_from_template('core/email_text', $context); 6280 6281 // Autogenerate a MessageID if it's missing. 6282 if (empty($mail->MessageID)) { 6283 $mail->MessageID = generate_email_messageid(); 6284 } 6285 6286 if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) { 6287 // Don't ever send HTML to users who don't want it. 6288 $mail->isHTML(true); 6289 $mail->Encoding = 'quoted-printable'; 6290 $mail->Body = $messagehtml; 6291 $mail->AltBody = "\n$messagetext\n"; 6292 } else { 6293 $mail->IsHTML(false); 6294 $mail->Body = "\n$messagetext\n"; 6295 } 6296 6297 if ($attachment && $attachname) { 6298 if (preg_match( "~\\.\\.~" , $attachment )) { 6299 // Security check for ".." in dir path. 6300 $supportuser = core_user::get_support_user(); 6301 $temprecipients[] = array($supportuser->email, fullname($supportuser, true)); 6302 $mail->addStringAttachment('Error in attachment. User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain'); 6303 } else { 6304 require_once($CFG->libdir.'/filelib.php'); 6305 $mimetype = mimeinfo('type', $attachname); 6306 6307 $attachmentpath = $attachment; 6308 6309 // Before doing the comparison, make sure that the paths are correct (Windows uses slashes in the other direction). 6310 $attachpath = str_replace('\\', '/', $attachmentpath); 6311 6312 // Add allowed paths to an array (also check if it's not empty). 6313 $allowedpaths = array_filter([ 6314 $CFG->cachedir, 6315 $CFG->dataroot, 6316 $CFG->dirroot, 6317 $CFG->localcachedir, 6318 $CFG->tempdir 6319 ]); 6320 // Set addpath to true. 6321 $addpath = true; 6322 // Check if attachment includes one of the allowed paths. 6323 foreach ($allowedpaths as $tmpvar) { 6324 // Make sure both variables are normalised before comparing. 6325 $temppath = str_replace('\\', '/', realpath($tmpvar)); 6326 // Set addpath to false if the attachment includes one of the allowed paths. 6327 if (strpos($attachpath, $temppath) === 0) { 6328 $addpath = false; 6329 break; 6330 } 6331 } 6332 6333 // If the attachment is a full path to a file in the multiple allowed paths, use it as is, 6334 // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons). 6335 if ($addpath == true) { 6336 $attachmentpath = $CFG->dataroot . '/' . $attachmentpath; 6337 } 6338 6339 $mail->addAttachment($attachmentpath, $attachname, 'base64', $mimetype); 6340 } 6341 } 6342 6343 // Check if the email should be sent in an other charset then the default UTF-8. 6344 if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) { 6345 6346 // Use the defined site mail charset or eventually the one preferred by the recipient. 6347 $charset = $CFG->sitemailcharset; 6348 if (!empty($CFG->allowusermailcharset)) { 6349 if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) { 6350 $charset = $useremailcharset; 6351 } 6352 } 6353 6354 // Convert all the necessary strings if the charset is supported. 6355 $charsets = get_list_of_charsets(); 6356 unset($charsets['UTF-8']); 6357 if (in_array($charset, $charsets)) { 6358 $mail->CharSet = $charset; 6359 $mail->FromName = core_text::convert($mail->FromName, 'utf-8', strtolower($charset)); 6360 $mail->Subject = core_text::convert($mail->Subject, 'utf-8', strtolower($charset)); 6361 $mail->Body = core_text::convert($mail->Body, 'utf-8', strtolower($charset)); 6362 $mail->AltBody = core_text::convert($mail->AltBody, 'utf-8', strtolower($charset)); 6363 6364 foreach ($temprecipients as $key => $values) { 6365 $temprecipients[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset)); 6366 } 6367 foreach ($tempreplyto as $key => $values) { 6368 $tempreplyto[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset)); 6369 } 6370 } 6371 } 6372 6373 foreach ($temprecipients as $values) { 6374 $mail->addAddress($values[0], $values[1]); 6375 } 6376 foreach ($tempreplyto as $values) { 6377 $mail->addReplyTo($values[0], $values[1]); 6378 } 6379 6380 if ($mail->send()) { 6381 set_send_count($user); 6382 if (!empty($mail->SMTPDebug)) { 6383 echo '</pre>'; 6384 } 6385 return true; 6386 } else { 6387 // Trigger event for failing to send email. 6388 $event = \core\event\email_failed::create(array( 6389 'context' => context_system::instance(), 6390 'userid' => $from->id, 6391 'relateduserid' => $user->id, 6392 'other' => array( 6393 'subject' => $subject, 6394 'message' => $messagetext, 6395 'errorinfo' => $mail->ErrorInfo 6396 ) 6397 )); 6398 $event->trigger(); 6399 if (CLI_SCRIPT) { 6400 mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo); 6401 } 6402 if (!empty($mail->SMTPDebug)) { 6403 echo '</pre>'; 6404 } 6405 return false; 6406 } 6407 } 6408 6409 /** 6410 * Check to see if a user's real email address should be used for the "From" field. 6411 * 6412 * @param object $from The user object for the user we are sending the email from. 6413 * @param object $user The user object that we are sending the email to. 6414 * @param array $unused No longer used. 6415 * @return bool Returns true if we can use the from user's email adress in the "From" field. 6416 */ 6417 function can_send_from_real_email_address($from, $user, $unused = null) { 6418 global $CFG; 6419 if (!isset($CFG->allowedemaildomains) || empty(trim($CFG->allowedemaildomains))) { 6420 return false; 6421 } 6422 $alloweddomains = array_map('trim', explode("\n", $CFG->allowedemaildomains)); 6423 // Email is in the list of allowed domains for sending email, 6424 // and the senders email setting is either displayed to everyone, or display to only other users that are enrolled 6425 // in a course with the sender. 6426 if (\core\ip_utils::is_domain_in_allowed_list(substr($from->email, strpos($from->email, '@') + 1), $alloweddomains) 6427 && ($from->maildisplay == core_user::MAILDISPLAY_EVERYONE 6428 || ($from->maildisplay == core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY 6429 && enrol_get_shared_courses($user, $from, false, true)))) { 6430 return true; 6431 } 6432 return false; 6433 } 6434 6435 /** 6436 * Generate a signoff for emails based on support settings 6437 * 6438 * @return string 6439 */ 6440 function generate_email_signoff() { 6441 global $CFG; 6442 6443 $signoff = "\n"; 6444 if (!empty($CFG->supportname)) { 6445 $signoff .= $CFG->supportname."\n"; 6446 } 6447 if (!empty($CFG->supportemail)) { 6448 $signoff .= $CFG->supportemail."\n"; 6449 } 6450 if (!empty($CFG->supportpage)) { 6451 $signoff .= $CFG->supportpage."\n"; 6452 } 6453 return $signoff; 6454 } 6455 6456 /** 6457 * Sets specified user's password and send the new password to the user via email. 6458 * 6459 * @param stdClass $user A {@link $USER} object 6460 * @param bool $fasthash If true, use a low cost factor when generating the hash for speed. 6461 * @return bool|string Returns "true" if mail was sent OK and "false" if there was an error 6462 */ 6463 function setnew_password_and_mail($user, $fasthash = false) { 6464 global $CFG, $DB; 6465 6466 // We try to send the mail in language the user understands, 6467 // unfortunately the filter_string() does not support alternative langs yet 6468 // so multilang will not work properly for site->fullname. 6469 $lang = empty($user->lang) ? $CFG->lang : $user->lang; 6470 6471 $site = get_site(); 6472 6473 $supportuser = core_user::get_support_user(); 6474 6475 $newpassword = generate_password(); 6476 6477 update_internal_user_password($user, $newpassword, $fasthash); 6478 6479 $a = new stdClass(); 6480 $a->firstname = fullname($user, true); 6481 $a->sitename = format_string($site->fullname); 6482 $a->username = $user->username; 6483 $a->newpassword = $newpassword; 6484 $a->link = $CFG->wwwroot .'/login/?lang='.$lang; 6485 $a->signoff = generate_email_signoff(); 6486 6487 $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang); 6488 6489 $subject = format_string($site->fullname) .': '. (string)new lang_string('newusernewpasswordsubj', '', $a, $lang); 6490 6491 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 6492 return email_to_user($user, $supportuser, $subject, $message); 6493 6494 } 6495 6496 /** 6497 * Resets specified user's password and send the new password to the user via email. 6498 * 6499 * @param stdClass $user A {@link $USER} object 6500 * @return bool Returns true if mail was sent OK and false if there was an error. 6501 */ 6502 function reset_password_and_mail($user) { 6503 global $CFG; 6504 6505 $site = get_site(); 6506 $supportuser = core_user::get_support_user(); 6507 6508 $userauth = get_auth_plugin($user->auth); 6509 if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) { 6510 trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth."); 6511 return false; 6512 } 6513 6514 $newpassword = generate_password(); 6515 6516 if (!$userauth->user_update_password($user, $newpassword)) { 6517 print_error("cannotsetpassword"); 6518 } 6519 6520 $a = new stdClass(); 6521 $a->firstname = $user->firstname; 6522 $a->lastname = $user->lastname; 6523 $a->sitename = format_string($site->fullname); 6524 $a->username = $user->username; 6525 $a->newpassword = $newpassword; 6526 $a->link = $CFG->wwwroot .'/login/change_password.php'; 6527 $a->signoff = generate_email_signoff(); 6528 6529 $message = get_string('newpasswordtext', '', $a); 6530 6531 $subject = format_string($site->fullname) .': '. get_string('changedpassword'); 6532 6533 unset_user_preference('create_password', $user); // Prevent cron from generating the password. 6534 6535 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 6536 return email_to_user($user, $supportuser, $subject, $message); 6537 } 6538 6539 /** 6540 * Send email to specified user with confirmation text and activation link. 6541 * 6542 * @param stdClass $user A {@link $USER} object 6543 * @param string $confirmationurl user confirmation URL 6544 * @return bool Returns true if mail was sent OK and false if there was an error. 6545 */ 6546 function send_confirmation_email($user, $confirmationurl = null) { 6547 global $CFG; 6548 6549 $site = get_site(); 6550 $supportuser = core_user::get_support_user(); 6551 6552 $data = new stdClass(); 6553 $data->sitename = format_string($site->fullname); 6554 $data->admin = generate_email_signoff(); 6555 6556 $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname)); 6557 6558 if (empty($confirmationurl)) { 6559 $confirmationurl = '/login/confirm.php'; 6560 } 6561 6562 $confirmationurl = new moodle_url($confirmationurl); 6563 // Remove data parameter just in case it was included in the confirmation so we can add it manually later. 6564 $confirmationurl->remove_params('data'); 6565 $confirmationpath = $confirmationurl->out(false); 6566 6567 // We need to custom encode the username to include trailing dots in the link. 6568 // Because of this custom encoding we can't use moodle_url directly. 6569 // Determine if a query string is present in the confirmation url. 6570 $hasquerystring = strpos($confirmationpath, '?') !== false; 6571 // Perform normal url encoding of the username first. 6572 $username = urlencode($user->username); 6573 // Prevent problems with trailing dots not being included as part of link in some mail clients. 6574 $username = str_replace('.', '%2E', $username); 6575 6576 $data->link = $confirmationpath . ( $hasquerystring ? '&' : '?') . 'data='. $user->secret .'/'. $username; 6577 6578 $message = get_string('emailconfirmation', '', $data); 6579 $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true); 6580 6581 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 6582 return email_to_user($user, $supportuser, $subject, $message, $messagehtml); 6583 } 6584 6585 /** 6586 * Sends a password change confirmation email. 6587 * 6588 * @param stdClass $user A {@link $USER} object 6589 * @param stdClass $resetrecord An object tracking metadata regarding password reset request 6590 * @return bool Returns true if mail was sent OK and false if there was an error. 6591 */ 6592 function send_password_change_confirmation_email($user, $resetrecord) { 6593 global $CFG; 6594 6595 $site = get_site(); 6596 $supportuser = core_user::get_support_user(); 6597 $pwresetmins = isset($CFG->pwresettime) ? floor($CFG->pwresettime / MINSECS) : 30; 6598 6599 $data = new stdClass(); 6600 $data->firstname = $user->firstname; 6601 $data->lastname = $user->lastname; 6602 $data->username = $user->username; 6603 $data->sitename = format_string($site->fullname); 6604 $data->link = $CFG->wwwroot .'/login/forgot_password.php?token='. $resetrecord->token; 6605 $data->admin = generate_email_signoff(); 6606 $data->resetminutes = $pwresetmins; 6607 6608 $message = get_string('emailresetconfirmation', '', $data); 6609 $subject = get_string('emailresetconfirmationsubject', '', format_string($site->fullname)); 6610 6611 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 6612 return email_to_user($user, $supportuser, $subject, $message); 6613 6614 } 6615 6616 /** 6617 * Sends an email containing information on how to change your password. 6618 * 6619 * @param stdClass $user A {@link $USER} object 6620 * @return bool Returns true if mail was sent OK and false if there was an error. 6621 */ 6622 function send_password_change_info($user) { 6623 $site = get_site(); 6624 $supportuser = core_user::get_support_user(); 6625 6626 $data = new stdClass(); 6627 $data->firstname = $user->firstname; 6628 $data->lastname = $user->lastname; 6629 $data->username = $user->username; 6630 $data->sitename = format_string($site->fullname); 6631 $data->admin = generate_email_signoff(); 6632 6633 if (!is_enabled_auth($user->auth)) { 6634 $message = get_string('emailpasswordchangeinfodisabled', '', $data); 6635 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname)); 6636 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 6637 return email_to_user($user, $supportuser, $subject, $message); 6638 } 6639 6640 $userauth = get_auth_plugin($user->auth); 6641 ['subject' => $subject, 'message' => $message] = $userauth->get_password_change_info($user); 6642 6643 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 6644 return email_to_user($user, $supportuser, $subject, $message); 6645 } 6646 6647 /** 6648 * Check that an email is allowed. It returns an error message if there was a problem. 6649 * 6650 * @param string $email Content of email 6651 * @return string|false 6652 */ 6653 function email_is_not_allowed($email) { 6654 global $CFG; 6655 6656 // Comparing lowercase domains. 6657 $email = strtolower($email); 6658 if (!empty($CFG->allowemailaddresses)) { 6659 $allowed = explode(' ', strtolower($CFG->allowemailaddresses)); 6660 foreach ($allowed as $allowedpattern) { 6661 $allowedpattern = trim($allowedpattern); 6662 if (!$allowedpattern) { 6663 continue; 6664 } 6665 if (strpos($allowedpattern, '.') === 0) { 6666 if (strpos(strrev($email), strrev($allowedpattern)) === 0) { 6667 // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com". 6668 return false; 6669 } 6670 6671 } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) { 6672 return false; 6673 } 6674 } 6675 return get_string('emailonlyallowed', '', $CFG->allowemailaddresses); 6676 6677 } else if (!empty($CFG->denyemailaddresses)) { 6678 $denied = explode(' ', strtolower($CFG->denyemailaddresses)); 6679 foreach ($denied as $deniedpattern) { 6680 $deniedpattern = trim($deniedpattern); 6681 if (!$deniedpattern) { 6682 continue; 6683 } 6684 if (strpos($deniedpattern, '.') === 0) { 6685 if (strpos(strrev($email), strrev($deniedpattern)) === 0) { 6686 // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com". 6687 return get_string('emailnotallowed', '', $CFG->denyemailaddresses); 6688 } 6689 6690 } else if (strpos(strrev($email), strrev('@'.$deniedpattern)) === 0) { 6691 return get_string('emailnotallowed', '', $CFG->denyemailaddresses); 6692 } 6693 } 6694 } 6695 6696 return false; 6697 } 6698 6699 // FILE HANDLING. 6700 6701 /** 6702 * Returns local file storage instance 6703 * 6704 * @return file_storage 6705 */ 6706 function get_file_storage($reset = false) { 6707 global $CFG; 6708 6709 static $fs = null; 6710 6711 if ($reset) { 6712 $fs = null; 6713 return; 6714 } 6715 6716 if ($fs) { 6717 return $fs; 6718 } 6719 6720 require_once("$CFG->libdir/filelib.php"); 6721 6722 $fs = new file_storage(); 6723 6724 return $fs; 6725 } 6726 6727 /** 6728 * Returns local file storage instance 6729 * 6730 * @return file_browser 6731 */ 6732 function get_file_browser() { 6733 global $CFG; 6734 6735 static $fb = null; 6736 6737 if ($fb) { 6738 return $fb; 6739 } 6740 6741 require_once("$CFG->libdir/filelib.php"); 6742 6743 $fb = new file_browser(); 6744 6745 return $fb; 6746 } 6747 6748 /** 6749 * Returns file packer 6750 * 6751 * @param string $mimetype default application/zip 6752 * @return file_packer 6753 */ 6754 function get_file_packer($mimetype='application/zip') { 6755 global $CFG; 6756 6757 static $fp = array(); 6758 6759 if (isset($fp[$mimetype])) { 6760 return $fp[$mimetype]; 6761 } 6762 6763 switch ($mimetype) { 6764 case 'application/zip': 6765 case 'application/vnd.moodle.profiling': 6766 $classname = 'zip_packer'; 6767 break; 6768 6769 case 'application/x-gzip' : 6770 $classname = 'tgz_packer'; 6771 break; 6772 6773 case 'application/vnd.moodle.backup': 6774 $classname = 'mbz_packer'; 6775 break; 6776 6777 default: 6778 return false; 6779 } 6780 6781 require_once("$CFG->libdir/filestorage/$classname.php"); 6782 $fp[$mimetype] = new $classname(); 6783 6784 return $fp[$mimetype]; 6785 } 6786 6787 /** 6788 * Returns current name of file on disk if it exists. 6789 * 6790 * @param string $newfile File to be verified 6791 * @return string Current name of file on disk if true 6792 */ 6793 function valid_uploaded_file($newfile) { 6794 if (empty($newfile)) { 6795 return ''; 6796 } 6797 if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) { 6798 return $newfile['tmp_name']; 6799 } else { 6800 return ''; 6801 } 6802 } 6803 6804 /** 6805 * Returns the maximum size for uploading files. 6806 * 6807 * There are seven possible upload limits: 6808 * 1. in Apache using LimitRequestBody (no way of checking or changing this) 6809 * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP) 6810 * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP) 6811 * 4. in php.ini for 'post_max_size' (can not be changed inside PHP) 6812 * 5. by the Moodle admin in $CFG->maxbytes 6813 * 6. by the teacher in the current course $course->maxbytes 6814 * 7. by the teacher for the current module, eg $assignment->maxbytes 6815 * 6816 * These last two are passed to this function as arguments (in bytes). 6817 * Anything defined as 0 is ignored. 6818 * The smallest of all the non-zero numbers is returned. 6819 * 6820 * @todo Finish documenting this function 6821 * 6822 * @param int $sitebytes Set maximum size 6823 * @param int $coursebytes Current course $course->maxbytes (in bytes) 6824 * @param int $modulebytes Current module ->maxbytes (in bytes) 6825 * @param bool $unused This parameter has been deprecated and is not used any more. 6826 * @return int The maximum size for uploading files. 6827 */ 6828 function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0, $unused = false) { 6829 6830 if (! $filesize = ini_get('upload_max_filesize')) { 6831 $filesize = '5M'; 6832 } 6833 $minimumsize = get_real_size($filesize); 6834 6835 if ($postsize = ini_get('post_max_size')) { 6836 $postsize = get_real_size($postsize); 6837 if ($postsize < $minimumsize) { 6838 $minimumsize = $postsize; 6839 } 6840 } 6841 6842 if (($sitebytes > 0) and ($sitebytes < $minimumsize)) { 6843 $minimumsize = $sitebytes; 6844 } 6845 6846 if (($coursebytes > 0) and ($coursebytes < $minimumsize)) { 6847 $minimumsize = $coursebytes; 6848 } 6849 6850 if (($modulebytes > 0) and ($modulebytes < $minimumsize)) { 6851 $minimumsize = $modulebytes; 6852 } 6853 6854 return $minimumsize; 6855 } 6856 6857 /** 6858 * Returns the maximum size for uploading files for the current user 6859 * 6860 * This function takes in account {@link get_max_upload_file_size()} the user's capabilities 6861 * 6862 * @param context $context The context in which to check user capabilities 6863 * @param int $sitebytes Set maximum size 6864 * @param int $coursebytes Current course $course->maxbytes (in bytes) 6865 * @param int $modulebytes Current module ->maxbytes (in bytes) 6866 * @param stdClass $user The user 6867 * @param bool $unused This parameter has been deprecated and is not used any more. 6868 * @return int The maximum size for uploading files. 6869 */ 6870 function get_user_max_upload_file_size($context, $sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $user = null, 6871 $unused = false) { 6872 global $USER; 6873 6874 if (empty($user)) { 6875 $user = $USER; 6876 } 6877 6878 if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) { 6879 return USER_CAN_IGNORE_FILE_SIZE_LIMITS; 6880 } 6881 6882 return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes); 6883 } 6884 6885 /** 6886 * Returns an array of possible sizes in local language 6887 * 6888 * Related to {@link get_max_upload_file_size()} - this function returns an 6889 * array of possible sizes in an array, translated to the 6890 * local language. 6891 * 6892 * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes. 6893 * 6894 * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)" 6895 * with the value set to 0. This option will be the first in the list. 6896 * 6897 * @uses SORT_NUMERIC 6898 * @param int $sitebytes Set maximum size 6899 * @param int $coursebytes Current course $course->maxbytes (in bytes) 6900 * @param int $modulebytes Current module ->maxbytes (in bytes) 6901 * @param int|array $custombytes custom upload size/s which will be added to list, 6902 * Only value/s smaller then maxsize will be added to list. 6903 * @return array 6904 */ 6905 function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null) { 6906 global $CFG; 6907 6908 if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) { 6909 return array(); 6910 } 6911 6912 if ($sitebytes == 0) { 6913 // Will get the minimum of upload_max_filesize or post_max_size. 6914 $sitebytes = get_max_upload_file_size(); 6915 } 6916 6917 $filesize = array(); 6918 $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152, 6919 5242880, 10485760, 20971520, 52428800, 104857600, 6920 262144000, 524288000, 786432000, 1073741824, 6921 2147483648, 4294967296, 8589934592); 6922 6923 // If custombytes is given and is valid then add it to the list. 6924 if (is_number($custombytes) and $custombytes > 0) { 6925 $custombytes = (int)$custombytes; 6926 if (!in_array($custombytes, $sizelist)) { 6927 $sizelist[] = $custombytes; 6928 } 6929 } else if (is_array($custombytes)) { 6930 $sizelist = array_unique(array_merge($sizelist, $custombytes)); 6931 } 6932 6933 // Allow maxbytes to be selected if it falls outside the above boundaries. 6934 if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) { 6935 // Note: get_real_size() is used in order to prevent problems with invalid values. 6936 $sizelist[] = get_real_size($CFG->maxbytes); 6937 } 6938 6939 foreach ($sizelist as $sizebytes) { 6940 if ($sizebytes < $maxsize && $sizebytes > 0) { 6941 $filesize[(string)intval($sizebytes)] = display_size($sizebytes); 6942 } 6943 } 6944 6945 $limitlevel = ''; 6946 $displaysize = ''; 6947 if ($modulebytes && 6948 (($modulebytes < $coursebytes || $coursebytes == 0) && 6949 ($modulebytes < $sitebytes || $sitebytes == 0))) { 6950 $limitlevel = get_string('activity', 'core'); 6951 $displaysize = display_size($modulebytes); 6952 $filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list. 6953 6954 } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) { 6955 $limitlevel = get_string('course', 'core'); 6956 $displaysize = display_size($coursebytes); 6957 $filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list. 6958 6959 } else if ($sitebytes) { 6960 $limitlevel = get_string('site', 'core'); 6961 $displaysize = display_size($sitebytes); 6962 $filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list. 6963 } 6964 6965 krsort($filesize, SORT_NUMERIC); 6966 if ($limitlevel) { 6967 $params = (object) array('contextname' => $limitlevel, 'displaysize' => $displaysize); 6968 $filesize = array('0' => get_string('uploadlimitwithsize', 'core', $params)) + $filesize; 6969 } 6970 6971 return $filesize; 6972 } 6973 6974 /** 6975 * Returns an array with all the filenames in all subdirectories, relative to the given rootdir. 6976 * 6977 * If excludefiles is defined, then that file/directory is ignored 6978 * If getdirs is true, then (sub)directories are included in the output 6979 * If getfiles is true, then files are included in the output 6980 * (at least one of these must be true!) 6981 * 6982 * @todo Finish documenting this function. Add examples of $excludefile usage. 6983 * 6984 * @param string $rootdir A given root directory to start from 6985 * @param string|array $excludefiles If defined then the specified file/directory is ignored 6986 * @param bool $descend If true then subdirectories are recursed as well 6987 * @param bool $getdirs If true then (sub)directories are included in the output 6988 * @param bool $getfiles If true then files are included in the output 6989 * @return array An array with all the filenames in all subdirectories, relative to the given rootdir 6990 */ 6991 function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) { 6992 6993 $dirs = array(); 6994 6995 if (!$getdirs and !$getfiles) { // Nothing to show. 6996 return $dirs; 6997 } 6998 6999 if (!is_dir($rootdir)) { // Must be a directory. 7000 return $dirs; 7001 } 7002 7003 if (!$dir = opendir($rootdir)) { // Can't open it for some reason. 7004 return $dirs; 7005 } 7006 7007 if (!is_array($excludefiles)) { 7008 $excludefiles = array($excludefiles); 7009 } 7010 7011 while (false !== ($file = readdir($dir))) { 7012 $firstchar = substr($file, 0, 1); 7013 if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) { 7014 continue; 7015 } 7016 $fullfile = $rootdir .'/'. $file; 7017 if (filetype($fullfile) == 'dir') { 7018 if ($getdirs) { 7019 $dirs[] = $file; 7020 } 7021 if ($descend) { 7022 $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles); 7023 foreach ($subdirs as $subdir) { 7024 $dirs[] = $file .'/'. $subdir; 7025 } 7026 } 7027 } else if ($getfiles) { 7028 $dirs[] = $file; 7029 } 7030 } 7031 closedir($dir); 7032 7033 asort($dirs); 7034 7035 return $dirs; 7036 } 7037 7038 7039 /** 7040 * Adds up all the files in a directory and works out the size. 7041 * 7042 * @param string $rootdir The directory to start from 7043 * @param string $excludefile A file to exclude when summing directory size 7044 * @return int The summed size of all files and subfiles within the root directory 7045 */ 7046 function get_directory_size($rootdir, $excludefile='') { 7047 global $CFG; 7048 7049 // Do it this way if we can, it's much faster. 7050 if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) { 7051 $command = trim($CFG->pathtodu).' -sk '.escapeshellarg($rootdir); 7052 $output = null; 7053 $return = null; 7054 exec($command, $output, $return); 7055 if (is_array($output)) { 7056 // We told it to return k. 7057 return get_real_size(intval($output[0]).'k'); 7058 } 7059 } 7060 7061 if (!is_dir($rootdir)) { 7062 // Must be a directory. 7063 return 0; 7064 } 7065 7066 if (!$dir = @opendir($rootdir)) { 7067 // Can't open it for some reason. 7068 return 0; 7069 } 7070 7071 $size = 0; 7072 7073 while (false !== ($file = readdir($dir))) { 7074 $firstchar = substr($file, 0, 1); 7075 if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) { 7076 continue; 7077 } 7078 $fullfile = $rootdir .'/'. $file; 7079 if (filetype($fullfile) == 'dir') { 7080 $size += get_directory_size($fullfile, $excludefile); 7081 } else { 7082 $size += filesize($fullfile); 7083 } 7084 } 7085 closedir($dir); 7086 7087 return $size; 7088 } 7089 7090 /** 7091 * Converts bytes into display form 7092 * 7093 * @static string $gb Localized string for size in gigabytes 7094 * @static string $mb Localized string for size in megabytes 7095 * @static string $kb Localized string for size in kilobytes 7096 * @static string $b Localized string for size in bytes 7097 * @param int $size The size to convert to human readable form 7098 * @return string 7099 */ 7100 function display_size($size) { 7101 7102 static $gb, $mb, $kb, $b; 7103 7104 if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) { 7105 return get_string('unlimited'); 7106 } 7107 7108 if (empty($gb)) { 7109 $gb = get_string('sizegb'); 7110 $mb = get_string('sizemb'); 7111 $kb = get_string('sizekb'); 7112 $b = get_string('sizeb'); 7113 } 7114 7115 if ($size >= 1073741824) { 7116 $size = round($size / 1073741824 * 10) / 10 . $gb; 7117 } else if ($size >= 1048576) { 7118 $size = round($size / 1048576 * 10) / 10 . $mb; 7119 } else if ($size >= 1024) { 7120 $size = round($size / 1024 * 10) / 10 . $kb; 7121 } else { 7122 $size = intval($size) .' '. $b; // File sizes over 2GB can not work in 32bit PHP anyway. 7123 } 7124 return $size; 7125 } 7126 7127 /** 7128 * Cleans a given filename by removing suspicious or troublesome characters 7129 * 7130 * @see clean_param() 7131 * @param string $string file name 7132 * @return string cleaned file name 7133 */ 7134 function clean_filename($string) { 7135 return clean_param($string, PARAM_FILE); 7136 } 7137 7138 // STRING TRANSLATION. 7139 7140 /** 7141 * Returns the code for the current language 7142 * 7143 * @category string 7144 * @return string 7145 */ 7146 function current_language() { 7147 global $CFG, $USER, $SESSION, $COURSE; 7148 7149 if (!empty($SESSION->forcelang)) { 7150 // Allows overriding course-forced language (useful for admins to check 7151 // issues in courses whose language they don't understand). 7152 // Also used by some code to temporarily get language-related information in a 7153 // specific language (see force_current_language()). 7154 $return = $SESSION->forcelang; 7155 7156 } else if (!empty($COURSE->id) and $COURSE->id != SITEID and !empty($COURSE->lang)) { 7157 // Course language can override all other settings for this page. 7158 $return = $COURSE->lang; 7159 7160 } else if (!empty($SESSION->lang)) { 7161 // Session language can override other settings. 7162 $return = $SESSION->lang; 7163 7164 } else if (!empty($USER->lang)) { 7165 $return = $USER->lang; 7166 7167 } else if (isset($CFG->lang)) { 7168 $return = $CFG->lang; 7169 7170 } else { 7171 $return = 'en'; 7172 } 7173 7174 // Just in case this slipped in from somewhere by accident. 7175 $return = str_replace('_utf8', '', $return); 7176 7177 return $return; 7178 } 7179 7180 /** 7181 * Returns parent language of current active language if defined 7182 * 7183 * @category string 7184 * @param string $lang null means current language 7185 * @return string 7186 */ 7187 function get_parent_language($lang=null) { 7188 7189 $parentlang = get_string_manager()->get_string('parentlanguage', 'langconfig', null, $lang); 7190 7191 if ($parentlang === 'en') { 7192 $parentlang = ''; 7193 } 7194 7195 return $parentlang; 7196 } 7197 7198 /** 7199 * Force the current language to get strings and dates localised in the given language. 7200 * 7201 * After calling this function, all strings will be provided in the given language 7202 * until this function is called again, or equivalent code is run. 7203 * 7204 * @param string $language 7205 * @return string previous $SESSION->forcelang value 7206 */ 7207 function force_current_language($language) { 7208 global $SESSION; 7209 $sessionforcelang = isset($SESSION->forcelang) ? $SESSION->forcelang : ''; 7210 if ($language !== $sessionforcelang) { 7211 // Seting forcelang to null or an empty string disables it's effect. 7212 if (empty($language) || get_string_manager()->translation_exists($language, false)) { 7213 $SESSION->forcelang = $language; 7214 moodle_setlocale(); 7215 } 7216 } 7217 return $sessionforcelang; 7218 } 7219 7220 /** 7221 * Returns current string_manager instance. 7222 * 7223 * The param $forcereload is needed for CLI installer only where the string_manager instance 7224 * must be replaced during the install.php script life time. 7225 * 7226 * @category string 7227 * @param bool $forcereload shall the singleton be released and new instance created instead? 7228 * @return core_string_manager 7229 */ 7230 function get_string_manager($forcereload=false) { 7231 global $CFG; 7232 7233 static $singleton = null; 7234 7235 if ($forcereload) { 7236 $singleton = null; 7237 } 7238 if ($singleton === null) { 7239 if (empty($CFG->early_install_lang)) { 7240 7241 $transaliases = array(); 7242 if (empty($CFG->langlist)) { 7243 $translist = array(); 7244 } else { 7245 $translist = explode(',', $CFG->langlist); 7246 $translist = array_map('trim', $translist); 7247 // Each language in the $CFG->langlist can has an "alias" that would substitute the default language name. 7248 foreach ($translist as $i => $value) { 7249 $parts = preg_split('/\s*\|\s*/', $value, 2); 7250 if (count($parts) == 2) { 7251 $transaliases[$parts[0]] = $parts[1]; 7252 $translist[$i] = $parts[0]; 7253 } 7254 } 7255 } 7256 7257 if (!empty($CFG->config_php_settings['customstringmanager'])) { 7258 $classname = $CFG->config_php_settings['customstringmanager']; 7259 7260 if (class_exists($classname)) { 7261 $implements = class_implements($classname); 7262 7263 if (isset($implements['core_string_manager'])) { 7264 $singleton = new $classname($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases); 7265 return $singleton; 7266 7267 } else { 7268 debugging('Unable to instantiate custom string manager: class '.$classname. 7269 ' does not implement the core_string_manager interface.'); 7270 } 7271 7272 } else { 7273 debugging('Unable to instantiate custom string manager: class '.$classname.' can not be found.'); 7274 } 7275 } 7276 7277 $singleton = new core_string_manager_standard($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases); 7278 7279 } else { 7280 $singleton = new core_string_manager_install(); 7281 } 7282 } 7283 7284 return $singleton; 7285 } 7286 7287 /** 7288 * Returns a localized string. 7289 * 7290 * Returns the translated string specified by $identifier as 7291 * for $module. Uses the same format files as STphp. 7292 * $a is an object, string or number that can be used 7293 * within translation strings 7294 * 7295 * eg 'hello {$a->firstname} {$a->lastname}' 7296 * or 'hello {$a}' 7297 * 7298 * If you would like to directly echo the localized string use 7299 * the function {@link print_string()} 7300 * 7301 * Example usage of this function involves finding the string you would 7302 * like a local equivalent of and using its identifier and module information 7303 * to retrieve it.<br/> 7304 * If you open moodle/lang/en/moodle.php and look near line 278 7305 * you will find a string to prompt a user for their word for 'course' 7306 * <code> 7307 * $string['course'] = 'Course'; 7308 * </code> 7309 * So if you want to display the string 'Course' 7310 * in any language that supports it on your site 7311 * you just need to use the identifier 'course' 7312 * <code> 7313 * $mystring = '<strong>'. get_string('course') .'</strong>'; 7314 * or 7315 * </code> 7316 * If the string you want is in another file you'd take a slightly 7317 * different approach. Looking in moodle/lang/en/calendar.php you find 7318 * around line 75: 7319 * <code> 7320 * $string['typecourse'] = 'Course event'; 7321 * </code> 7322 * If you want to display the string "Course event" in any language 7323 * supported you would use the identifier 'typecourse' and the module 'calendar' 7324 * (because it is in the file calendar.php): 7325 * <code> 7326 * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>'; 7327 * </code> 7328 * 7329 * As a last resort, should the identifier fail to map to a string 7330 * the returned string will be [[ $identifier ]] 7331 * 7332 * In Moodle 2.3 there is a new argument to this function $lazyload. 7333 * Setting $lazyload to true causes get_string to return a lang_string object 7334 * rather than the string itself. The fetching of the string is then put off until 7335 * the string object is first used. The object can be used by calling it's out 7336 * method or by casting the object to a string, either directly e.g. 7337 * (string)$stringobject 7338 * or indirectly by using the string within another string or echoing it out e.g. 7339 * echo $stringobject 7340 * return "<p>{$stringobject}</p>"; 7341 * It is worth noting that using $lazyload and attempting to use the string as an 7342 * array key will cause a fatal error as objects cannot be used as array keys. 7343 * But you should never do that anyway! 7344 * For more information {@link lang_string} 7345 * 7346 * @category string 7347 * @param string $identifier The key identifier for the localized string 7348 * @param string $component The module where the key identifier is stored, 7349 * usually expressed as the filename in the language pack without the 7350 * .php on the end but can also be written as mod/forum or grade/export/xls. 7351 * If none is specified then moodle.php is used. 7352 * @param string|object|array $a An object, string or number that can be used 7353 * within translation strings 7354 * @param bool $lazyload If set to true a string object is returned instead of 7355 * the string itself. The string then isn't calculated until it is first used. 7356 * @return string The localized string. 7357 * @throws coding_exception 7358 */ 7359 function get_string($identifier, $component = '', $a = null, $lazyload = false) { 7360 global $CFG; 7361 7362 // If the lazy load argument has been supplied return a lang_string object 7363 // instead. 7364 // We need to make sure it is true (and a bool) as you will see below there 7365 // used to be a forth argument at one point. 7366 if ($lazyload === true) { 7367 return new lang_string($identifier, $component, $a); 7368 } 7369 7370 if ($CFG->debugdeveloper && clean_param($identifier, PARAM_STRINGID) === '') { 7371 throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.', DEBUG_DEVELOPER); 7372 } 7373 7374 // There is now a forth argument again, this time it is a boolean however so 7375 // we can still check for the old extralocations parameter. 7376 if (!is_bool($lazyload) && !empty($lazyload)) { 7377 debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.'); 7378 } 7379 7380 if (strpos($component, '/') !== false) { 7381 debugging('The module name you passed to get_string is the deprecated format ' . 7382 'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER); 7383 $componentpath = explode('/', $component); 7384 7385 switch ($componentpath[0]) { 7386 case 'mod': 7387 $component = $componentpath[1]; 7388 break; 7389 case 'blocks': 7390 case 'block': 7391 $component = 'block_'.$componentpath[1]; 7392 break; 7393 case 'enrol': 7394 $component = 'enrol_'.$componentpath[1]; 7395 break; 7396 case 'format': 7397 $component = 'format_'.$componentpath[1]; 7398 break; 7399 case 'grade': 7400 $component = 'grade'.$componentpath[1].'_'.$componentpath[2]; 7401 break; 7402 } 7403 } 7404 7405 $result = get_string_manager()->get_string($identifier, $component, $a); 7406 7407 // Debugging feature lets you display string identifier and component. 7408 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) { 7409 $result .= ' {' . $identifier . '/' . $component . '}'; 7410 } 7411 return $result; 7412 } 7413 7414 /** 7415 * Converts an array of strings to their localized value. 7416 * 7417 * @param array $array An array of strings 7418 * @param string $component The language module that these strings can be found in. 7419 * @return stdClass translated strings. 7420 */ 7421 function get_strings($array, $component = '') { 7422 $string = new stdClass; 7423 foreach ($array as $item) { 7424 $string->$item = get_string($item, $component); 7425 } 7426 return $string; 7427 } 7428 7429 /** 7430 * Prints out a translated string. 7431 * 7432 * Prints out a translated string using the return value from the {@link get_string()} function. 7433 * 7434 * Example usage of this function when the string is in the moodle.php file:<br/> 7435 * <code> 7436 * echo '<strong>'; 7437 * print_string('course'); 7438 * echo '</strong>'; 7439 * </code> 7440 * 7441 * Example usage of this function when the string is not in the moodle.php file:<br/> 7442 * <code> 7443 * echo '<h1>'; 7444 * print_string('typecourse', 'calendar'); 7445 * echo '</h1>'; 7446 * </code> 7447 * 7448 * @category string 7449 * @param string $identifier The key identifier for the localized string 7450 * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used. 7451 * @param string|object|array $a An object, string or number that can be used within translation strings 7452 */ 7453 function print_string($identifier, $component = '', $a = null) { 7454 echo get_string($identifier, $component, $a); 7455 } 7456 7457 /** 7458 * Returns a list of charset codes 7459 * 7460 * Returns a list of charset codes. It's hardcoded, so they should be added manually 7461 * (checking that such charset is supported by the texlib library!) 7462 * 7463 * @return array And associative array with contents in the form of charset => charset 7464 */ 7465 function get_list_of_charsets() { 7466 7467 $charsets = array( 7468 'EUC-JP' => 'EUC-JP', 7469 'ISO-2022-JP'=> 'ISO-2022-JP', 7470 'ISO-8859-1' => 'ISO-8859-1', 7471 'SHIFT-JIS' => 'SHIFT-JIS', 7472 'GB2312' => 'GB2312', 7473 'GB18030' => 'GB18030', // GB18030 not supported by typo and mbstring. 7474 'UTF-8' => 'UTF-8'); 7475 7476 asort($charsets); 7477 7478 return $charsets; 7479 } 7480 7481 /** 7482 * Returns a list of valid and compatible themes 7483 * 7484 * @return array 7485 */ 7486 function get_list_of_themes() { 7487 global $CFG; 7488 7489 $themes = array(); 7490 7491 if (!empty($CFG->themelist)) { // Use admin's list of themes. 7492 $themelist = explode(',', $CFG->themelist); 7493 } else { 7494 $themelist = array_keys(core_component::get_plugin_list("theme")); 7495 } 7496 7497 foreach ($themelist as $key => $themename) { 7498 $theme = theme_config::load($themename); 7499 $themes[$themename] = $theme; 7500 } 7501 7502 core_collator::asort_objects_by_method($themes, 'get_theme_name'); 7503 7504 return $themes; 7505 } 7506 7507 /** 7508 * Factory function for emoticon_manager 7509 * 7510 * @return emoticon_manager singleton 7511 */ 7512 function get_emoticon_manager() { 7513 static $singleton = null; 7514 7515 if (is_null($singleton)) { 7516 $singleton = new emoticon_manager(); 7517 } 7518 7519 return $singleton; 7520 } 7521 7522 /** 7523 * Provides core support for plugins that have to deal with emoticons (like HTML editor or emoticon filter). 7524 * 7525 * Whenever this manager mentiones 'emoticon object', the following data 7526 * structure is expected: stdClass with properties text, imagename, imagecomponent, 7527 * altidentifier and altcomponent 7528 * 7529 * @see admin_setting_emoticons 7530 * 7531 * @copyright 2010 David Mudrak 7532 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 7533 */ 7534 class emoticon_manager { 7535 7536 /** 7537 * Returns the currently enabled emoticons 7538 * 7539 * @param boolean $selectable - If true, only return emoticons that should be selectable from a list. 7540 * @return array of emoticon objects 7541 */ 7542 public function get_emoticons($selectable = false) { 7543 global $CFG; 7544 $notselectable = ['martin', 'egg']; 7545 7546 if (empty($CFG->emoticons)) { 7547 return array(); 7548 } 7549 7550 $emoticons = $this->decode_stored_config($CFG->emoticons); 7551 7552 if (!is_array($emoticons)) { 7553 // Something is wrong with the format of stored setting. 7554 debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL); 7555 return array(); 7556 } 7557 if ($selectable) { 7558 foreach ($emoticons as $index => $emote) { 7559 if (in_array($emote->altidentifier, $notselectable)) { 7560 // Skip this one. 7561 unset($emoticons[$index]); 7562 } 7563 } 7564 } 7565 7566 return $emoticons; 7567 } 7568 7569 /** 7570 * Converts emoticon object into renderable pix_emoticon object 7571 * 7572 * @param stdClass $emoticon emoticon object 7573 * @param array $attributes explicit HTML attributes to set 7574 * @return pix_emoticon 7575 */ 7576 public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array()) { 7577 $stringmanager = get_string_manager(); 7578 if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) { 7579 $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent); 7580 } else { 7581 $alt = s($emoticon->text); 7582 } 7583 return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes); 7584 } 7585 7586 /** 7587 * Encodes the array of emoticon objects into a string storable in config table 7588 * 7589 * @see self::decode_stored_config() 7590 * @param array $emoticons array of emtocion objects 7591 * @return string 7592 */ 7593 public function encode_stored_config(array $emoticons) { 7594 return json_encode($emoticons); 7595 } 7596 7597 /** 7598 * Decodes the string into an array of emoticon objects 7599 * 7600 * @see self::encode_stored_config() 7601 * @param string $encoded 7602 * @return string|null 7603 */ 7604 public function decode_stored_config($encoded) { 7605 $decoded = json_decode($encoded); 7606 if (!is_array($decoded)) { 7607 return null; 7608 } 7609 return $decoded; 7610 } 7611 7612 /** 7613 * Returns default set of emoticons supported by Moodle 7614 * 7615 * @return array of sdtClasses 7616 */ 7617 public function default_emoticons() { 7618 return array( 7619 $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'), 7620 $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'), 7621 $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'), 7622 $this->prepare_emoticon_object(";-)", 's/wink', 'wink'), 7623 $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'), 7624 $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'), 7625 $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'), 7626 $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'), 7627 $this->prepare_emoticon_object("B-)", 's/cool', 'cool'), 7628 $this->prepare_emoticon_object("^-)", 's/approve', 'approve'), 7629 $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'), 7630 $this->prepare_emoticon_object(":o)", 's/clown', 'clown'), 7631 $this->prepare_emoticon_object(":-(", 's/sad', 'sad'), 7632 $this->prepare_emoticon_object(":(", 's/sad', 'sad'), 7633 $this->prepare_emoticon_object("8-.", 's/shy', 'shy'), 7634 $this->prepare_emoticon_object(":-I", 's/blush', 'blush'), 7635 $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'), 7636 $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'), 7637 $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'), 7638 $this->prepare_emoticon_object("8-[", 's/angry', 'angry'), 7639 $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'), 7640 $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'), 7641 $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'), 7642 $this->prepare_emoticon_object("}-]", 's/evil', 'evil'), 7643 $this->prepare_emoticon_object("(h)", 's/heart', 'heart'), 7644 $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'), 7645 $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'), 7646 $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'), 7647 $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'), 7648 $this->prepare_emoticon_object("( )", 's/egg', 'egg'), 7649 ); 7650 } 7651 7652 /** 7653 * Helper method preparing the stdClass with the emoticon properties 7654 * 7655 * @param string|array $text or array of strings 7656 * @param string $imagename to be used by {@link pix_emoticon} 7657 * @param string $altidentifier alternative string identifier, null for no alt 7658 * @param string $altcomponent where the alternative string is defined 7659 * @param string $imagecomponent to be used by {@link pix_emoticon} 7660 * @return stdClass 7661 */ 7662 protected function prepare_emoticon_object($text, $imagename, $altidentifier = null, 7663 $altcomponent = 'core_pix', $imagecomponent = 'core') { 7664 return (object)array( 7665 'text' => $text, 7666 'imagename' => $imagename, 7667 'imagecomponent' => $imagecomponent, 7668 'altidentifier' => $altidentifier, 7669 'altcomponent' => $altcomponent, 7670 ); 7671 } 7672 } 7673 7674 // ENCRYPTION. 7675 7676 /** 7677 * rc4encrypt 7678 * 7679 * @param string $data Data to encrypt. 7680 * @return string The now encrypted data. 7681 */ 7682 function rc4encrypt($data) { 7683 return endecrypt(get_site_identifier(), $data, ''); 7684 } 7685 7686 /** 7687 * rc4decrypt 7688 * 7689 * @param string $data Data to decrypt. 7690 * @return string The now decrypted data. 7691 */ 7692 function rc4decrypt($data) { 7693 return endecrypt(get_site_identifier(), $data, 'de'); 7694 } 7695 7696 /** 7697 * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com] 7698 * 7699 * @todo Finish documenting this function 7700 * 7701 * @param string $pwd The password to use when encrypting or decrypting 7702 * @param string $data The data to be decrypted/encrypted 7703 * @param string $case Either 'de' for decrypt or '' for encrypt 7704 * @return string 7705 */ 7706 function endecrypt ($pwd, $data, $case) { 7707 7708 if ($case == 'de') { 7709 $data = urldecode($data); 7710 } 7711 7712 $key[] = ''; 7713 $box[] = ''; 7714 $pwdlength = strlen($pwd); 7715 7716 for ($i = 0; $i <= 255; $i++) { 7717 $key[$i] = ord(substr($pwd, ($i % $pwdlength), 1)); 7718 $box[$i] = $i; 7719 } 7720 7721 $x = 0; 7722 7723 for ($i = 0; $i <= 255; $i++) { 7724 $x = ($x + $box[$i] + $key[$i]) % 256; 7725 $tempswap = $box[$i]; 7726 $box[$i] = $box[$x]; 7727 $box[$x] = $tempswap; 7728 } 7729 7730 $cipher = ''; 7731 7732 $a = 0; 7733 $j = 0; 7734 7735 for ($i = 0; $i < strlen($data); $i++) { 7736 $a = ($a + 1) % 256; 7737 $j = ($j + $box[$a]) % 256; 7738 $temp = $box[$a]; 7739 $box[$a] = $box[$j]; 7740 $box[$j] = $temp; 7741 $k = $box[(($box[$a] + $box[$j]) % 256)]; 7742 $cipherby = ord(substr($data, $i, 1)) ^ $k; 7743 $cipher .= chr($cipherby); 7744 } 7745 7746 if ($case == 'de') { 7747 $cipher = urldecode(urlencode($cipher)); 7748 } else { 7749 $cipher = urlencode($cipher); 7750 } 7751 7752 return $cipher; 7753 } 7754 7755 // ENVIRONMENT CHECKING. 7756 7757 /** 7758 * This method validates a plug name. It is much faster than calling clean_param. 7759 * 7760 * @param string $name a string that might be a plugin name. 7761 * @return bool if this string is a valid plugin name. 7762 */ 7763 function is_valid_plugin_name($name) { 7764 // This does not work for 'mod', bad luck, use any other type. 7765 return core_component::is_valid_plugin_name('tool', $name); 7766 } 7767 7768 /** 7769 * Get a list of all the plugins of a given type that define a certain API function 7770 * in a certain file. The plugin component names and function names are returned. 7771 * 7772 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'. 7773 * @param string $function the part of the name of the function after the 7774 * frankenstyle prefix. e.g 'hook' if you are looking for functions with 7775 * names like report_courselist_hook. 7776 * @param string $file the name of file within the plugin that defines the 7777 * function. Defaults to lib.php. 7778 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum') 7779 * and the function names as values (e.g. 'report_courselist_hook', 'forum_hook'). 7780 */ 7781 function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php') { 7782 global $CFG; 7783 7784 // We don't include here as all plugin types files would be included. 7785 $plugins = get_plugins_with_function($function, $file, false); 7786 7787 if (empty($plugins[$plugintype])) { 7788 return array(); 7789 } 7790 7791 $allplugins = core_component::get_plugin_list($plugintype); 7792 7793 // Reformat the array and include the files. 7794 $pluginfunctions = array(); 7795 foreach ($plugins[$plugintype] as $pluginname => $functionname) { 7796 7797 // Check that it has not been removed and the file is still available. 7798 if (!empty($allplugins[$pluginname])) { 7799 7800 $filepath = $allplugins[$pluginname] . DIRECTORY_SEPARATOR . $file; 7801 if (file_exists($filepath)) { 7802 include_once($filepath); 7803 7804 // Now that the file is loaded, we must verify the function still exists. 7805 if (function_exists($functionname)) { 7806 $pluginfunctions[$plugintype . '_' . $pluginname] = $functionname; 7807 } else { 7808 // Invalidate the cache for next run. 7809 \cache_helper::invalidate_by_definition('core', 'plugin_functions'); 7810 } 7811 } 7812 } 7813 } 7814 7815 return $pluginfunctions; 7816 } 7817 7818 /** 7819 * Get a list of all the plugins that define a certain API function in a certain file. 7820 * 7821 * @param string $function the part of the name of the function after the 7822 * frankenstyle prefix. e.g 'hook' if you are looking for functions with 7823 * names like report_courselist_hook. 7824 * @param string $file the name of file within the plugin that defines the 7825 * function. Defaults to lib.php. 7826 * @param bool $include Whether to include the files that contain the functions or not. 7827 * @return array with [plugintype][plugin] = functionname 7828 */ 7829 function get_plugins_with_function($function, $file = 'lib.php', $include = true) { 7830 global $CFG; 7831 7832 if (during_initial_install() || isset($CFG->upgraderunning)) { 7833 // API functions _must not_ be called during an installation or upgrade. 7834 return []; 7835 } 7836 7837 $cache = \cache::make('core', 'plugin_functions'); 7838 7839 // Including both although I doubt that we will find two functions definitions with the same name. 7840 // Clearning the filename as cache_helper::hash_key only allows a-zA-Z0-9_. 7841 $key = $function . '_' . clean_param($file, PARAM_ALPHA); 7842 $pluginfunctions = $cache->get($key); 7843 $dirty = false; 7844 7845 // Use the plugin manager to check that plugins are currently installed. 7846 $pluginmanager = \core_plugin_manager::instance(); 7847 7848 if ($pluginfunctions !== false) { 7849 7850 // Checking that the files are still available. 7851 foreach ($pluginfunctions as $plugintype => $plugins) { 7852 7853 $allplugins = \core_component::get_plugin_list($plugintype); 7854 $installedplugins = $pluginmanager->get_installed_plugins($plugintype); 7855 foreach ($plugins as $plugin => $function) { 7856 if (!isset($installedplugins[$plugin])) { 7857 // Plugin code is still present on disk but it is not installed. 7858 $dirty = true; 7859 break 2; 7860 } 7861 7862 // Cache might be out of sync with the codebase, skip the plugin if it is not available. 7863 if (empty($allplugins[$plugin])) { 7864 $dirty = true; 7865 break 2; 7866 } 7867 7868 $fileexists = file_exists($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file); 7869 if ($include && $fileexists) { 7870 // Include the files if it was requested. 7871 include_once($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file); 7872 } else if (!$fileexists) { 7873 // If the file is not available any more it should not be returned. 7874 $dirty = true; 7875 break 2; 7876 } 7877 7878 // Check if the function still exists in the file. 7879 if ($include && !function_exists($function)) { 7880 $dirty = true; 7881 break 2; 7882 } 7883 } 7884 } 7885 7886 // If the cache is dirty, we should fall through and let it rebuild. 7887 if (!$dirty) { 7888 return $pluginfunctions; 7889 } 7890 } 7891 7892 $pluginfunctions = array(); 7893 7894 // To fill the cached. Also, everything should continue working with cache disabled. 7895 $plugintypes = \core_component::get_plugin_types(); 7896 foreach ($plugintypes as $plugintype => $unused) { 7897 7898 // We need to include files here. 7899 $pluginswithfile = \core_component::get_plugin_list_with_file($plugintype, $file, true); 7900 $installedplugins = $pluginmanager->get_installed_plugins($plugintype); 7901 foreach ($pluginswithfile as $plugin => $notused) { 7902 7903 if (!isset($installedplugins[$plugin])) { 7904 continue; 7905 } 7906 7907 $fullfunction = $plugintype . '_' . $plugin . '_' . $function; 7908 7909 $pluginfunction = false; 7910 if (function_exists($fullfunction)) { 7911 // Function exists with standard name. Store, indexed by frankenstyle name of plugin. 7912 $pluginfunction = $fullfunction; 7913 7914 } else if ($plugintype === 'mod') { 7915 // For modules, we also allow plugin without full frankenstyle but just starting with the module name. 7916 $shortfunction = $plugin . '_' . $function; 7917 if (function_exists($shortfunction)) { 7918 $pluginfunction = $shortfunction; 7919 } 7920 } 7921 7922 if ($pluginfunction) { 7923 if (empty($pluginfunctions[$plugintype])) { 7924 $pluginfunctions[$plugintype] = array(); 7925 } 7926 $pluginfunctions[$plugintype][$plugin] = $pluginfunction; 7927 } 7928 7929 } 7930 } 7931 $cache->set($key, $pluginfunctions); 7932 7933 return $pluginfunctions; 7934 7935 } 7936 7937 /** 7938 * Lists plugin-like directories within specified directory 7939 * 7940 * This function was originally used for standard Moodle plugins, please use 7941 * new core_component::get_plugin_list() now. 7942 * 7943 * This function is used for general directory listing and backwards compatility. 7944 * 7945 * @param string $directory relative directory from root 7946 * @param string $exclude dir name to exclude from the list (defaults to none) 7947 * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot) 7948 * @return array Sorted array of directory names found under the requested parameters 7949 */ 7950 function get_list_of_plugins($directory='mod', $exclude='', $basedir='') { 7951 global $CFG; 7952 7953 $plugins = array(); 7954 7955 if (empty($basedir)) { 7956 $basedir = $CFG->dirroot .'/'. $directory; 7957 7958 } else { 7959 $basedir = $basedir .'/'. $directory; 7960 } 7961 7962 if ($CFG->debugdeveloper and empty($exclude)) { 7963 // Make sure devs do not use this to list normal plugins, 7964 // this is intended for general directories that are not plugins! 7965 7966 $subtypes = core_component::get_plugin_types(); 7967 if (in_array($basedir, $subtypes)) { 7968 debugging('get_list_of_plugins() should not be used to list real plugins, use core_component::get_plugin_list() instead!', DEBUG_DEVELOPER); 7969 } 7970 unset($subtypes); 7971 } 7972 7973 if (file_exists($basedir) && filetype($basedir) == 'dir') { 7974 if (!$dirhandle = opendir($basedir)) { 7975 debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER); 7976 return array(); 7977 } 7978 while (false !== ($dir = readdir($dirhandle))) { 7979 // Func: strpos is marginally but reliably faster than substr($dir, 0, 1). 7980 if (strpos($dir, '.') === 0 or $dir === 'CVS' or $dir === '_vti_cnf' or $dir === 'simpletest' or $dir === 'yui' or 7981 $dir === 'tests' or $dir === 'classes' or $dir === $exclude) { 7982 continue; 7983 } 7984 if (filetype($basedir .'/'. $dir) != 'dir') { 7985 continue; 7986 } 7987 $plugins[] = $dir; 7988 } 7989 closedir($dirhandle); 7990 } 7991 if ($plugins) { 7992 asort($plugins); 7993 } 7994 return $plugins; 7995 } 7996 7997 /** 7998 * Invoke plugin's callback functions 7999 * 8000 * @param string $type plugin type e.g. 'mod' 8001 * @param string $name plugin name 8002 * @param string $feature feature name 8003 * @param string $action feature's action 8004 * @param array $params parameters of callback function, should be an array 8005 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null. 8006 * @return mixed 8007 * 8008 * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743 8009 */ 8010 function plugin_callback($type, $name, $feature, $action, $params = null, $default = null) { 8011 return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default); 8012 } 8013 8014 /** 8015 * Invoke component's callback functions 8016 * 8017 * @param string $component frankenstyle component name, e.g. 'mod_quiz' 8018 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron' 8019 * @param array $params parameters of callback function 8020 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null. 8021 * @return mixed 8022 */ 8023 function component_callback($component, $function, array $params = array(), $default = null) { 8024 8025 $functionname = component_callback_exists($component, $function); 8026 8027 if ($functionname) { 8028 // Function exists, so just return function result. 8029 $ret = call_user_func_array($functionname, $params); 8030 if (is_null($ret)) { 8031 return $default; 8032 } else { 8033 return $ret; 8034 } 8035 } 8036 return $default; 8037 } 8038 8039 /** 8040 * Determine if a component callback exists and return the function name to call. Note that this 8041 * function will include the required library files so that the functioname returned can be 8042 * called directly. 8043 * 8044 * @param string $component frankenstyle component name, e.g. 'mod_quiz' 8045 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron' 8046 * @return mixed Complete function name to call if the callback exists or false if it doesn't. 8047 * @throws coding_exception if invalid component specfied 8048 */ 8049 function component_callback_exists($component, $function) { 8050 global $CFG; // This is needed for the inclusions. 8051 8052 $cleancomponent = clean_param($component, PARAM_COMPONENT); 8053 if (empty($cleancomponent)) { 8054 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component); 8055 } 8056 $component = $cleancomponent; 8057 8058 list($type, $name) = core_component::normalize_component($component); 8059 $component = $type . '_' . $name; 8060 8061 $oldfunction = $name.'_'.$function; 8062 $function = $component.'_'.$function; 8063 8064 $dir = core_component::get_component_directory($component); 8065 if (empty($dir)) { 8066 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component); 8067 } 8068 8069 // Load library and look for function. 8070 if (file_exists($dir.'/lib.php')) { 8071 require_once($dir.'/lib.php'); 8072 } 8073 8074 if (!function_exists($function) and function_exists($oldfunction)) { 8075 if ($type !== 'mod' and $type !== 'core') { 8076 debugging("Please use new function name $function instead of legacy $oldfunction", DEBUG_DEVELOPER); 8077 } 8078 $function = $oldfunction; 8079 } 8080 8081 if (function_exists($function)) { 8082 return $function; 8083 } 8084 return false; 8085 } 8086 8087 /** 8088 * Call the specified callback method on the provided class. 8089 * 8090 * If the callback returns null, then the default value is returned instead. 8091 * If the class does not exist, then the default value is returned. 8092 * 8093 * @param string $classname The name of the class to call upon. 8094 * @param string $methodname The name of the staticically defined method on the class. 8095 * @param array $params The arguments to pass into the method. 8096 * @param mixed $default The default value. 8097 * @return mixed The return value. 8098 */ 8099 function component_class_callback($classname, $methodname, array $params, $default = null) { 8100 if (!class_exists($classname)) { 8101 return $default; 8102 } 8103 8104 if (!method_exists($classname, $methodname)) { 8105 return $default; 8106 } 8107 8108 $fullfunction = $classname . '::' . $methodname; 8109 $result = call_user_func_array($fullfunction, $params); 8110 8111 if (null === $result) { 8112 return $default; 8113 } else { 8114 return $result; 8115 } 8116 } 8117 8118 /** 8119 * Checks whether a plugin supports a specified feature. 8120 * 8121 * @param string $type Plugin type e.g. 'mod' 8122 * @param string $name Plugin name e.g. 'forum' 8123 * @param string $feature Feature code (FEATURE_xx constant) 8124 * @param mixed $default default value if feature support unknown 8125 * @return mixed Feature result (false if not supported, null if feature is unknown, 8126 * otherwise usually true but may have other feature-specific value such as array) 8127 * @throws coding_exception 8128 */ 8129 function plugin_supports($type, $name, $feature, $default = null) { 8130 global $CFG; 8131 8132 if ($type === 'mod' and $name === 'NEWMODULE') { 8133 // Somebody forgot to rename the module template. 8134 return false; 8135 } 8136 8137 $component = clean_param($type . '_' . $name, PARAM_COMPONENT); 8138 if (empty($component)) { 8139 throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name); 8140 } 8141 8142 $function = null; 8143 8144 if ($type === 'mod') { 8145 // We need this special case because we support subplugins in modules, 8146 // otherwise it would end up in infinite loop. 8147 if (file_exists("$CFG->dirroot/mod/$name/lib.php")) { 8148 include_once("$CFG->dirroot/mod/$name/lib.php"); 8149 $function = $component.'_supports'; 8150 if (!function_exists($function)) { 8151 // Legacy non-frankenstyle function name. 8152 $function = $name.'_supports'; 8153 } 8154 } 8155 8156 } else { 8157 if (!$path = core_component::get_plugin_directory($type, $name)) { 8158 // Non existent plugin type. 8159 return false; 8160 } 8161 if (file_exists("$path/lib.php")) { 8162 include_once("$path/lib.php"); 8163 $function = $component.'_supports'; 8164 } 8165 } 8166 8167 if ($function and function_exists($function)) { 8168 $supports = $function($feature); 8169 if (is_null($supports)) { 8170 // Plugin does not know - use default. 8171 return $default; 8172 } else { 8173 return $supports; 8174 } 8175 } 8176 8177 // Plugin does not care, so use default. 8178 return $default; 8179 } 8180 8181 /** 8182 * Returns true if the current version of PHP is greater that the specified one. 8183 * 8184 * @todo Check PHP version being required here is it too low? 8185 * 8186 * @param string $version The version of php being tested. 8187 * @return bool 8188 */ 8189 function check_php_version($version='5.2.4') { 8190 return (version_compare(phpversion(), $version) >= 0); 8191 } 8192 8193 /** 8194 * Determine if moodle installation requires update. 8195 * 8196 * Checks version numbers of main code and all plugins to see 8197 * if there are any mismatches. 8198 * 8199 * @return bool 8200 */ 8201 function moodle_needs_upgrading() { 8202 global $CFG; 8203 8204 if (empty($CFG->version)) { 8205 return true; 8206 } 8207 8208 // There is no need to purge plugininfo caches here because 8209 // these caches are not used during upgrade and they are purged after 8210 // every upgrade. 8211 8212 if (empty($CFG->allversionshash)) { 8213 return true; 8214 } 8215 8216 $hash = core_component::get_all_versions_hash(); 8217 8218 return ($hash !== $CFG->allversionshash); 8219 } 8220 8221 /** 8222 * Returns the major version of this site 8223 * 8224 * Moodle version numbers consist of three numbers separated by a dot, for 8225 * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so 8226 * called major version. This function extracts the major version from either 8227 * $CFG->release (default) or eventually from the $release variable defined in 8228 * the main version.php. 8229 * 8230 * @param bool $fromdisk should the version if source code files be used 8231 * @return string|false the major version like '2.3', false if could not be determined 8232 */ 8233 function moodle_major_version($fromdisk = false) { 8234 global $CFG; 8235 8236 if ($fromdisk) { 8237 $release = null; 8238 require($CFG->dirroot.'/version.php'); 8239 if (empty($release)) { 8240 return false; 8241 } 8242 8243 } else { 8244 if (empty($CFG->release)) { 8245 return false; 8246 } 8247 $release = $CFG->release; 8248 } 8249 8250 if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) { 8251 return $matches[0]; 8252 } else { 8253 return false; 8254 } 8255 } 8256 8257 // MISCELLANEOUS. 8258 8259 /** 8260 * Gets the system locale 8261 * 8262 * @return string Retuns the current locale. 8263 */ 8264 function moodle_getlocale() { 8265 global $CFG; 8266 8267 // Fetch the correct locale based on ostype. 8268 if ($CFG->ostype == 'WINDOWS') { 8269 $stringtofetch = 'localewin'; 8270 } else { 8271 $stringtofetch = 'locale'; 8272 } 8273 8274 if (!empty($CFG->locale)) { // Override locale for all language packs. 8275 return $CFG->locale; 8276 } 8277 8278 return get_string($stringtofetch, 'langconfig'); 8279 } 8280 8281 /** 8282 * Sets the system locale 8283 * 8284 * @category string 8285 * @param string $locale Can be used to force a locale 8286 */ 8287 function moodle_setlocale($locale='') { 8288 global $CFG; 8289 8290 static $currentlocale = ''; // Last locale caching. 8291 8292 $oldlocale = $currentlocale; 8293 8294 // The priority is the same as in get_string() - parameter, config, course, session, user, global language. 8295 if (!empty($locale)) { 8296 $currentlocale = $locale; 8297 } else { 8298 $currentlocale = moodle_getlocale(); 8299 } 8300 8301 // Do nothing if locale already set up. 8302 if ($oldlocale == $currentlocale) { 8303 return; 8304 } 8305 8306 // Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values, 8307 // set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7 8308 // Some day, numeric, monetary and other categories should be set too, I think. :-/. 8309 8310 // Get current values. 8311 $monetary= setlocale (LC_MONETARY, 0); 8312 $numeric = setlocale (LC_NUMERIC, 0); 8313 $ctype = setlocale (LC_CTYPE, 0); 8314 if ($CFG->ostype != 'WINDOWS') { 8315 $messages= setlocale (LC_MESSAGES, 0); 8316 } 8317 // Set locale to all. 8318 $result = setlocale (LC_ALL, $currentlocale); 8319 // If setting of locale fails try the other utf8 or utf-8 variant, 8320 // some operating systems support both (Debian), others just one (OSX). 8321 if ($result === false) { 8322 if (stripos($currentlocale, '.UTF-8') !== false) { 8323 $newlocale = str_ireplace('.UTF-8', '.UTF8', $currentlocale); 8324 setlocale (LC_ALL, $newlocale); 8325 } else if (stripos($currentlocale, '.UTF8') !== false) { 8326 $newlocale = str_ireplace('.UTF8', '.UTF-8', $currentlocale); 8327 setlocale (LC_ALL, $newlocale); 8328 } 8329 } 8330 // Set old values. 8331 setlocale (LC_MONETARY, $monetary); 8332 setlocale (LC_NUMERIC, $numeric); 8333 if ($CFG->ostype != 'WINDOWS') { 8334 setlocale (LC_MESSAGES, $messages); 8335 } 8336 if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') { 8337 // To workaround a well-known PHP problem with Turkish letter Ii. 8338 setlocale (LC_CTYPE, $ctype); 8339 } 8340 } 8341 8342 /** 8343 * Count words in a string. 8344 * 8345 * Words are defined as things between whitespace. 8346 * 8347 * @category string 8348 * @param string $string The text to be searched for words. May be HTML. 8349 * @return int The count of words in the specified string 8350 */ 8351 function count_words($string) { 8352 // Before stripping tags, add a space after the close tag of anything that is not obviously inline. 8353 // Also, br is a special case because it definitely delimits a word, but has no close tag. 8354 $string = preg_replace('~ 8355 ( # Capture the tag we match. 8356 </ # Start of close tag. 8357 (?! # Do not match any of these specific close tag names. 8358 a> | b> | del> | em> | i> | 8359 ins> | s> | small> | 8360 strong> | sub> | sup> | u> 8361 ) 8362 \w+ # But, apart from those execptions, match any tag name. 8363 > # End of close tag. 8364 | 8365 <br> | <br\s*/> # Special cases that are not close tags. 8366 ) 8367 ~x', '$1 ', $string); // Add a space after the close tag. 8368 // Now remove HTML tags. 8369 $string = strip_tags($string); 8370 // Decode HTML entities. 8371 $string = html_entity_decode($string); 8372 8373 // Now, the word count is the number of blocks of characters separated 8374 // by any sort of space. That seems to be the definition used by all other systems. 8375 // To be precise about what is considered to separate words: 8376 // * Anything that Unicode considers a 'Separator' 8377 // * Anything that Unicode considers a 'Control character' 8378 // * An em- or en- dash. 8379 return count(preg_split('~[\p{Z}\p{Cc}—–]+~u', $string, -1, PREG_SPLIT_NO_EMPTY)); 8380 } 8381 8382 /** 8383 * Count letters in a string. 8384 * 8385 * Letters are defined as chars not in tags and different from whitespace. 8386 * 8387 * @category string 8388 * @param string $string The text to be searched for letters. May be HTML. 8389 * @return int The count of letters in the specified text. 8390 */ 8391 function count_letters($string) { 8392 $string = strip_tags($string); // Tags are out now. 8393 $string = html_entity_decode($string); 8394 $string = preg_replace('/[[:space:]]*/', '', $string); // Whitespace are out now. 8395 8396 return core_text::strlen($string); 8397 } 8398 8399 /** 8400 * Generate and return a random string of the specified length. 8401 * 8402 * @param int $length The length of the string to be created. 8403 * @return string 8404 */ 8405 function random_string($length=15) { 8406 $randombytes = random_bytes_emulate($length); 8407 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 8408 $pool .= 'abcdefghijklmnopqrstuvwxyz'; 8409 $pool .= '0123456789'; 8410 $poollen = strlen($pool); 8411 $string = ''; 8412 for ($i = 0; $i < $length; $i++) { 8413 $rand = ord($randombytes[$i]); 8414 $string .= substr($pool, ($rand%($poollen)), 1); 8415 } 8416 return $string; 8417 } 8418 8419 /** 8420 * Generate a complex random string (useful for md5 salts) 8421 * 8422 * This function is based on the above {@link random_string()} however it uses a 8423 * larger pool of characters and generates a string between 24 and 32 characters 8424 * 8425 * @param int $length Optional if set generates a string to exactly this length 8426 * @return string 8427 */ 8428 function complex_random_string($length=null) { 8429 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 8430 $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} '; 8431 $poollen = strlen($pool); 8432 if ($length===null) { 8433 $length = floor(rand(24, 32)); 8434 } 8435 $randombytes = random_bytes_emulate($length); 8436 $string = ''; 8437 for ($i = 0; $i < $length; $i++) { 8438 $rand = ord($randombytes[$i]); 8439 $string .= $pool[($rand%$poollen)]; 8440 } 8441 return $string; 8442 } 8443 8444 /** 8445 * Try to generates cryptographically secure pseudo-random bytes. 8446 * 8447 * Note this is achieved by fallbacking between: 8448 * - PHP 7 random_bytes(). 8449 * - OpenSSL openssl_random_pseudo_bytes(). 8450 * - In house random generator getting its entropy from various, hard to guess, pseudo-random sources. 8451 * 8452 * @param int $length requested length in bytes 8453 * @return string binary data 8454 */ 8455 function random_bytes_emulate($length) { 8456 global $CFG; 8457 if ($length <= 0) { 8458 debugging('Invalid random bytes length', DEBUG_DEVELOPER); 8459 return ''; 8460 } 8461 if (function_exists('random_bytes')) { 8462 // Use PHP 7 goodness. 8463 $hash = @random_bytes($length); 8464 if ($hash !== false) { 8465 return $hash; 8466 } 8467 } 8468 if (function_exists('openssl_random_pseudo_bytes')) { 8469 // If you have the openssl extension enabled. 8470 $hash = openssl_random_pseudo_bytes($length); 8471 if ($hash !== false) { 8472 return $hash; 8473 } 8474 } 8475 8476 // Bad luck, there is no reliable random generator, let's just slowly hash some unique stuff that is hard to guess. 8477 $staticdata = serialize($CFG) . serialize($_SERVER); 8478 $hash = ''; 8479 do { 8480 $hash .= sha1($staticdata . microtime(true) . uniqid('', true), true); 8481 } while (strlen($hash) < $length); 8482 8483 return substr($hash, 0, $length); 8484 } 8485 8486 /** 8487 * Given some text (which may contain HTML) and an ideal length, 8488 * this function truncates the text neatly on a word boundary if possible 8489 * 8490 * @category string 8491 * @param string $text text to be shortened 8492 * @param int $ideal ideal string length 8493 * @param boolean $exact if false, $text will not be cut mid-word 8494 * @param string $ending The string to append if the passed string is truncated 8495 * @return string $truncate shortened string 8496 */ 8497 function shorten_text($text, $ideal=30, $exact = false, $ending='...') { 8498 // If the plain text is shorter than the maximum length, return the whole text. 8499 if (core_text::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) { 8500 return $text; 8501 } 8502 8503 // Splits on HTML tags. Each open/close/empty tag will be the first thing 8504 // and only tag in its 'line'. 8505 preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER); 8506 8507 $totallength = core_text::strlen($ending); 8508 $truncate = ''; 8509 8510 // This array stores information about open and close tags and their position 8511 // in the truncated string. Each item in the array is an object with fields 8512 // ->open (true if open), ->tag (tag name in lower case), and ->pos 8513 // (byte position in truncated text). 8514 $tagdetails = array(); 8515 8516 foreach ($lines as $linematchings) { 8517 // If there is any html-tag in this line, handle it and add it (uncounted) to the output. 8518 if (!empty($linematchings[1])) { 8519 // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>). 8520 if (!preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $linematchings[1])) { 8521 if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $linematchings[1], $tagmatchings)) { 8522 // Record closing tag. 8523 $tagdetails[] = (object) array( 8524 'open' => false, 8525 'tag' => core_text::strtolower($tagmatchings[1]), 8526 'pos' => core_text::strlen($truncate), 8527 ); 8528 8529 } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $linematchings[1], $tagmatchings)) { 8530 // Record opening tag. 8531 $tagdetails[] = (object) array( 8532 'open' => true, 8533 'tag' => core_text::strtolower($tagmatchings[1]), 8534 'pos' => core_text::strlen($truncate), 8535 ); 8536 } else if (preg_match('/^<!--\[if\s.*?\]>$/s', $linematchings[1], $tagmatchings)) { 8537 $tagdetails[] = (object) array( 8538 'open' => true, 8539 'tag' => core_text::strtolower('if'), 8540 'pos' => core_text::strlen($truncate), 8541 ); 8542 } else if (preg_match('/^<!--<!\[endif\]-->$/s', $linematchings[1], $tagmatchings)) { 8543 $tagdetails[] = (object) array( 8544 'open' => false, 8545 'tag' => core_text::strtolower('if'), 8546 'pos' => core_text::strlen($truncate), 8547 ); 8548 } 8549 } 8550 // Add html-tag to $truncate'd text. 8551 $truncate .= $linematchings[1]; 8552 } 8553 8554 // Calculate the length of the plain text part of the line; handle entities as one character. 8555 $contentlength = core_text::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $linematchings[2])); 8556 if ($totallength + $contentlength > $ideal) { 8557 // The number of characters which are left. 8558 $left = $ideal - $totallength; 8559 $entitieslength = 0; 8560 // Search for html entities. 8561 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)) { 8562 // Calculate the real length of all entities in the legal range. 8563 foreach ($entities[0] as $entity) { 8564 if ($entity[1]+1-$entitieslength <= $left) { 8565 $left--; 8566 $entitieslength += core_text::strlen($entity[0]); 8567 } else { 8568 // No more characters left. 8569 break; 8570 } 8571 } 8572 } 8573 $breakpos = $left + $entitieslength; 8574 8575 // If the words shouldn't be cut in the middle... 8576 if (!$exact) { 8577 // Search the last occurence of a space. 8578 for (; $breakpos > 0; $breakpos--) { 8579 if ($char = core_text::substr($linematchings[2], $breakpos, 1)) { 8580 if ($char === '.' or $char === ' ') { 8581 $breakpos += 1; 8582 break; 8583 } else if (strlen($char) > 2) { 8584 // Chinese/Japanese/Korean text can be truncated at any UTF-8 character boundary. 8585 $breakpos += 1; 8586 break; 8587 } 8588 } 8589 } 8590 } 8591 if ($breakpos == 0) { 8592 // This deals with the test_shorten_text_no_spaces case. 8593 $breakpos = $left + $entitieslength; 8594 } else if ($breakpos > $left + $entitieslength) { 8595 // This deals with the previous for loop breaking on the first char. 8596 $breakpos = $left + $entitieslength; 8597 } 8598 8599 $truncate .= core_text::substr($linematchings[2], 0, $breakpos); 8600 // Maximum length is reached, so get off the loop. 8601 break; 8602 } else { 8603 $truncate .= $linematchings[2]; 8604 $totallength += $contentlength; 8605 } 8606 8607 // If the maximum length is reached, get off the loop. 8608 if ($totallength >= $ideal) { 8609 break; 8610 } 8611 } 8612 8613 // Add the defined ending to the text. 8614 $truncate .= $ending; 8615 8616 // Now calculate the list of open html tags based on the truncate position. 8617 $opentags = array(); 8618 foreach ($tagdetails as $taginfo) { 8619 if ($taginfo->open) { 8620 // Add tag to the beginning of $opentags list. 8621 array_unshift($opentags, $taginfo->tag); 8622 } else { 8623 // Can have multiple exact same open tags, close the last one. 8624 $pos = array_search($taginfo->tag, array_reverse($opentags, true)); 8625 if ($pos !== false) { 8626 unset($opentags[$pos]); 8627 } 8628 } 8629 } 8630 8631 // Close all unclosed html-tags. 8632 foreach ($opentags as $tag) { 8633 if ($tag === 'if') { 8634 $truncate .= '<!--<![endif]-->'; 8635 } else { 8636 $truncate .= '</' . $tag . '>'; 8637 } 8638 } 8639 8640 return $truncate; 8641 } 8642 8643 /** 8644 * Shortens a given filename by removing characters positioned after the ideal string length. 8645 * When the filename is too long, the file cannot be created on the filesystem due to exceeding max byte size. 8646 * Limiting the filename to a certain size (considering multibyte characters) will prevent this. 8647 * 8648 * @param string $filename file name 8649 * @param int $length ideal string length 8650 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness. 8651 * @return string $shortened shortened file name 8652 */ 8653 function shorten_filename($filename, $length = MAX_FILENAME_SIZE, $includehash = false) { 8654 $shortened = $filename; 8655 // Extract a part of the filename if it's char size exceeds the ideal string length. 8656 if (core_text::strlen($filename) > $length) { 8657 // Exclude extension if present in filename. 8658 $mimetypes = get_mimetypes_array(); 8659 $extension = pathinfo($filename, PATHINFO_EXTENSION); 8660 if ($extension && !empty($mimetypes[$extension])) { 8661 $basename = pathinfo($filename, PATHINFO_FILENAME); 8662 $hash = empty($includehash) ? '' : ' - ' . substr(sha1($basename), 0, 10); 8663 $shortened = core_text::substr($basename, 0, $length - strlen($hash)) . $hash; 8664 $shortened .= '.' . $extension; 8665 } else { 8666 $hash = empty($includehash) ? '' : ' - ' . substr(sha1($filename), 0, 10); 8667 $shortened = core_text::substr($filename, 0, $length - strlen($hash)) . $hash; 8668 } 8669 } 8670 return $shortened; 8671 } 8672 8673 /** 8674 * Shortens a given array of filenames by removing characters positioned after the ideal string length. 8675 * 8676 * @param array $path The paths to reduce the length. 8677 * @param int $length Ideal string length 8678 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness. 8679 * @return array $result Shortened paths in array. 8680 */ 8681 function shorten_filenames(array $path, $length = MAX_FILENAME_SIZE, $includehash = false) { 8682 $result = null; 8683 8684 $result = array_reduce($path, function($carry, $singlepath) use ($length, $includehash) { 8685 $carry[] = shorten_filename($singlepath, $length, $includehash); 8686 return $carry; 8687 }, []); 8688 8689 return $result; 8690 } 8691 8692 /** 8693 * Given dates in seconds, how many weeks is the date from startdate 8694 * The first week is 1, the second 2 etc ... 8695 * 8696 * @param int $startdate Timestamp for the start date 8697 * @param int $thedate Timestamp for the end date 8698 * @return string 8699 */ 8700 function getweek ($startdate, $thedate) { 8701 if ($thedate < $startdate) { 8702 return 0; 8703 } 8704 8705 return floor(($thedate - $startdate) / WEEKSECS) + 1; 8706 } 8707 8708 /** 8709 * Returns a randomly generated password of length $maxlen. inspired by 8710 * 8711 * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and 8712 * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254} 8713 * 8714 * @param int $maxlen The maximum size of the password being generated. 8715 * @return string 8716 */ 8717 function generate_password($maxlen=10) { 8718 global $CFG; 8719 8720 if (empty($CFG->passwordpolicy)) { 8721 $fillers = PASSWORD_DIGITS; 8722 $wordlist = file($CFG->wordlist); 8723 $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]); 8724 $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]); 8725 $filler1 = $fillers[rand(0, strlen($fillers) - 1)]; 8726 $password = $word1 . $filler1 . $word2; 8727 } else { 8728 $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0; 8729 $digits = $CFG->minpassworddigits; 8730 $lower = $CFG->minpasswordlower; 8731 $upper = $CFG->minpasswordupper; 8732 $nonalphanum = $CFG->minpasswordnonalphanum; 8733 $total = $lower + $upper + $digits + $nonalphanum; 8734 // Var minlength should be the greater one of the two ( $minlen and $total ). 8735 $minlen = $minlen < $total ? $total : $minlen; 8736 // Var maxlen can never be smaller than minlen. 8737 $maxlen = $minlen > $maxlen ? $minlen : $maxlen; 8738 $additional = $maxlen - $total; 8739 8740 // Make sure we have enough characters to fulfill 8741 // complexity requirements. 8742 $passworddigits = PASSWORD_DIGITS; 8743 while ($digits > strlen($passworddigits)) { 8744 $passworddigits .= PASSWORD_DIGITS; 8745 } 8746 $passwordlower = PASSWORD_LOWER; 8747 while ($lower > strlen($passwordlower)) { 8748 $passwordlower .= PASSWORD_LOWER; 8749 } 8750 $passwordupper = PASSWORD_UPPER; 8751 while ($upper > strlen($passwordupper)) { 8752 $passwordupper .= PASSWORD_UPPER; 8753 } 8754 $passwordnonalphanum = PASSWORD_NONALPHANUM; 8755 while ($nonalphanum > strlen($passwordnonalphanum)) { 8756 $passwordnonalphanum .= PASSWORD_NONALPHANUM; 8757 } 8758 8759 // Now mix and shuffle it all. 8760 $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) . 8761 substr(str_shuffle ($passwordupper), 0, $upper) . 8762 substr(str_shuffle ($passworddigits), 0, $digits) . 8763 substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) . 8764 substr(str_shuffle ($passwordlower . 8765 $passwordupper . 8766 $passworddigits . 8767 $passwordnonalphanum), 0 , $additional)); 8768 } 8769 8770 return substr ($password, 0, $maxlen); 8771 } 8772 8773 /** 8774 * Given a float, prints it nicely. 8775 * Localized floats must not be used in calculations! 8776 * 8777 * The stripzeros feature is intended for making numbers look nicer in small 8778 * areas where it is not necessary to indicate the degree of accuracy by showing 8779 * ending zeros. If you turn it on with $decimalpoints set to 3, for example, 8780 * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'. 8781 * 8782 * @param float $float The float to print 8783 * @param int $decimalpoints The number of decimal places to print. -1 is a special value for auto detect (full precision). 8784 * @param bool $localized use localized decimal separator 8785 * @param bool $stripzeros If true, removes final zeros after decimal point. It will be ignored and the trailing zeros after 8786 * the decimal point are always striped if $decimalpoints is -1. 8787 * @return string locale float 8788 */ 8789 function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=false) { 8790 if (is_null($float)) { 8791 return ''; 8792 } 8793 if ($localized) { 8794 $separator = get_string('decsep', 'langconfig'); 8795 } else { 8796 $separator = '.'; 8797 } 8798 if ($decimalpoints == -1) { 8799 // The following counts the number of decimals. 8800 // It is safe as both floatval() and round() functions have same behaviour when non-numeric values are provided. 8801 $floatval = floatval($float); 8802 for ($decimalpoints = 0; $floatval != round($float, $decimalpoints); $decimalpoints++); 8803 } 8804 8805 $result = number_format($float, $decimalpoints, $separator, ''); 8806 if ($stripzeros) { 8807 // Remove zeros and final dot if not needed. 8808 $result = preg_replace('~(' . preg_quote($separator, '~') . ')?0+$~', '', $result); 8809 } 8810 return $result; 8811 } 8812 8813 /** 8814 * Converts locale specific floating point/comma number back to standard PHP float value 8815 * Do NOT try to do any math operations before this conversion on any user submitted floats! 8816 * 8817 * @param string $localefloat locale aware float representation 8818 * @param bool $strict If true, then check the input and return false if it is not a valid number. 8819 * @return mixed float|bool - false or the parsed float. 8820 */ 8821 function unformat_float($localefloat, $strict = false) { 8822 $localefloat = trim($localefloat); 8823 8824 if ($localefloat == '') { 8825 return null; 8826 } 8827 8828 $localefloat = str_replace(' ', '', $localefloat); // No spaces - those might be used as thousand separators. 8829 $localefloat = str_replace(get_string('decsep', 'langconfig'), '.', $localefloat); 8830 8831 if ($strict && !is_numeric($localefloat)) { 8832 return false; 8833 } 8834 8835 return (float)$localefloat; 8836 } 8837 8838 /** 8839 * Given a simple array, this shuffles it up just like shuffle() 8840 * Unlike PHP's shuffle() this function works on any machine. 8841 * 8842 * @param array $array The array to be rearranged 8843 * @return array 8844 */ 8845 function swapshuffle($array) { 8846 8847 $last = count($array) - 1; 8848 for ($i = 0; $i <= $last; $i++) { 8849 $from = rand(0, $last); 8850 $curr = $array[$i]; 8851 $array[$i] = $array[$from]; 8852 $array[$from] = $curr; 8853 } 8854 return $array; 8855 } 8856 8857 /** 8858 * Like {@link swapshuffle()}, but works on associative arrays 8859 * 8860 * @param array $array The associative array to be rearranged 8861 * @return array 8862 */ 8863 function swapshuffle_assoc($array) { 8864 8865 $newarray = array(); 8866 $newkeys = swapshuffle(array_keys($array)); 8867 8868 foreach ($newkeys as $newkey) { 8869 $newarray[$newkey] = $array[$newkey]; 8870 } 8871 return $newarray; 8872 } 8873 8874 /** 8875 * Given an arbitrary array, and a number of draws, 8876 * this function returns an array with that amount 8877 * of items. The indexes are retained. 8878 * 8879 * @todo Finish documenting this function 8880 * 8881 * @param array $array 8882 * @param int $draws 8883 * @return array 8884 */ 8885 function draw_rand_array($array, $draws) { 8886 8887 $return = array(); 8888 8889 $last = count($array); 8890 8891 if ($draws > $last) { 8892 $draws = $last; 8893 } 8894 8895 while ($draws > 0) { 8896 $last--; 8897 8898 $keys = array_keys($array); 8899 $rand = rand(0, $last); 8900 8901 $return[$keys[$rand]] = $array[$keys[$rand]]; 8902 unset($array[$keys[$rand]]); 8903 8904 $draws--; 8905 } 8906 8907 return $return; 8908 } 8909 8910 /** 8911 * Calculate the difference between two microtimes 8912 * 8913 * @param string $a The first Microtime 8914 * @param string $b The second Microtime 8915 * @return string 8916 */ 8917 function microtime_diff($a, $b) { 8918 list($adec, $asec) = explode(' ', $a); 8919 list($bdec, $bsec) = explode(' ', $b); 8920 return $bsec - $asec + $bdec - $adec; 8921 } 8922 8923 /** 8924 * Given a list (eg a,b,c,d,e) this function returns 8925 * an array of 1->a, 2->b, 3->c etc 8926 * 8927 * @param string $list The string to explode into array bits 8928 * @param string $separator The separator used within the list string 8929 * @return array The now assembled array 8930 */ 8931 function make_menu_from_list($list, $separator=',') { 8932 8933 $array = array_reverse(explode($separator, $list), true); 8934 foreach ($array as $key => $item) { 8935 $outarray[$key+1] = trim($item); 8936 } 8937 return $outarray; 8938 } 8939 8940 /** 8941 * Creates an array that represents all the current grades that 8942 * can be chosen using the given grading type. 8943 * 8944 * Negative numbers 8945 * are scales, zero is no grade, and positive numbers are maximum 8946 * grades. 8947 * 8948 * @todo Finish documenting this function or better deprecated this completely! 8949 * 8950 * @param int $gradingtype 8951 * @return array 8952 */ 8953 function make_grades_menu($gradingtype) { 8954 global $DB; 8955 8956 $grades = array(); 8957 if ($gradingtype < 0) { 8958 if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) { 8959 return make_menu_from_list($scale->scale); 8960 } 8961 } else if ($gradingtype > 0) { 8962 for ($i=$gradingtype; $i>=0; $i--) { 8963 $grades[$i] = $i .' / '. $gradingtype; 8964 } 8965 return $grades; 8966 } 8967 return $grades; 8968 } 8969 8970 /** 8971 * make_unique_id_code 8972 * 8973 * @todo Finish documenting this function 8974 * 8975 * @uses $_SERVER 8976 * @param string $extra Extra string to append to the end of the code 8977 * @return string 8978 */ 8979 function make_unique_id_code($extra = '') { 8980 8981 $hostname = 'unknownhost'; 8982 if (!empty($_SERVER['HTTP_HOST'])) { 8983 $hostname = $_SERVER['HTTP_HOST']; 8984 } else if (!empty($_ENV['HTTP_HOST'])) { 8985 $hostname = $_ENV['HTTP_HOST']; 8986 } else if (!empty($_SERVER['SERVER_NAME'])) { 8987 $hostname = $_SERVER['SERVER_NAME']; 8988 } else if (!empty($_ENV['SERVER_NAME'])) { 8989 $hostname = $_ENV['SERVER_NAME']; 8990 } 8991 8992 $date = gmdate("ymdHis"); 8993 8994 $random = random_string(6); 8995 8996 if ($extra) { 8997 return $hostname .'+'. $date .'+'. $random .'+'. $extra; 8998 } else { 8999 return $hostname .'+'. $date .'+'. $random; 9000 } 9001 } 9002 9003 9004 /** 9005 * Function to check the passed address is within the passed subnet 9006 * 9007 * The parameter is a comma separated string of subnet definitions. 9008 * Subnet strings can be in one of three formats: 9009 * 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn (number of bits in net mask) 9010 * 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) 9011 * 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. (incomplete address, a bit non-technical ;-) 9012 * Code for type 1 modified from user posted comments by mediator at 9013 * {@link http://au.php.net/manual/en/function.ip2long.php} 9014 * 9015 * @param string $addr The address you are checking 9016 * @param string $subnetstr The string of subnet addresses 9017 * @param bool $checkallzeros The state to whether check for 0.0.0.0 9018 * @return bool 9019 */ 9020 function address_in_subnet($addr, $subnetstr, $checkallzeros = false) { 9021 9022 if ($addr == '0.0.0.0' && !$checkallzeros) { 9023 return false; 9024 } 9025 $subnets = explode(',', $subnetstr); 9026 $found = false; 9027 $addr = trim($addr); 9028 $addr = cleanremoteaddr($addr, false); // Normalise. 9029 if ($addr === null) { 9030 return false; 9031 } 9032 $addrparts = explode(':', $addr); 9033 9034 $ipv6 = strpos($addr, ':'); 9035 9036 foreach ($subnets as $subnet) { 9037 $subnet = trim($subnet); 9038 if ($subnet === '') { 9039 continue; 9040 } 9041 9042 if (strpos($subnet, '/') !== false) { 9043 // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn. 9044 list($ip, $mask) = explode('/', $subnet); 9045 $mask = trim($mask); 9046 if (!is_number($mask)) { 9047 continue; // Incorect mask number, eh? 9048 } 9049 $ip = cleanremoteaddr($ip, false); // Normalise. 9050 if ($ip === null) { 9051 continue; 9052 } 9053 if (strpos($ip, ':') !== false) { 9054 // IPv6. 9055 if (!$ipv6) { 9056 continue; 9057 } 9058 if ($mask > 128 or $mask < 0) { 9059 continue; // Nonsense. 9060 } 9061 if ($mask == 0) { 9062 return true; // Any address. 9063 } 9064 if ($mask == 128) { 9065 if ($ip === $addr) { 9066 return true; 9067 } 9068 continue; 9069 } 9070 $ipparts = explode(':', $ip); 9071 $modulo = $mask % 16; 9072 $ipnet = array_slice($ipparts, 0, ($mask-$modulo)/16); 9073 $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16); 9074 if (implode(':', $ipnet) === implode(':', $addrnet)) { 9075 if ($modulo == 0) { 9076 return true; 9077 } 9078 $pos = ($mask-$modulo)/16; 9079 $ipnet = hexdec($ipparts[$pos]); 9080 $addrnet = hexdec($addrparts[$pos]); 9081 $mask = 0xffff << (16 - $modulo); 9082 if (($addrnet & $mask) == ($ipnet & $mask)) { 9083 return true; 9084 } 9085 } 9086 9087 } else { 9088 // IPv4. 9089 if ($ipv6) { 9090 continue; 9091 } 9092 if ($mask > 32 or $mask < 0) { 9093 continue; // Nonsense. 9094 } 9095 if ($mask == 0) { 9096 return true; 9097 } 9098 if ($mask == 32) { 9099 if ($ip === $addr) { 9100 return true; 9101 } 9102 continue; 9103 } 9104 $mask = 0xffffffff << (32 - $mask); 9105 if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) { 9106 return true; 9107 } 9108 } 9109 9110 } else if (strpos($subnet, '-') !== false) { 9111 // 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. 9112 $parts = explode('-', $subnet); 9113 if (count($parts) != 2) { 9114 continue; 9115 } 9116 9117 if (strpos($subnet, ':') !== false) { 9118 // IPv6. 9119 if (!$ipv6) { 9120 continue; 9121 } 9122 $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise. 9123 if ($ipstart === null) { 9124 continue; 9125 } 9126 $ipparts = explode(':', $ipstart); 9127 $start = hexdec(array_pop($ipparts)); 9128 $ipparts[] = trim($parts[1]); 9129 $ipend = cleanremoteaddr(implode(':', $ipparts), false); // Normalise. 9130 if ($ipend === null) { 9131 continue; 9132 } 9133 $ipparts[7] = ''; 9134 $ipnet = implode(':', $ipparts); 9135 if (strpos($addr, $ipnet) !== 0) { 9136 continue; 9137 } 9138 $ipparts = explode(':', $ipend); 9139 $end = hexdec($ipparts[7]); 9140 9141 $addrend = hexdec($addrparts[7]); 9142 9143 if (($addrend >= $start) and ($addrend <= $end)) { 9144 return true; 9145 } 9146 9147 } else { 9148 // IPv4. 9149 if ($ipv6) { 9150 continue; 9151 } 9152 $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise. 9153 if ($ipstart === null) { 9154 continue; 9155 } 9156 $ipparts = explode('.', $ipstart); 9157 $ipparts[3] = trim($parts[1]); 9158 $ipend = cleanremoteaddr(implode('.', $ipparts), false); // Normalise. 9159 if ($ipend === null) { 9160 continue; 9161 } 9162 9163 if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) { 9164 return true; 9165 } 9166 } 9167 9168 } else { 9169 // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. 9170 if (strpos($subnet, ':') !== false) { 9171 // IPv6. 9172 if (!$ipv6) { 9173 continue; 9174 } 9175 $parts = explode(':', $subnet); 9176 $count = count($parts); 9177 if ($parts[$count-1] === '') { 9178 unset($parts[$count-1]); // Trim trailing :'s. 9179 $count--; 9180 $subnet = implode('.', $parts); 9181 } 9182 $isip = cleanremoteaddr($subnet, false); // Normalise. 9183 if ($isip !== null) { 9184 if ($isip === $addr) { 9185 return true; 9186 } 9187 continue; 9188 } else if ($count > 8) { 9189 continue; 9190 } 9191 $zeros = array_fill(0, 8-$count, '0'); 9192 $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16); 9193 if (address_in_subnet($addr, $subnet)) { 9194 return true; 9195 } 9196 9197 } else { 9198 // IPv4. 9199 if ($ipv6) { 9200 continue; 9201 } 9202 $parts = explode('.', $subnet); 9203 $count = count($parts); 9204 if ($parts[$count-1] === '') { 9205 unset($parts[$count-1]); // Trim trailing . 9206 $count--; 9207 $subnet = implode('.', $parts); 9208 } 9209 if ($count == 4) { 9210 $subnet = cleanremoteaddr($subnet, false); // Normalise. 9211 if ($subnet === $addr) { 9212 return true; 9213 } 9214 continue; 9215 } else if ($count > 4) { 9216 continue; 9217 } 9218 $zeros = array_fill(0, 4-$count, '0'); 9219 $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8); 9220 if (address_in_subnet($addr, $subnet)) { 9221 return true; 9222 } 9223 } 9224 } 9225 } 9226 9227 return false; 9228 } 9229 9230 /** 9231 * For outputting debugging info 9232 * 9233 * @param string $string The string to write 9234 * @param string $eol The end of line char(s) to use 9235 * @param string $sleep Period to make the application sleep 9236 * This ensures any messages have time to display before redirect 9237 */ 9238 function mtrace($string, $eol="\n", $sleep=0) { 9239 global $CFG; 9240 9241 if (isset($CFG->mtrace_wrapper) && function_exists($CFG->mtrace_wrapper)) { 9242 $fn = $CFG->mtrace_wrapper; 9243 $fn($string, $eol); 9244 return; 9245 } else if (defined('STDOUT') && !PHPUNIT_TEST && !defined('BEHAT_TEST')) { 9246 // We must explicitly call the add_line function here. 9247 // Uses of fwrite to STDOUT are not picked up by ob_start. 9248 if ($output = \core\task\logmanager::add_line("{$string}{$eol}")) { 9249 fwrite(STDOUT, $output); 9250 } 9251 } else { 9252 echo $string . $eol; 9253 } 9254 9255 // Flush again. 9256 flush(); 9257 9258 // Delay to keep message on user's screen in case of subsequent redirect. 9259 if ($sleep) { 9260 sleep($sleep); 9261 } 9262 } 9263 9264 /** 9265 * Replace 1 or more slashes or backslashes to 1 slash 9266 * 9267 * @param string $path The path to strip 9268 * @return string the path with double slashes removed 9269 */ 9270 function cleardoubleslashes ($path) { 9271 return preg_replace('/(\/|\\\){1,}/', '/', $path); 9272 } 9273 9274 /** 9275 * Is the current ip in a given list? 9276 * 9277 * @param string $list 9278 * @return bool 9279 */ 9280 function remoteip_in_list($list) { 9281 $clientip = getremoteaddr(null); 9282 9283 if (!$clientip) { 9284 // Ensure access on cli. 9285 return true; 9286 } 9287 return \core\ip_utils::is_ip_in_subnet_list($clientip, $list); 9288 } 9289 9290 /** 9291 * Returns most reliable client address 9292 * 9293 * @param string $default If an address can't be determined, then return this 9294 * @return string The remote IP address 9295 */ 9296 function getremoteaddr($default='0.0.0.0') { 9297 global $CFG; 9298 9299 if (!isset($CFG->getremoteaddrconf)) { 9300 // This will happen, for example, before just after the upgrade, as the 9301 // user is redirected to the admin screen. 9302 $variablestoskip = GETREMOTEADDR_SKIP_DEFAULT; 9303 } else { 9304 $variablestoskip = $CFG->getremoteaddrconf; 9305 } 9306 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) { 9307 if (!empty($_SERVER['HTTP_CLIENT_IP'])) { 9308 $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']); 9309 return $address ? $address : $default; 9310 } 9311 } 9312 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) { 9313 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { 9314 $forwardedaddresses = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']); 9315 9316 $forwardedaddresses = array_filter($forwardedaddresses, function($ip) { 9317 global $CFG; 9318 return !\core\ip_utils::is_ip_in_subnet_list($ip, $CFG->reverseproxyignore ?? '', ','); 9319 }); 9320 9321 // Multiple proxies can append values to this header including an 9322 // untrusted original request header so we must only trust the last ip. 9323 $address = end($forwardedaddresses); 9324 9325 if (substr_count($address, ":") > 1) { 9326 // Remove port and brackets from IPv6. 9327 if (preg_match("/\[(.*)\]:/", $address, $matches)) { 9328 $address = $matches[1]; 9329 } 9330 } else { 9331 // Remove port from IPv4. 9332 if (substr_count($address, ":") == 1) { 9333 $parts = explode(":", $address); 9334 $address = $parts[0]; 9335 } 9336 } 9337 9338 $address = cleanremoteaddr($address); 9339 return $address ? $address : $default; 9340 } 9341 } 9342 if (!empty($_SERVER['REMOTE_ADDR'])) { 9343 $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']); 9344 return $address ? $address : $default; 9345 } else { 9346 return $default; 9347 } 9348 } 9349 9350 /** 9351 * Cleans an ip address. Internal addresses are now allowed. 9352 * (Originally local addresses were not allowed.) 9353 * 9354 * @param string $addr IPv4 or IPv6 address 9355 * @param bool $compress use IPv6 address compression 9356 * @return string normalised ip address string, null if error 9357 */ 9358 function cleanremoteaddr($addr, $compress=false) { 9359 $addr = trim($addr); 9360 9361 if (strpos($addr, ':') !== false) { 9362 // Can be only IPv6. 9363 $parts = explode(':', $addr); 9364 $count = count($parts); 9365 9366 if (strpos($parts[$count-1], '.') !== false) { 9367 // Legacy ipv4 notation. 9368 $last = array_pop($parts); 9369 $ipv4 = cleanremoteaddr($last, true); 9370 if ($ipv4 === null) { 9371 return null; 9372 } 9373 $bits = explode('.', $ipv4); 9374 $parts[] = dechex($bits[0]).dechex($bits[1]); 9375 $parts[] = dechex($bits[2]).dechex($bits[3]); 9376 $count = count($parts); 9377 $addr = implode(':', $parts); 9378 } 9379 9380 if ($count < 3 or $count > 8) { 9381 return null; // Severly malformed. 9382 } 9383 9384 if ($count != 8) { 9385 if (strpos($addr, '::') === false) { 9386 return null; // Malformed. 9387 } 9388 // Uncompress. 9389 $insertat = array_search('', $parts, true); 9390 $missing = array_fill(0, 1 + 8 - $count, '0'); 9391 array_splice($parts, $insertat, 1, $missing); 9392 foreach ($parts as $key => $part) { 9393 if ($part === '') { 9394 $parts[$key] = '0'; 9395 } 9396 } 9397 } 9398 9399 $adr = implode(':', $parts); 9400 if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) { 9401 return null; // Incorrect format - sorry. 9402 } 9403 9404 // Normalise 0s and case. 9405 $parts = array_map('hexdec', $parts); 9406 $parts = array_map('dechex', $parts); 9407 9408 $result = implode(':', $parts); 9409 9410 if (!$compress) { 9411 return $result; 9412 } 9413 9414 if ($result === '0:0:0:0:0:0:0:0') { 9415 return '::'; // All addresses. 9416 } 9417 9418 $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1); 9419 if ($compressed !== $result) { 9420 return $compressed; 9421 } 9422 9423 $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1); 9424 if ($compressed !== $result) { 9425 return $compressed; 9426 } 9427 9428 $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1); 9429 if ($compressed !== $result) { 9430 return $compressed; 9431 } 9432 9433 return $result; 9434 } 9435 9436 // First get all things that look like IPv4 addresses. 9437 $parts = array(); 9438 if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) { 9439 return null; 9440 } 9441 unset($parts[0]); 9442 9443 foreach ($parts as $key => $match) { 9444 if ($match > 255) { 9445 return null; 9446 } 9447 $parts[$key] = (int)$match; // Normalise 0s. 9448 } 9449 9450 return implode('.', $parts); 9451 } 9452 9453 9454 /** 9455 * Is IP address a public address? 9456 * 9457 * @param string $ip The ip to check 9458 * @return bool true if the ip is public 9459 */ 9460 function ip_is_public($ip) { 9461 return (bool) filter_var($ip, FILTER_VALIDATE_IP, (FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)); 9462 } 9463 9464 /** 9465 * This function will make a complete copy of anything it's given, 9466 * regardless of whether it's an object or not. 9467 * 9468 * @param mixed $thing Something you want cloned 9469 * @return mixed What ever it is you passed it 9470 */ 9471 function fullclone($thing) { 9472 return unserialize(serialize($thing)); 9473 } 9474 9475 /** 9476 * Used to make sure that $min <= $value <= $max 9477 * 9478 * Make sure that value is between min, and max 9479 * 9480 * @param int $min The minimum value 9481 * @param int $value The value to check 9482 * @param int $max The maximum value 9483 * @return int 9484 */ 9485 function bounded_number($min, $value, $max) { 9486 if ($value < $min) { 9487 return $min; 9488 } 9489 if ($value > $max) { 9490 return $max; 9491 } 9492 return $value; 9493 } 9494 9495 /** 9496 * Check if there is a nested array within the passed array 9497 * 9498 * @param array $array 9499 * @return bool true if there is a nested array false otherwise 9500 */ 9501 function array_is_nested($array) { 9502 foreach ($array as $value) { 9503 if (is_array($value)) { 9504 return true; 9505 } 9506 } 9507 return false; 9508 } 9509 9510 /** 9511 * get_performance_info() pairs up with init_performance_info() 9512 * loaded in setup.php. Returns an array with 'html' and 'txt' 9513 * values ready for use, and each of the individual stats provided 9514 * separately as well. 9515 * 9516 * @return array 9517 */ 9518 function get_performance_info() { 9519 global $CFG, $PERF, $DB, $PAGE; 9520 9521 $info = array(); 9522 $info['txt'] = me() . ' '; // Holds log-friendly representation. 9523 9524 $info['html'] = ''; 9525 if (!empty($CFG->themedesignermode)) { 9526 // Attempt to avoid devs debugging peformance issues, when its caused by css building and so on. 9527 $info['html'] .= '<p><strong>Warning: Theme designer mode is enabled.</strong></p>'; 9528 } 9529 $info['html'] .= '<ul class="list-unstyled row mx-md-0">'; // Holds userfriendly HTML representation. 9530 9531 $info['realtime'] = microtime_diff($PERF->starttime, microtime()); 9532 9533 $info['html'] .= '<li class="timeused col-sm-4">'.$info['realtime'].' secs</li> '; 9534 $info['txt'] .= 'time: '.$info['realtime'].'s '; 9535 9536 // GET/POST (or NULL if $_SERVER['REQUEST_METHOD'] is undefined) is useful for txt logged information. 9537 $info['txt'] .= 'method: ' . ($_SERVER['REQUEST_METHOD'] ?? "NULL") . ' '; 9538 9539 if (function_exists('memory_get_usage')) { 9540 $info['memory_total'] = memory_get_usage(); 9541 $info['memory_growth'] = memory_get_usage() - $PERF->startmemory; 9542 $info['html'] .= '<li class="memoryused col-sm-4">RAM: '.display_size($info['memory_total']).'</li> '; 9543 $info['txt'] .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '. 9544 $info['memory_growth'].'B ('.display_size($info['memory_growth']).') '; 9545 } 9546 9547 if (function_exists('memory_get_peak_usage')) { 9548 $info['memory_peak'] = memory_get_peak_usage(); 9549 $info['html'] .= '<li class="memoryused col-sm-4">RAM peak: '.display_size($info['memory_peak']).'</li> '; 9550 $info['txt'] .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') '; 9551 } 9552 9553 $info['html'] .= '</ul><ul class="list-unstyled row mx-md-0">'; 9554 $inc = get_included_files(); 9555 $info['includecount'] = count($inc); 9556 $info['html'] .= '<li class="included col-sm-4">Included '.$info['includecount'].' files</li> '; 9557 $info['txt'] .= 'includecount: '.$info['includecount'].' '; 9558 9559 if (!empty($CFG->early_install_lang) or empty($PAGE)) { 9560 // We can not track more performance before installation or before PAGE init, sorry. 9561 return $info; 9562 } 9563 9564 $filtermanager = filter_manager::instance(); 9565 if (method_exists($filtermanager, 'get_performance_summary')) { 9566 list($filterinfo, $nicenames) = $filtermanager->get_performance_summary(); 9567 $info = array_merge($filterinfo, $info); 9568 foreach ($filterinfo as $key => $value) { 9569 $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> "; 9570 $info['txt'] .= "$key: $value "; 9571 } 9572 } 9573 9574 $stringmanager = get_string_manager(); 9575 if (method_exists($stringmanager, 'get_performance_summary')) { 9576 list($filterinfo, $nicenames) = $stringmanager->get_performance_summary(); 9577 $info = array_merge($filterinfo, $info); 9578 foreach ($filterinfo as $key => $value) { 9579 $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> "; 9580 $info['txt'] .= "$key: $value "; 9581 } 9582 } 9583 9584 if (!empty($PERF->logwrites)) { 9585 $info['logwrites'] = $PERF->logwrites; 9586 $info['html'] .= '<li class="logwrites col-sm-4">Log DB writes '.$info['logwrites'].'</li> '; 9587 $info['txt'] .= 'logwrites: '.$info['logwrites'].' '; 9588 } 9589 9590 $info['dbqueries'] = $DB->perf_get_reads().'/'.($DB->perf_get_writes() - $PERF->logwrites); 9591 $info['html'] .= '<li class="dbqueries col-sm-4">DB reads/writes: '.$info['dbqueries'].'</li> '; 9592 $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' '; 9593 9594 if ($DB->want_read_slave()) { 9595 $info['dbreads_slave'] = $DB->perf_get_reads_slave(); 9596 $info['html'] .= '<li class="dbqueries col-sm-4">DB reads from slave: '.$info['dbreads_slave'].'</li> '; 9597 $info['txt'] .= 'db reads from slave: '.$info['dbreads_slave'].' '; 9598 } 9599 9600 $info['dbtime'] = round($DB->perf_get_queries_time(), 5); 9601 $info['html'] .= '<li class="dbtime col-sm-4">DB queries time: '.$info['dbtime'].' secs</li> '; 9602 $info['txt'] .= 'db queries time: ' . $info['dbtime'] . 's '; 9603 9604 if (function_exists('posix_times')) { 9605 $ptimes = posix_times(); 9606 if (is_array($ptimes)) { 9607 foreach ($ptimes as $key => $val) { 9608 $info[$key] = $ptimes[$key] - $PERF->startposixtimes[$key]; 9609 } 9610 $info['html'] .= "<li class=\"posixtimes col-sm-4\">ticks: $info[ticks] user: $info[utime]"; 9611 $info['html'] .= "sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</li> "; 9612 $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] "; 9613 } 9614 } 9615 9616 // Grab the load average for the last minute. 9617 // /proc will only work under some linux configurations 9618 // while uptime is there under MacOSX/Darwin and other unices. 9619 if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) { 9620 list($serverload) = explode(' ', $loadavg[0]); 9621 unset($loadavg); 9622 } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) { 9623 if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) { 9624 $serverload = $matches[1]; 9625 } else { 9626 trigger_error('Could not parse uptime output!'); 9627 } 9628 } 9629 if (!empty($serverload)) { 9630 $info['serverload'] = $serverload; 9631 $info['html'] .= '<li class="serverload col-sm-4">Load average: '.$info['serverload'].'</li> '; 9632 $info['txt'] .= "serverload: {$info['serverload']} "; 9633 } 9634 9635 // Display size of session if session started. 9636 if ($si = \core\session\manager::get_performance_info()) { 9637 $info['sessionsize'] = $si['size']; 9638 $info['html'] .= "<li class=\"serverload col-sm-4\">" . $si['html'] . "</li>"; 9639 $info['txt'] .= $si['txt']; 9640 } 9641 9642 $info['html'] .= '</ul>'; 9643 $html = ''; 9644 if ($stats = cache_helper::get_stats()) { 9645 9646 $table = new html_table(); 9647 $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered'; 9648 $table->head = ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S']; 9649 $table->data = []; 9650 $table->align = ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right']; 9651 9652 $text = 'Caches used (hits/misses/sets): '; 9653 $hits = 0; 9654 $misses = 0; 9655 $sets = 0; 9656 $maxstores = 0; 9657 9658 // We want to align static caches into their own column. 9659 $hasstatic = false; 9660 foreach ($stats as $definition => $details) { 9661 $numstores = count($details['stores']); 9662 $first = key($details['stores']); 9663 if ($first !== cache_store::STATIC_ACCEL) { 9664 $numstores++; // Add a blank space for the missing static store. 9665 } 9666 $maxstores = max($maxstores, $numstores); 9667 } 9668 9669 $storec = 0; 9670 9671 while ($storec++ < ($maxstores - 2)) { 9672 if ($storec == ($maxstores - 2)) { 9673 $table->head[] = get_string('mappingfinal', 'cache'); 9674 } else { 9675 $table->head[] = "Store $storec"; 9676 } 9677 $table->align[] = 'left'; 9678 $table->align[] = 'right'; 9679 $table->align[] = 'right'; 9680 $table->align[] = 'right'; 9681 $table->head[] = 'H'; 9682 $table->head[] = 'M'; 9683 $table->head[] = 'S'; 9684 } 9685 9686 ksort($stats); 9687 9688 foreach ($stats as $definition => $details) { 9689 switch ($details['mode']) { 9690 case cache_store::MODE_APPLICATION: 9691 $modeclass = 'application'; 9692 $mode = ' <span title="application cache">App</span>'; 9693 break; 9694 case cache_store::MODE_SESSION: 9695 $modeclass = 'session'; 9696 $mode = ' <span title="session cache">Ses</span>'; 9697 break; 9698 case cache_store::MODE_REQUEST: 9699 $modeclass = 'request'; 9700 $mode = ' <span title="request cache">Req</span>'; 9701 break; 9702 } 9703 $row = [$mode, $definition]; 9704 9705 $text .= "$definition {"; 9706 9707 $storec = 0; 9708 foreach ($details['stores'] as $store => $data) { 9709 9710 if ($storec == 0 && $store !== cache_store::STATIC_ACCEL) { 9711 $row[] = ''; 9712 $row[] = ''; 9713 $row[] = ''; 9714 $storec++; 9715 } 9716 9717 $hits += $data['hits']; 9718 $misses += $data['misses']; 9719 $sets += $data['sets']; 9720 if ($data['hits'] == 0 and $data['misses'] > 0) { 9721 $cachestoreclass = 'nohits bg-danger'; 9722 } else if ($data['hits'] < $data['misses']) { 9723 $cachestoreclass = 'lowhits bg-warning text-dark'; 9724 } else { 9725 $cachestoreclass = 'hihits'; 9726 } 9727 $text .= "$store($data[hits]/$data[misses]/$data[sets]) "; 9728 $cell = new html_table_cell($store); 9729 $cell->attributes = ['class' => $cachestoreclass]; 9730 $row[] = $cell; 9731 $cell = new html_table_cell($data['hits']); 9732 $cell->attributes = ['class' => $cachestoreclass]; 9733 $row[] = $cell; 9734 $cell = new html_table_cell($data['misses']); 9735 $cell->attributes = ['class' => $cachestoreclass]; 9736 $row[] = $cell; 9737 9738 if ($store !== cache_store::STATIC_ACCEL) { 9739 // The static cache is never set. 9740 $cell = new html_table_cell($data['sets']); 9741 $cell->attributes = ['class' => $cachestoreclass]; 9742 $row[] = $cell; 9743 } 9744 $storec++; 9745 } 9746 while ($storec++ < $maxstores) { 9747 $row[] = ''; 9748 $row[] = ''; 9749 $row[] = ''; 9750 $row[] = ''; 9751 } 9752 $text .= '} '; 9753 9754 $table->data[] = $row; 9755 } 9756 9757 $html .= html_writer::table($table); 9758 9759 // Now lets also show sub totals for each cache store. 9760 $storetotals = []; 9761 $storetotal = ['hits' => 0, 'misses' => 0, 'sets' => 0]; 9762 foreach ($stats as $definition => $details) { 9763 foreach ($details['stores'] as $store => $data) { 9764 if (!array_key_exists($store, $storetotals)) { 9765 $storetotals[$store] = ['hits' => 0, 'misses' => 0, 'sets' => 0]; 9766 } 9767 $storetotals[$store]['class'] = $data['class']; 9768 $storetotals[$store]['hits'] += $data['hits']; 9769 $storetotals[$store]['misses'] += $data['misses']; 9770 $storetotals[$store]['sets'] += $data['sets']; 9771 $storetotal['hits'] += $data['hits']; 9772 $storetotal['misses'] += $data['misses']; 9773 $storetotal['sets'] += $data['sets']; 9774 } 9775 } 9776 9777 $table = new html_table(); 9778 $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered'; 9779 $table->head = [get_string('storename', 'cache'), get_string('type_cachestore', 'plugin'), 'H', 'M', 'S']; 9780 $table->data = []; 9781 $table->align = ['left', 'left', 'right', 'right', 'right']; 9782 9783 ksort($storetotals); 9784 9785 foreach ($storetotals as $store => $data) { 9786 $row = []; 9787 if ($data['hits'] == 0 and $data['misses'] > 0) { 9788 $cachestoreclass = 'nohits bg-danger'; 9789 } else if ($data['hits'] < $data['misses']) { 9790 $cachestoreclass = 'lowhits bg-warning text-dark'; 9791 } else { 9792 $cachestoreclass = 'hihits'; 9793 } 9794 $cell = new html_table_cell($store); 9795 $cell->attributes = ['class' => $cachestoreclass]; 9796 $row[] = $cell; 9797 $cell = new html_table_cell($data['class']); 9798 $cell->attributes = ['class' => $cachestoreclass]; 9799 $row[] = $cell; 9800 $cell = new html_table_cell($data['hits']); 9801 $cell->attributes = ['class' => $cachestoreclass]; 9802 $row[] = $cell; 9803 $cell = new html_table_cell($data['misses']); 9804 $cell->attributes = ['class' => $cachestoreclass]; 9805 $row[] = $cell; 9806 $cell = new html_table_cell($data['sets']); 9807 $cell->attributes = ['class' => $cachestoreclass]; 9808 $row[] = $cell; 9809 $table->data[] = $row; 9810 } 9811 $row = [ 9812 get_string('total'), 9813 '', 9814 $storetotal['hits'], 9815 $storetotal['misses'], 9816 $storetotal['sets'], 9817 ]; 9818 $table->data[] = $row; 9819 9820 $html .= html_writer::table($table); 9821 9822 $info['cachesused'] = "$hits / $misses / $sets"; 9823 $info['html'] .= $html; 9824 $info['txt'] .= $text.'. '; 9825 } else { 9826 $info['cachesused'] = '0 / 0 / 0'; 9827 $info['html'] .= '<div class="cachesused">Caches used (hits/misses/sets): 0/0/0</div>'; 9828 $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 '; 9829 } 9830 9831 $info['html'] = '<div class="performanceinfo siteinfo container-fluid px-md-0 overflow-auto mt-3">'.$info['html'].'</div>'; 9832 return $info; 9833 } 9834 9835 /** 9836 * Delete directory or only its content 9837 * 9838 * @param string $dir directory path 9839 * @param bool $contentonly 9840 * @return bool success, true also if dir does not exist 9841 */ 9842 function remove_dir($dir, $contentonly=false) { 9843 if (!file_exists($dir)) { 9844 // Nothing to do. 9845 return true; 9846 } 9847 if (!$handle = opendir($dir)) { 9848 return false; 9849 } 9850 $result = true; 9851 while (false!==($item = readdir($handle))) { 9852 if ($item != '.' && $item != '..') { 9853 if (is_dir($dir.'/'.$item)) { 9854 $result = remove_dir($dir.'/'.$item) && $result; 9855 } else { 9856 $result = unlink($dir.'/'.$item) && $result; 9857 } 9858 } 9859 } 9860 closedir($handle); 9861 if ($contentonly) { 9862 clearstatcache(); // Make sure file stat cache is properly invalidated. 9863 return $result; 9864 } 9865 $result = rmdir($dir); // If anything left the result will be false, no need for && $result. 9866 clearstatcache(); // Make sure file stat cache is properly invalidated. 9867 return $result; 9868 } 9869 9870 /** 9871 * Detect if an object or a class contains a given property 9872 * will take an actual object or the name of a class 9873 * 9874 * @param mix $obj Name of class or real object to test 9875 * @param string $property name of property to find 9876 * @return bool true if property exists 9877 */ 9878 function object_property_exists( $obj, $property ) { 9879 if (is_string( $obj )) { 9880 $properties = get_class_vars( $obj ); 9881 } else { 9882 $properties = get_object_vars( $obj ); 9883 } 9884 return array_key_exists( $property, $properties ); 9885 } 9886 9887 /** 9888 * Converts an object into an associative array 9889 * 9890 * This function converts an object into an associative array by iterating 9891 * over its public properties. Because this function uses the foreach 9892 * construct, Iterators are respected. It works recursively on arrays of objects. 9893 * Arrays and simple values are returned as is. 9894 * 9895 * If class has magic properties, it can implement IteratorAggregate 9896 * and return all available properties in getIterator() 9897 * 9898 * @param mixed $var 9899 * @return array 9900 */ 9901 function convert_to_array($var) { 9902 $result = array(); 9903 9904 // Loop over elements/properties. 9905 foreach ($var as $key => $value) { 9906 // Recursively convert objects. 9907 if (is_object($value) || is_array($value)) { 9908 $result[$key] = convert_to_array($value); 9909 } else { 9910 // Simple values are untouched. 9911 $result[$key] = $value; 9912 } 9913 } 9914 return $result; 9915 } 9916 9917 /** 9918 * Detect a custom script replacement in the data directory that will 9919 * replace an existing moodle script 9920 * 9921 * @return string|bool full path name if a custom script exists, false if no custom script exists 9922 */ 9923 function custom_script_path() { 9924 global $CFG, $SCRIPT; 9925 9926 if ($SCRIPT === null) { 9927 // Probably some weird external script. 9928 return false; 9929 } 9930 9931 $scriptpath = $CFG->customscripts . $SCRIPT; 9932 9933 // Check the custom script exists. 9934 if (file_exists($scriptpath) and is_file($scriptpath)) { 9935 return $scriptpath; 9936 } else { 9937 return false; 9938 } 9939 } 9940 9941 /** 9942 * Returns whether or not the user object is a remote MNET user. This function 9943 * is in moodlelib because it does not rely on loading any of the MNET code. 9944 * 9945 * @param object $user A valid user object 9946 * @return bool True if the user is from a remote Moodle. 9947 */ 9948 function is_mnet_remote_user($user) { 9949 global $CFG; 9950 9951 if (!isset($CFG->mnet_localhost_id)) { 9952 include_once($CFG->dirroot . '/mnet/lib.php'); 9953 $env = new mnet_environment(); 9954 $env->init(); 9955 unset($env); 9956 } 9957 9958 return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id); 9959 } 9960 9961 /** 9962 * This function will search for browser prefereed languages, setting Moodle 9963 * to use the best one available if $SESSION->lang is undefined 9964 */ 9965 function setup_lang_from_browser() { 9966 global $CFG, $SESSION, $USER; 9967 9968 if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) { 9969 // Lang is defined in session or user profile, nothing to do. 9970 return; 9971 } 9972 9973 if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do. 9974 return; 9975 } 9976 9977 // Extract and clean langs from headers. 9978 $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE']; 9979 $rawlangs = str_replace('-', '_', $rawlangs); // We are using underscores. 9980 $rawlangs = explode(',', $rawlangs); // Convert to array. 9981 $langs = array(); 9982 9983 $order = 1.0; 9984 foreach ($rawlangs as $lang) { 9985 if (strpos($lang, ';') === false) { 9986 $langs[(string)$order] = $lang; 9987 $order = $order-0.01; 9988 } else { 9989 $parts = explode(';', $lang); 9990 $pos = strpos($parts[1], '='); 9991 $langs[substr($parts[1], $pos+1)] = $parts[0]; 9992 } 9993 } 9994 krsort($langs, SORT_NUMERIC); 9995 9996 // Look for such langs under standard locations. 9997 foreach ($langs as $lang) { 9998 // Clean it properly for include. 9999 $lang = strtolower(clean_param($lang, PARAM_SAFEDIR)); 10000 if (get_string_manager()->translation_exists($lang, false)) { 10001 // Lang exists, set it in session. 10002 $SESSION->lang = $lang; 10003 // We have finished. Go out. 10004 break; 10005 } 10006 } 10007 return; 10008 } 10009 10010 /** 10011 * Check if $url matches anything in proxybypass list 10012 * 10013 * Any errors just result in the proxy being used (least bad) 10014 * 10015 * @param string $url url to check 10016 * @return boolean true if we should bypass the proxy 10017 */ 10018 function is_proxybypass( $url ) { 10019 global $CFG; 10020 10021 // Sanity check. 10022 if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) { 10023 return false; 10024 } 10025 10026 // Get the host part out of the url. 10027 if (!$host = parse_url( $url, PHP_URL_HOST )) { 10028 return false; 10029 } 10030 10031 // Get the possible bypass hosts into an array. 10032 $matches = explode( ',', $CFG->proxybypass ); 10033 10034 // Check for a exact match on the IP or in the domains. 10035 $isdomaininallowedlist = \core\ip_utils::is_domain_in_allowed_list($host, $matches); 10036 $isipinsubnetlist = \core\ip_utils::is_ip_in_subnet_list($host, $CFG->proxybypass, ','); 10037 10038 if ($isdomaininallowedlist || $isipinsubnetlist) { 10039 return true; 10040 } 10041 10042 // Nothing matched. 10043 return false; 10044 } 10045 10046 /** 10047 * Check if the passed navigation is of the new style 10048 * 10049 * @param mixed $navigation 10050 * @return bool true for yes false for no 10051 */ 10052 function is_newnav($navigation) { 10053 if (is_array($navigation) && !empty($navigation['newnav'])) { 10054 return true; 10055 } else { 10056 return false; 10057 } 10058 } 10059 10060 /** 10061 * Checks whether the given variable name is defined as a variable within the given object. 10062 * 10063 * This will NOT work with stdClass objects, which have no class variables. 10064 * 10065 * @param string $var The variable name 10066 * @param object $object The object to check 10067 * @return boolean 10068 */ 10069 function in_object_vars($var, $object) { 10070 $classvars = get_class_vars(get_class($object)); 10071 $classvars = array_keys($classvars); 10072 return in_array($var, $classvars); 10073 } 10074 10075 /** 10076 * Returns an array without repeated objects. 10077 * This function is similar to array_unique, but for arrays that have objects as values 10078 * 10079 * @param array $array 10080 * @param bool $keepkeyassoc 10081 * @return array 10082 */ 10083 function object_array_unique($array, $keepkeyassoc = true) { 10084 $duplicatekeys = array(); 10085 $tmp = array(); 10086 10087 foreach ($array as $key => $val) { 10088 // Convert objects to arrays, in_array() does not support objects. 10089 if (is_object($val)) { 10090 $val = (array)$val; 10091 } 10092 10093 if (!in_array($val, $tmp)) { 10094 $tmp[] = $val; 10095 } else { 10096 $duplicatekeys[] = $key; 10097 } 10098 } 10099 10100 foreach ($duplicatekeys as $key) { 10101 unset($array[$key]); 10102 } 10103 10104 return $keepkeyassoc ? $array : array_values($array); 10105 } 10106 10107 /** 10108 * Is a userid the primary administrator? 10109 * 10110 * @param int $userid int id of user to check 10111 * @return boolean 10112 */ 10113 function is_primary_admin($userid) { 10114 $primaryadmin = get_admin(); 10115 10116 if ($userid == $primaryadmin->id) { 10117 return true; 10118 } else { 10119 return false; 10120 } 10121 } 10122 10123 /** 10124 * Returns the site identifier 10125 * 10126 * @return string $CFG->siteidentifier, first making sure it is properly initialised. 10127 */ 10128 function get_site_identifier() { 10129 global $CFG; 10130 // Check to see if it is missing. If so, initialise it. 10131 if (empty($CFG->siteidentifier)) { 10132 set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']); 10133 } 10134 // Return it. 10135 return $CFG->siteidentifier; 10136 } 10137 10138 /** 10139 * Check whether the given password has no more than the specified 10140 * number of consecutive identical characters. 10141 * 10142 * @param string $password password to be checked against the password policy 10143 * @param integer $maxchars maximum number of consecutive identical characters 10144 * @return bool 10145 */ 10146 function check_consecutive_identical_characters($password, $maxchars) { 10147 10148 if ($maxchars < 1) { 10149 return true; // Zero 0 is to disable this check. 10150 } 10151 if (strlen($password) <= $maxchars) { 10152 return true; // Too short to fail this test. 10153 } 10154 10155 $previouschar = ''; 10156 $consecutivecount = 1; 10157 foreach (str_split($password) as $char) { 10158 if ($char != $previouschar) { 10159 $consecutivecount = 1; 10160 } else { 10161 $consecutivecount++; 10162 if ($consecutivecount > $maxchars) { 10163 return false; // Check failed already. 10164 } 10165 } 10166 10167 $previouschar = $char; 10168 } 10169 10170 return true; 10171 } 10172 10173 /** 10174 * Helper function to do partial function binding. 10175 * so we can use it for preg_replace_callback, for example 10176 * this works with php functions, user functions, static methods and class methods 10177 * it returns you a callback that you can pass on like so: 10178 * 10179 * $callback = partial('somefunction', $arg1, $arg2); 10180 * or 10181 * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2); 10182 * or even 10183 * $obj = new someclass(); 10184 * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2); 10185 * 10186 * and then the arguments that are passed through at calltime are appended to the argument list. 10187 * 10188 * @param mixed $function a php callback 10189 * @param mixed $arg1,... $argv arguments to partially bind with 10190 * @return array Array callback 10191 */ 10192 function partial() { 10193 if (!class_exists('partial')) { 10194 /** 10195 * Used to manage function binding. 10196 * @copyright 2009 Penny Leach 10197 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 10198 */ 10199 class partial{ 10200 /** @var array */ 10201 public $values = array(); 10202 /** @var string The function to call as a callback. */ 10203 public $func; 10204 /** 10205 * Constructor 10206 * @param string $func 10207 * @param array $args 10208 */ 10209 public function __construct($func, $args) { 10210 $this->values = $args; 10211 $this->func = $func; 10212 } 10213 /** 10214 * Calls the callback function. 10215 * @return mixed 10216 */ 10217 public function method() { 10218 $args = func_get_args(); 10219 return call_user_func_array($this->func, array_merge($this->values, $args)); 10220 } 10221 } 10222 } 10223 $args = func_get_args(); 10224 $func = array_shift($args); 10225 $p = new partial($func, $args); 10226 return array($p, 'method'); 10227 } 10228 10229 /** 10230 * helper function to load up and initialise the mnet environment 10231 * this must be called before you use mnet functions. 10232 * 10233 * @return mnet_environment the equivalent of old $MNET global 10234 */ 10235 function get_mnet_environment() { 10236 global $CFG; 10237 require_once($CFG->dirroot . '/mnet/lib.php'); 10238 static $instance = null; 10239 if (empty($instance)) { 10240 $instance = new mnet_environment(); 10241 $instance->init(); 10242 } 10243 return $instance; 10244 } 10245 10246 /** 10247 * during xmlrpc server code execution, any code wishing to access 10248 * information about the remote peer must use this to get it. 10249 * 10250 * @return mnet_remote_client the equivalent of old $MNETREMOTE_CLIENT global 10251 */ 10252 function get_mnet_remote_client() { 10253 if (!defined('MNET_SERVER')) { 10254 debugging(get_string('notinxmlrpcserver', 'mnet')); 10255 return false; 10256 } 10257 global $MNET_REMOTE_CLIENT; 10258 if (isset($MNET_REMOTE_CLIENT)) { 10259 return $MNET_REMOTE_CLIENT; 10260 } 10261 return false; 10262 } 10263 10264 /** 10265 * during the xmlrpc server code execution, this will be called 10266 * to setup the object returned by {@link get_mnet_remote_client} 10267 * 10268 * @param mnet_remote_client $client the client to set up 10269 * @throws moodle_exception 10270 */ 10271 function set_mnet_remote_client($client) { 10272 if (!defined('MNET_SERVER')) { 10273 throw new moodle_exception('notinxmlrpcserver', 'mnet'); 10274 } 10275 global $MNET_REMOTE_CLIENT; 10276 $MNET_REMOTE_CLIENT = $client; 10277 } 10278 10279 /** 10280 * return the jump url for a given remote user 10281 * this is used for rewriting forum post links in emails, etc 10282 * 10283 * @param stdclass $user the user to get the idp url for 10284 */ 10285 function mnet_get_idp_jump_url($user) { 10286 global $CFG; 10287 10288 static $mnetjumps = array(); 10289 if (!array_key_exists($user->mnethostid, $mnetjumps)) { 10290 $idp = mnet_get_peer_host($user->mnethostid); 10291 $idpjumppath = mnet_get_app_jumppath($idp->applicationid); 10292 $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl='; 10293 } 10294 return $mnetjumps[$user->mnethostid]; 10295 } 10296 10297 /** 10298 * Gets the homepage to use for the current user 10299 * 10300 * @return int One of HOMEPAGE_* 10301 */ 10302 function get_home_page() { 10303 global $CFG; 10304 10305 if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) { 10306 if ($CFG->defaulthomepage == HOMEPAGE_MY) { 10307 return HOMEPAGE_MY; 10308 } else { 10309 return (int)get_user_preferences('user_home_page_preference', HOMEPAGE_MY); 10310 } 10311 } 10312 return HOMEPAGE_SITE; 10313 } 10314 10315 /** 10316 * Gets the name of a course to be displayed when showing a list of courses. 10317 * By default this is just $course->fullname but user can configure it. The 10318 * result of this function should be passed through print_string. 10319 * @param stdClass|core_course_list_element $course Moodle course object 10320 * @return string Display name of course (either fullname or short + fullname) 10321 */ 10322 function get_course_display_name_for_list($course) { 10323 global $CFG; 10324 if (!empty($CFG->courselistshortnames)) { 10325 if (!($course instanceof stdClass)) { 10326 $course = (object)convert_to_array($course); 10327 } 10328 return get_string('courseextendednamedisplay', '', $course); 10329 } else { 10330 return $course->fullname; 10331 } 10332 } 10333 10334 /** 10335 * Safe analogue of unserialize() that can only parse arrays 10336 * 10337 * Arrays may contain only integers or strings as both keys and values. Nested arrays are allowed. 10338 * 10339 * @param string $expression 10340 * @return array|bool either parsed array or false if parsing was impossible. 10341 */ 10342 function unserialize_array($expression) { 10343 10344 // Check the expression is an array. 10345 if (!preg_match('/^a:(\d+):/', $expression)) { 10346 return false; 10347 } 10348 10349 $values = (array) unserialize_object($expression); 10350 10351 // Callback that returns true if the given value is an unserialized object, executes recursively. 10352 $invalidvaluecallback = static function($value) use (&$invalidvaluecallback): bool { 10353 if (is_array($value)) { 10354 return (bool) array_filter($value, $invalidvaluecallback); 10355 } 10356 return ($value instanceof stdClass) || ($value instanceof __PHP_Incomplete_Class); 10357 }; 10358 10359 // Iterate over the result to ensure there are no stray objects. 10360 if (array_filter($values, $invalidvaluecallback)) { 10361 return false; 10362 } 10363 10364 return $values; 10365 } 10366 10367 /** 10368 * Safe method for unserializing given input that is expected to contain only a serialized instance of an stdClass object 10369 * 10370 * If any class type other than stdClass is included in the input string, it will not be instantiated and will be cast to an 10371 * stdClass object. The initial cast to array, then back to object is to ensure we are always returning the correct type, 10372 * otherwise we would return an instances of {@see __PHP_Incomplete_class} for malformed strings 10373 * 10374 * @param string $input 10375 * @return stdClass 10376 */ 10377 function unserialize_object(string $input): stdClass { 10378 $instance = (array) unserialize($input, ['allowed_classes' => [stdClass::class]]); 10379 return (object) $instance; 10380 } 10381 10382 /** 10383 * The lang_string class 10384 * 10385 * This special class is used to create an object representation of a string request. 10386 * It is special because processing doesn't occur until the object is first used. 10387 * The class was created especially to aid performance in areas where strings were 10388 * required to be generated but were not necessarily used. 10389 * As an example the admin tree when generated uses over 1500 strings, of which 10390 * normally only 1/3 are ever actually printed at any time. 10391 * The performance advantage is achieved by not actually processing strings that 10392 * arn't being used, as such reducing the processing required for the page. 10393 * 10394 * How to use the lang_string class? 10395 * There are two methods of using the lang_string class, first through the 10396 * forth argument of the get_string function, and secondly directly. 10397 * The following are examples of both. 10398 * 1. Through get_string calls e.g. 10399 * $string = get_string($identifier, $component, $a, true); 10400 * $string = get_string('yes', 'moodle', null, true); 10401 * 2. Direct instantiation 10402 * $string = new lang_string($identifier, $component, $a, $lang); 10403 * $string = new lang_string('yes'); 10404 * 10405 * How do I use a lang_string object? 10406 * The lang_string object makes use of a magic __toString method so that you 10407 * are able to use the object exactly as you would use a string in most cases. 10408 * This means you are able to collect it into a variable and then directly 10409 * echo it, or concatenate it into another string, or similar. 10410 * The other thing you can do is manually get the string by calling the 10411 * lang_strings out method e.g. 10412 * $string = new lang_string('yes'); 10413 * $string->out(); 10414 * Also worth noting is that the out method can take one argument, $lang which 10415 * allows the developer to change the language on the fly. 10416 * 10417 * When should I use a lang_string object? 10418 * The lang_string object is designed to be used in any situation where a 10419 * string may not be needed, but needs to be generated. 10420 * The admin tree is a good example of where lang_string objects should be 10421 * used. 10422 * A more practical example would be any class that requries strings that may 10423 * not be printed (after all classes get renderer by renderers and who knows 10424 * what they will do ;)) 10425 * 10426 * When should I not use a lang_string object? 10427 * Don't use lang_strings when you are going to use a string immediately. 10428 * There is no need as it will be processed immediately and there will be no 10429 * advantage, and in fact perhaps a negative hit as a class has to be 10430 * instantiated for a lang_string object, however get_string won't require 10431 * that. 10432 * 10433 * Limitations: 10434 * 1. You cannot use a lang_string object as an array offset. Doing so will 10435 * result in PHP throwing an error. (You can use it as an object property!) 10436 * 10437 * @package core 10438 * @category string 10439 * @copyright 2011 Sam Hemelryk 10440 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 10441 */ 10442 class lang_string { 10443 10444 /** @var string The strings identifier */ 10445 protected $identifier; 10446 /** @var string The strings component. Default '' */ 10447 protected $component = ''; 10448 /** @var array|stdClass Any arguments required for the string. Default null */ 10449 protected $a = null; 10450 /** @var string The language to use when processing the string. Default null */ 10451 protected $lang = null; 10452 10453 /** @var string The processed string (once processed) */ 10454 protected $string = null; 10455 10456 /** 10457 * A special boolean. If set to true then the object has been woken up and 10458 * cannot be regenerated. If this is set then $this->string MUST be used. 10459 * @var bool 10460 */ 10461 protected $forcedstring = false; 10462 10463 /** 10464 * Constructs a lang_string object 10465 * 10466 * This function should do as little processing as possible to ensure the best 10467 * performance for strings that won't be used. 10468 * 10469 * @param string $identifier The strings identifier 10470 * @param string $component The strings component 10471 * @param stdClass|array $a Any arguments the string requires 10472 * @param string $lang The language to use when processing the string. 10473 * @throws coding_exception 10474 */ 10475 public function __construct($identifier, $component = '', $a = null, $lang = null) { 10476 if (empty($component)) { 10477 $component = 'moodle'; 10478 } 10479 10480 $this->identifier = $identifier; 10481 $this->component = $component; 10482 $this->lang = $lang; 10483 10484 // We MUST duplicate $a to ensure that it if it changes by reference those 10485 // changes are not carried across. 10486 // To do this we always ensure $a or its properties/values are strings 10487 // and that any properties/values that arn't convertable are forgotten. 10488 if ($a !== null) { 10489 if (is_scalar($a)) { 10490 $this->a = $a; 10491 } else if ($a instanceof lang_string) { 10492 $this->a = $a->out(); 10493 } else if (is_object($a) or is_array($a)) { 10494 $a = (array)$a; 10495 $this->a = array(); 10496 foreach ($a as $key => $value) { 10497 // Make sure conversion errors don't get displayed (results in ''). 10498 if (is_array($value)) { 10499 $this->a[$key] = ''; 10500 } else if (is_object($value)) { 10501 if (method_exists($value, '__toString')) { 10502 $this->a[$key] = $value->__toString(); 10503 } else { 10504 $this->a[$key] = ''; 10505 } 10506 } else { 10507 $this->a[$key] = (string)$value; 10508 } 10509 } 10510 } 10511 } 10512 10513 if (debugging(false, DEBUG_DEVELOPER)) { 10514 if (clean_param($this->identifier, PARAM_STRINGID) == '') { 10515 throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition'); 10516 } 10517 if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') { 10518 throw new coding_exception('Invalid string compontent. Please check your string definition'); 10519 } 10520 if (!get_string_manager()->string_exists($this->identifier, $this->component)) { 10521 debugging('String does not exist. Please check your string definition for '.$this->identifier.'/'.$this->component, DEBUG_DEVELOPER); 10522 } 10523 } 10524 } 10525 10526 /** 10527 * Processes the string. 10528 * 10529 * This function actually processes the string, stores it in the string property 10530 * and then returns it. 10531 * You will notice that this function is VERY similar to the get_string method. 10532 * That is because it is pretty much doing the same thing. 10533 * However as this function is an upgrade it isn't as tolerant to backwards 10534 * compatibility. 10535 * 10536 * @return string 10537 * @throws coding_exception 10538 */ 10539 protected function get_string() { 10540 global $CFG; 10541 10542 // Check if we need to process the string. 10543 if ($this->string === null) { 10544 // Check the quality of the identifier. 10545 if ($CFG->debugdeveloper && clean_param($this->identifier, PARAM_STRINGID) === '') { 10546 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); 10547 } 10548 10549 // Process the string. 10550 $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang); 10551 // Debugging feature lets you display string identifier and component. 10552 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) { 10553 $this->string .= ' {' . $this->identifier . '/' . $this->component . '}'; 10554 } 10555 } 10556 // Return the string. 10557 return $this->string; 10558 } 10559 10560 /** 10561 * Returns the string 10562 * 10563 * @param string $lang The langauge to use when processing the string 10564 * @return string 10565 */ 10566 public function out($lang = null) { 10567 if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) { 10568 if ($this->forcedstring) { 10569 debugging('lang_string objects that have been used cannot be printed in another language. ('.$this->lang.' used)', DEBUG_DEVELOPER); 10570 return $this->get_string(); 10571 } 10572 $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang); 10573 return $translatedstring->out(); 10574 } 10575 return $this->get_string(); 10576 } 10577 10578 /** 10579 * Magic __toString method for printing a string 10580 * 10581 * @return string 10582 */ 10583 public function __toString() { 10584 return $this->get_string(); 10585 } 10586 10587 /** 10588 * Magic __set_state method used for var_export 10589 * 10590 * @return string 10591 */ 10592 public function __set_state() { 10593 return $this->get_string(); 10594 } 10595 10596 /** 10597 * Prepares the lang_string for sleep and stores only the forcedstring and 10598 * string properties... the string cannot be regenerated so we need to ensure 10599 * it is generated for this. 10600 * 10601 * @return string 10602 */ 10603 public function __sleep() { 10604 $this->get_string(); 10605 $this->forcedstring = true; 10606 return array('forcedstring', 'string', 'lang'); 10607 } 10608 10609 /** 10610 * Returns the identifier. 10611 * 10612 * @return string 10613 */ 10614 public function get_identifier() { 10615 return $this->identifier; 10616 } 10617 10618 /** 10619 * Returns the component. 10620 * 10621 * @return string 10622 */ 10623 public function get_component() { 10624 return $this->component; 10625 } 10626 } 10627 10628 /** 10629 * Get human readable name describing the given callable. 10630 * 10631 * This performs syntax check only to see if the given param looks like a valid function, method or closure. 10632 * It does not check if the callable actually exists. 10633 * 10634 * @param callable|string|array $callable 10635 * @return string|bool Human readable name of callable, or false if not a valid callable. 10636 */ 10637 function get_callable_name($callable) { 10638 10639 if (!is_callable($callable, true, $name)) { 10640 return false; 10641 10642 } else { 10643 return $name; 10644 } 10645 } 10646 10647 /** 10648 * Tries to guess if $CFG->wwwroot is publicly accessible or not. 10649 * Never put your faith on this function and rely on its accuracy as there might be false positives. 10650 * It just performs some simple checks, and mainly is used for places where we want to hide some options 10651 * such as site registration when $CFG->wwwroot is not publicly accessible. 10652 * Good thing is there is no false negative. 10653 * Note that it's possible to force the result of this check by specifying $CFG->site_is_public in config.php 10654 * 10655 * @return bool 10656 */ 10657 function site_is_public() { 10658 global $CFG; 10659 10660 // Return early if site admin has forced this setting. 10661 if (isset($CFG->site_is_public)) { 10662 return (bool)$CFG->site_is_public; 10663 } 10664 10665 $host = parse_url($CFG->wwwroot, PHP_URL_HOST); 10666 10667 if ($host === 'localhost' || preg_match('|^127\.\d+\.\d+\.\d+$|', $host)) { 10668 $ispublic = false; 10669 } else if (\core\ip_utils::is_ip_address($host) && !ip_is_public($host)) { 10670 $ispublic = false; 10671 } else if (($address = \core\ip_utils::get_ip_address($host)) && !ip_is_public($address)) { 10672 $ispublic = false; 10673 } else { 10674 $ispublic = true; 10675 } 10676 10677 return $ispublic; 10678 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body