Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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 /** 401 * Required password pepper entropy. 402 */ 403 define ('PEPPER_ENTROPY', 112); 404 405 // Feature constants. 406 // Used for plugin_supports() to report features that are, or are not, supported by a module. 407 408 /** True if module can provide a grade */ 409 define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade'); 410 /** True if module supports outcomes */ 411 define('FEATURE_GRADE_OUTCOMES', 'outcomes'); 412 /** True if module supports advanced grading methods */ 413 define('FEATURE_ADVANCED_GRADING', 'grade_advanced_grading'); 414 /** True if module controls the grade visibility over the gradebook */ 415 define('FEATURE_CONTROLS_GRADE_VISIBILITY', 'controlsgradevisbility'); 416 /** True if module supports plagiarism plugins */ 417 define('FEATURE_PLAGIARISM', 'plagiarism'); 418 419 /** True if module has code to track whether somebody viewed it */ 420 define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views'); 421 /** True if module has custom completion rules */ 422 define('FEATURE_COMPLETION_HAS_RULES', 'completion_has_rules'); 423 424 /** True if module has no 'view' page (like label) */ 425 define('FEATURE_NO_VIEW_LINK', 'viewlink'); 426 /** True (which is default) if the module wants support for setting the ID number for grade calculation purposes. */ 427 define('FEATURE_IDNUMBER', 'idnumber'); 428 /** True if module supports groups */ 429 define('FEATURE_GROUPS', 'groups'); 430 /** True if module supports groupings */ 431 define('FEATURE_GROUPINGS', 'groupings'); 432 /** 433 * True if module supports groupmembersonly (which no longer exists) 434 * @deprecated Since Moodle 2.8 435 */ 436 define('FEATURE_GROUPMEMBERSONLY', 'groupmembersonly'); 437 438 /** Type of module */ 439 define('FEATURE_MOD_ARCHETYPE', 'mod_archetype'); 440 /** True if module supports intro editor */ 441 define('FEATURE_MOD_INTRO', 'mod_intro'); 442 /** True if module has default completion */ 443 define('FEATURE_MODEDIT_DEFAULT_COMPLETION', 'modedit_default_completion'); 444 445 define('FEATURE_COMMENT', 'comment'); 446 447 define('FEATURE_RATE', 'rate'); 448 /** True if module supports backup/restore of moodle2 format */ 449 define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2'); 450 451 /** True if module can show description on course main page */ 452 define('FEATURE_SHOW_DESCRIPTION', 'showdescription'); 453 454 /** True if module uses the question bank */ 455 define('FEATURE_USES_QUESTIONS', 'usesquestions'); 456 457 /** 458 * Maximum filename char size 459 */ 460 define('MAX_FILENAME_SIZE', 100); 461 462 /** Unspecified module archetype */ 463 define('MOD_ARCHETYPE_OTHER', 0); 464 /** Resource-like type module */ 465 define('MOD_ARCHETYPE_RESOURCE', 1); 466 /** Assignment module archetype */ 467 define('MOD_ARCHETYPE_ASSIGNMENT', 2); 468 /** System (not user-addable) module archetype */ 469 define('MOD_ARCHETYPE_SYSTEM', 3); 470 471 /** Type of module */ 472 define('FEATURE_MOD_PURPOSE', 'mod_purpose'); 473 /** Module purpose administration */ 474 define('MOD_PURPOSE_ADMINISTRATION', 'administration'); 475 /** Module purpose assessment */ 476 define('MOD_PURPOSE_ASSESSMENT', 'assessment'); 477 /** Module purpose communication */ 478 define('MOD_PURPOSE_COLLABORATION', 'collaboration'); 479 /** Module purpose communication */ 480 define('MOD_PURPOSE_COMMUNICATION', 'communication'); 481 /** Module purpose content */ 482 define('MOD_PURPOSE_CONTENT', 'content'); 483 /** Module purpose interface */ 484 define('MOD_PURPOSE_INTERFACE', 'interface'); 485 /** Module purpose other */ 486 define('MOD_PURPOSE_OTHER', 'other'); 487 488 /** 489 * Security token used for allowing access 490 * from external application such as web services. 491 * Scripts do not use any session, performance is relatively 492 * low because we need to load access info in each request. 493 * Scripts are executed in parallel. 494 */ 495 define('EXTERNAL_TOKEN_PERMANENT', 0); 496 497 /** 498 * Security token used for allowing access 499 * of embedded applications, the code is executed in the 500 * active user session. Token is invalidated after user logs out. 501 * Scripts are executed serially - normal session locking is used. 502 */ 503 define('EXTERNAL_TOKEN_EMBEDDED', 1); 504 505 /** 506 * The home page should be the site home 507 */ 508 define('HOMEPAGE_SITE', 0); 509 /** 510 * The home page should be the users my page 511 */ 512 define('HOMEPAGE_MY', 1); 513 /** 514 * The home page can be chosen by the user 515 */ 516 define('HOMEPAGE_USER', 2); 517 /** 518 * The home page should be the users my courses page 519 */ 520 define('HOMEPAGE_MYCOURSES', 3); 521 522 /** 523 * URL of the Moodle sites registration portal. 524 */ 525 defined('HUB_MOODLEORGHUBURL') || define('HUB_MOODLEORGHUBURL', 'https://stats.moodle.org'); 526 527 /** 528 * URL of the statistic server public key. 529 */ 530 defined('HUB_STATSPUBLICKEY') || define('HUB_STATSPUBLICKEY', 'https://moodle.org/static/statspubkey.pem'); 531 532 /** 533 * Moodle mobile app service name 534 */ 535 define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app'); 536 537 /** 538 * Indicates the user has the capabilities required to ignore activity and course file size restrictions 539 */ 540 define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1); 541 542 /** 543 * Course display settings: display all sections on one page. 544 */ 545 define('COURSE_DISPLAY_SINGLEPAGE', 0); 546 /** 547 * Course display settings: split pages into a page per section. 548 */ 549 define('COURSE_DISPLAY_MULTIPAGE', 1); 550 551 /** 552 * Authentication constant: String used in password field when password is not stored. 553 */ 554 define('AUTH_PASSWORD_NOT_CACHED', 'not cached'); 555 556 /** 557 * Email from header to never include via information. 558 */ 559 define('EMAIL_VIA_NEVER', 0); 560 561 /** 562 * Email from header to always include via information. 563 */ 564 define('EMAIL_VIA_ALWAYS', 1); 565 566 /** 567 * Email from header to only include via information if the address is no-reply. 568 */ 569 define('EMAIL_VIA_NO_REPLY_ONLY', 2); 570 571 /** 572 * Contact site support form/link disabled. 573 */ 574 define('CONTACT_SUPPORT_DISABLED', 0); 575 576 /** 577 * Contact site support form/link only available to authenticated users. 578 */ 579 define('CONTACT_SUPPORT_AUTHENTICATED', 1); 580 581 /** 582 * Contact site support form/link available to anyone visiting the site. 583 */ 584 define('CONTACT_SUPPORT_ANYONE', 2); 585 586 /** 587 * Maximum number of characters for password. 588 */ 589 define('MAX_PASSWORD_CHARACTERS', 128); 590 591 // PARAMETER HANDLING. 592 593 /** 594 * Returns a particular value for the named variable, taken from 595 * POST or GET. If the parameter doesn't exist then an error is 596 * thrown because we require this variable. 597 * 598 * This function should be used to initialise all required values 599 * in a script that are based on parameters. Usually it will be 600 * used like this: 601 * $id = required_param('id', PARAM_INT); 602 * 603 * Please note the $type parameter is now required and the value can not be array. 604 * 605 * @param string $parname the name of the page parameter we want 606 * @param string $type expected type of parameter 607 * @return mixed 608 * @throws coding_exception 609 */ 610 function required_param($parname, $type) { 611 if (func_num_args() != 2 or empty($parname) or empty($type)) { 612 throw new coding_exception('required_param() requires $parname and $type to be specified (parameter: '.$parname.')'); 613 } 614 // POST has precedence. 615 if (isset($_POST[$parname])) { 616 $param = $_POST[$parname]; 617 } else if (isset($_GET[$parname])) { 618 $param = $_GET[$parname]; 619 } else { 620 throw new \moodle_exception('missingparam', '', '', $parname); 621 } 622 623 if (is_array($param)) { 624 debugging('Invalid array parameter detected in required_param(): '.$parname); 625 // TODO: switch to fatal error in Moodle 2.3. 626 return required_param_array($parname, $type); 627 } 628 629 return clean_param($param, $type); 630 } 631 632 /** 633 * Returns a particular array value for the named variable, taken from 634 * POST or GET. If the parameter doesn't exist then an error is 635 * thrown because we require this variable. 636 * 637 * This function should be used to initialise all required values 638 * in a script that are based on parameters. Usually it will be 639 * used like this: 640 * $ids = required_param_array('ids', PARAM_INT); 641 * 642 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported 643 * 644 * @param string $parname the name of the page parameter we want 645 * @param string $type expected type of parameter 646 * @return array 647 * @throws coding_exception 648 */ 649 function required_param_array($parname, $type) { 650 if (func_num_args() != 2 or empty($parname) or empty($type)) { 651 throw new coding_exception('required_param_array() requires $parname and $type to be specified (parameter: '.$parname.')'); 652 } 653 // POST has precedence. 654 if (isset($_POST[$parname])) { 655 $param = $_POST[$parname]; 656 } else if (isset($_GET[$parname])) { 657 $param = $_GET[$parname]; 658 } else { 659 throw new \moodle_exception('missingparam', '', '', $parname); 660 } 661 if (!is_array($param)) { 662 throw new \moodle_exception('missingparam', '', '', $parname); 663 } 664 665 $result = array(); 666 foreach ($param as $key => $value) { 667 if (!preg_match('/^[a-z0-9_-]+$/i', $key)) { 668 debugging('Invalid key name in required_param_array() detected: '.$key.', parameter: '.$parname); 669 continue; 670 } 671 $result[$key] = clean_param($value, $type); 672 } 673 674 return $result; 675 } 676 677 /** 678 * Returns a particular value for the named variable, taken from 679 * POST or GET, otherwise returning a given default. 680 * 681 * This function should be used to initialise all optional values 682 * in a script that are based on parameters. Usually it will be 683 * used like this: 684 * $name = optional_param('name', 'Fred', PARAM_TEXT); 685 * 686 * Please note the $type parameter is now required and the value can not be array. 687 * 688 * @param string $parname the name of the page parameter we want 689 * @param mixed $default the default value to return if nothing is found 690 * @param string $type expected type of parameter 691 * @return mixed 692 * @throws coding_exception 693 */ 694 function optional_param($parname, $default, $type) { 695 if (func_num_args() != 3 or empty($parname) or empty($type)) { 696 throw new coding_exception('optional_param requires $parname, $default + $type to be specified (parameter: '.$parname.')'); 697 } 698 699 // POST has precedence. 700 if (isset($_POST[$parname])) { 701 $param = $_POST[$parname]; 702 } else if (isset($_GET[$parname])) { 703 $param = $_GET[$parname]; 704 } else { 705 return $default; 706 } 707 708 if (is_array($param)) { 709 debugging('Invalid array parameter detected in required_param(): '.$parname); 710 // TODO: switch to $default in Moodle 2.3. 711 return optional_param_array($parname, $default, $type); 712 } 713 714 return clean_param($param, $type); 715 } 716 717 /** 718 * Returns a particular array value for the named variable, taken from 719 * POST or GET, otherwise returning a given default. 720 * 721 * This function should be used to initialise all optional values 722 * in a script that are based on parameters. Usually it will be 723 * used like this: 724 * $ids = optional_param('id', array(), PARAM_INT); 725 * 726 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported 727 * 728 * @param string $parname the name of the page parameter we want 729 * @param mixed $default the default value to return if nothing is found 730 * @param string $type expected type of parameter 731 * @return array 732 * @throws coding_exception 733 */ 734 function optional_param_array($parname, $default, $type) { 735 if (func_num_args() != 3 or empty($parname) or empty($type)) { 736 throw new coding_exception('optional_param_array requires $parname, $default + $type to be specified (parameter: '.$parname.')'); 737 } 738 739 // POST has precedence. 740 if (isset($_POST[$parname])) { 741 $param = $_POST[$parname]; 742 } else if (isset($_GET[$parname])) { 743 $param = $_GET[$parname]; 744 } else { 745 return $default; 746 } 747 if (!is_array($param)) { 748 debugging('optional_param_array() expects array parameters only: '.$parname); 749 return $default; 750 } 751 752 $result = array(); 753 foreach ($param as $key => $value) { 754 if (!preg_match('/^[a-z0-9_-]+$/i', $key)) { 755 debugging('Invalid key name in optional_param_array() detected: '.$key.', parameter: '.$parname); 756 continue; 757 } 758 $result[$key] = clean_param($value, $type); 759 } 760 761 return $result; 762 } 763 764 /** 765 * Strict validation of parameter values, the values are only converted 766 * to requested PHP type. Internally it is using clean_param, the values 767 * before and after cleaning must be equal - otherwise 768 * an invalid_parameter_exception is thrown. 769 * Objects and classes are not accepted. 770 * 771 * @param mixed $param 772 * @param string $type PARAM_ constant 773 * @param bool $allownull are nulls valid value? 774 * @param string $debuginfo optional debug information 775 * @return mixed the $param value converted to PHP type 776 * @throws invalid_parameter_exception if $param is not of given type 777 */ 778 function validate_param($param, $type, $allownull=NULL_NOT_ALLOWED, $debuginfo='') { 779 if (is_null($param)) { 780 if ($allownull == NULL_ALLOWED) { 781 return null; 782 } else { 783 throw new invalid_parameter_exception($debuginfo); 784 } 785 } 786 if (is_array($param) or is_object($param)) { 787 throw new invalid_parameter_exception($debuginfo); 788 } 789 790 $cleaned = clean_param($param, $type); 791 792 if ($type == PARAM_FLOAT) { 793 // Do not detect precision loss here. 794 if (is_float($param) or is_int($param)) { 795 // These always fit. 796 } else if (!is_numeric($param) or !preg_match('/^[\+-]?[0-9]*\.?[0-9]*(e[-+]?[0-9]+)?$/i', (string)$param)) { 797 throw new invalid_parameter_exception($debuginfo); 798 } 799 } else if ((string)$param !== (string)$cleaned) { 800 // Conversion to string is usually lossless. 801 throw new invalid_parameter_exception($debuginfo); 802 } 803 804 return $cleaned; 805 } 806 807 /** 808 * Makes sure array contains only the allowed types, this function does not validate array key names! 809 * 810 * <code> 811 * $options = clean_param($options, PARAM_INT); 812 * </code> 813 * 814 * @param array|null $param the variable array we are cleaning 815 * @param string $type expected format of param after cleaning. 816 * @param bool $recursive clean recursive arrays 817 * @return array 818 * @throws coding_exception 819 */ 820 function clean_param_array(?array $param, $type, $recursive = false) { 821 // Convert null to empty array. 822 $param = (array)$param; 823 foreach ($param as $key => $value) { 824 if (is_array($value)) { 825 if ($recursive) { 826 $param[$key] = clean_param_array($value, $type, true); 827 } else { 828 throw new coding_exception('clean_param_array can not process multidimensional arrays when $recursive is false.'); 829 } 830 } else { 831 $param[$key] = clean_param($value, $type); 832 } 833 } 834 return $param; 835 } 836 837 /** 838 * Used by {@link optional_param()} and {@link required_param()} to 839 * clean the variables and/or cast to specific types, based on 840 * an options field. 841 * <code> 842 * $course->format = clean_param($course->format, PARAM_ALPHA); 843 * $selectedgradeitem = clean_param($selectedgradeitem, PARAM_INT); 844 * </code> 845 * 846 * @param mixed $param the variable we are cleaning 847 * @param string $type expected format of param after cleaning. 848 * @return mixed 849 * @throws coding_exception 850 */ 851 function clean_param($param, $type) { 852 global $CFG; 853 854 if (is_array($param)) { 855 throw new coding_exception('clean_param() can not process arrays, please use clean_param_array() instead.'); 856 } else if (is_object($param)) { 857 if (method_exists($param, '__toString')) { 858 $param = $param->__toString(); 859 } else { 860 throw new coding_exception('clean_param() can not process objects, please use clean_param_array() instead.'); 861 } 862 } 863 864 switch ($type) { 865 case PARAM_RAW: 866 // No cleaning at all. 867 $param = fix_utf8($param); 868 return $param; 869 870 case PARAM_RAW_TRIMMED: 871 // No cleaning, but strip leading and trailing whitespace. 872 $param = (string)fix_utf8($param); 873 return trim($param); 874 875 case PARAM_CLEAN: 876 // General HTML cleaning, try to use more specific type if possible this is deprecated! 877 // Please use more specific type instead. 878 if (is_numeric($param)) { 879 return $param; 880 } 881 $param = fix_utf8($param); 882 // Sweep for scripts, etc. 883 return clean_text($param); 884 885 case PARAM_CLEANHTML: 886 // Clean html fragment. 887 $param = (string)fix_utf8($param); 888 // Sweep for scripts, etc. 889 $param = clean_text($param, FORMAT_HTML); 890 return trim($param); 891 892 case PARAM_INT: 893 // Convert to integer. 894 return (int)$param; 895 896 case PARAM_FLOAT: 897 // Convert to float. 898 return (float)$param; 899 900 case PARAM_LOCALISEDFLOAT: 901 // Convert to float. 902 return unformat_float($param, true); 903 904 case PARAM_ALPHA: 905 // Remove everything not `a-z`. 906 return preg_replace('/[^a-zA-Z]/i', '', (string)$param); 907 908 case PARAM_ALPHAEXT: 909 // Remove everything not `a-zA-Z_-` (originally allowed "/" too). 910 return preg_replace('/[^a-zA-Z_-]/i', '', (string)$param); 911 912 case PARAM_ALPHANUM: 913 // Remove everything not `a-zA-Z0-9`. 914 return preg_replace('/[^A-Za-z0-9]/i', '', (string)$param); 915 916 case PARAM_ALPHANUMEXT: 917 // Remove everything not `a-zA-Z0-9_-`. 918 return preg_replace('/[^A-Za-z0-9_-]/i', '', (string)$param); 919 920 case PARAM_SEQUENCE: 921 // Remove everything not `0-9,`. 922 return preg_replace('/[^0-9,]/i', '', (string)$param); 923 924 case PARAM_BOOL: 925 // Convert to 1 or 0. 926 $tempstr = strtolower((string)$param); 927 if ($tempstr === 'on' or $tempstr === 'yes' or $tempstr === 'true') { 928 $param = 1; 929 } else if ($tempstr === 'off' or $tempstr === 'no' or $tempstr === 'false') { 930 $param = 0; 931 } else { 932 $param = empty($param) ? 0 : 1; 933 } 934 return $param; 935 936 case PARAM_NOTAGS: 937 // Strip all tags. 938 $param = fix_utf8($param); 939 return strip_tags((string)$param); 940 941 case PARAM_TEXT: 942 // Leave only tags needed for multilang. 943 $param = fix_utf8($param); 944 // If the multilang syntax is not correct we strip all tags because it would break xhtml strict which is required 945 // for accessibility standards please note this cleaning does not strip unbalanced '>' for BC compatibility reasons. 946 do { 947 if (strpos((string)$param, '</lang>') !== false) { 948 // Old and future mutilang syntax. 949 $param = strip_tags($param, '<lang>'); 950 if (!preg_match_all('/<.*>/suU', $param, $matches)) { 951 break; 952 } 953 $open = false; 954 foreach ($matches[0] as $match) { 955 if ($match === '</lang>') { 956 if ($open) { 957 $open = false; 958 continue; 959 } else { 960 break 2; 961 } 962 } 963 if (!preg_match('/^<lang lang="[a-zA-Z0-9_-]+"\s*>$/u', $match)) { 964 break 2; 965 } else { 966 $open = true; 967 } 968 } 969 if ($open) { 970 break; 971 } 972 return $param; 973 974 } else if (strpos((string)$param, '</span>') !== false) { 975 // Current problematic multilang syntax. 976 $param = strip_tags($param, '<span>'); 977 if (!preg_match_all('/<.*>/suU', $param, $matches)) { 978 break; 979 } 980 $open = false; 981 foreach ($matches[0] as $match) { 982 if ($match === '</span>') { 983 if ($open) { 984 $open = false; 985 continue; 986 } else { 987 break 2; 988 } 989 } 990 if (!preg_match('/^<span(\s+lang="[a-zA-Z0-9_-]+"|\s+class="multilang"){2}\s*>$/u', $match)) { 991 break 2; 992 } else { 993 $open = true; 994 } 995 } 996 if ($open) { 997 break; 998 } 999 return $param; 1000 } 1001 } while (false); 1002 // Easy, just strip all tags, if we ever want to fix orphaned '&' we have to do that in format_string(). 1003 return strip_tags((string)$param); 1004 1005 case PARAM_COMPONENT: 1006 // We do not want any guessing here, either the name is correct or not 1007 // please note only normalised component names are accepted. 1008 $param = (string)$param; 1009 if (!preg_match('/^[a-z][a-z0-9]*(_[a-z][a-z0-9_]*)?[a-z0-9]+$/', $param)) { 1010 return ''; 1011 } 1012 if (strpos($param, '__') !== false) { 1013 return ''; 1014 } 1015 if (strpos($param, 'mod_') === 0) { 1016 // Module names must not contain underscores because we need to differentiate them from invalid plugin types. 1017 if (substr_count($param, '_') != 1) { 1018 return ''; 1019 } 1020 } 1021 return $param; 1022 1023 case PARAM_PLUGIN: 1024 case PARAM_AREA: 1025 // We do not want any guessing here, either the name is correct or not. 1026 if (!is_valid_plugin_name($param)) { 1027 return ''; 1028 } 1029 return $param; 1030 1031 case PARAM_SAFEDIR: 1032 // Remove everything not a-zA-Z0-9_- . 1033 return preg_replace('/[^a-zA-Z0-9_-]/i', '', (string)$param); 1034 1035 case PARAM_SAFEPATH: 1036 // Remove everything not a-zA-Z0-9/_- . 1037 return preg_replace('/[^a-zA-Z0-9\/_-]/i', '', (string)$param); 1038 1039 case PARAM_FILE: 1040 // Strip all suspicious characters from filename. 1041 $param = (string)fix_utf8($param); 1042 $param = preg_replace('~[[:cntrl:]]|[&<>"`\|\':\\\\/]~u', '', $param); 1043 if ($param === '.' || $param === '..') { 1044 $param = ''; 1045 } 1046 return $param; 1047 1048 case PARAM_PATH: 1049 // Strip all suspicious characters from file path. 1050 $param = (string)fix_utf8($param); 1051 $param = str_replace('\\', '/', $param); 1052 1053 // Explode the path and clean each element using the PARAM_FILE rules. 1054 $breadcrumb = explode('/', $param); 1055 foreach ($breadcrumb as $key => $crumb) { 1056 if ($crumb === '.' && $key === 0) { 1057 // Special condition to allow for relative current path such as ./currentdirfile.txt. 1058 } else { 1059 $crumb = clean_param($crumb, PARAM_FILE); 1060 } 1061 $breadcrumb[$key] = $crumb; 1062 } 1063 $param = implode('/', $breadcrumb); 1064 1065 // Remove multiple current path (./././) and multiple slashes (///). 1066 $param = preg_replace('~//+~', '/', $param); 1067 $param = preg_replace('~/(\./)+~', '/', $param); 1068 return $param; 1069 1070 case PARAM_HOST: 1071 // Allow FQDN or IPv4 dotted quad. 1072 $param = preg_replace('/[^\.\d\w-]/', '', (string)$param ); 1073 // Match ipv4 dotted quad. 1074 if (preg_match('/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/', $param, $match)) { 1075 // Confirm values are ok. 1076 if ( $match[0] > 255 1077 || $match[1] > 255 1078 || $match[3] > 255 1079 || $match[4] > 255 ) { 1080 // Hmmm, what kind of dotted quad is this? 1081 $param = ''; 1082 } 1083 } else if ( preg_match('/^[\w\d\.-]+$/', $param) // Dots, hyphens, numbers. 1084 && !preg_match('/^[\.-]/', $param) // No leading dots/hyphens. 1085 && !preg_match('/[\.-]$/', $param) // No trailing dots/hyphens. 1086 ) { 1087 // All is ok - $param is respected. 1088 } else { 1089 // All is not ok... 1090 $param=''; 1091 } 1092 return $param; 1093 1094 case PARAM_URL: 1095 // Allow safe urls. 1096 $param = (string)fix_utf8($param); 1097 include_once($CFG->dirroot . '/lib/validateurlsyntax.php'); 1098 if (!empty($param) && validateUrlSyntax($param, 's?H?S?F?E-u-P-a?I?p?f?q?r?')) { 1099 // All is ok, param is respected. 1100 } else { 1101 // Not really ok. 1102 $param =''; 1103 } 1104 return $param; 1105 1106 case PARAM_LOCALURL: 1107 // Allow http absolute, root relative and relative URLs within wwwroot. 1108 $param = clean_param($param, PARAM_URL); 1109 if (!empty($param)) { 1110 1111 if ($param === $CFG->wwwroot) { 1112 // Exact match; 1113 } else if (preg_match(':^/:', $param)) { 1114 // Root-relative, ok! 1115 } else if (preg_match('/^' . preg_quote($CFG->wwwroot . '/', '/') . '/i', $param)) { 1116 // Absolute, and matches our wwwroot. 1117 } else { 1118 1119 // Relative - let's make sure there are no tricks. 1120 if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?') && !preg_match('/javascript:/i', $param)) { 1121 // Looks ok. 1122 } else { 1123 $param = ''; 1124 } 1125 } 1126 } 1127 return $param; 1128 1129 case PARAM_PEM: 1130 $param = trim((string)$param); 1131 // PEM formatted strings may contain letters/numbers and the symbols: 1132 // forward slash: / 1133 // plus sign: + 1134 // equal sign: = 1135 // , surrounded by BEGIN and END CERTIFICATE prefix and suffixes. 1136 if (preg_match('/^-----BEGIN CERTIFICATE-----([\s\w\/\+=]+)-----END CERTIFICATE-----$/', trim($param), $matches)) { 1137 list($wholething, $body) = $matches; 1138 unset($wholething, $matches); 1139 $b64 = clean_param($body, PARAM_BASE64); 1140 if (!empty($b64)) { 1141 return "-----BEGIN CERTIFICATE-----\n$b64\n-----END CERTIFICATE-----\n"; 1142 } else { 1143 return ''; 1144 } 1145 } 1146 return ''; 1147 1148 case PARAM_BASE64: 1149 if (!empty($param)) { 1150 // PEM formatted strings may contain letters/numbers and the symbols 1151 // forward slash: / 1152 // plus sign: + 1153 // equal sign: =. 1154 if (0 >= preg_match('/^([\s\w\/\+=]+)$/', trim($param))) { 1155 return ''; 1156 } 1157 $lines = preg_split('/[\s]+/', $param, -1, PREG_SPLIT_NO_EMPTY); 1158 // Each line of base64 encoded data must be 64 characters in length, except for the last line which may be less 1159 // than (or equal to) 64 characters long. 1160 for ($i=0, $j=count($lines); $i < $j; $i++) { 1161 if ($i + 1 == $j) { 1162 if (64 < strlen($lines[$i])) { 1163 return ''; 1164 } 1165 continue; 1166 } 1167 1168 if (64 != strlen($lines[$i])) { 1169 return ''; 1170 } 1171 } 1172 return implode("\n", $lines); 1173 } else { 1174 return ''; 1175 } 1176 1177 case PARAM_TAG: 1178 $param = (string)fix_utf8($param); 1179 // Please note it is not safe to use the tag name directly anywhere, 1180 // it must be processed with s(), urlencode() before embedding anywhere. 1181 // Remove some nasties. 1182 $param = preg_replace('~[[:cntrl:]]|[<>`]~u', '', $param); 1183 // Convert many whitespace chars into one. 1184 $param = preg_replace('/\s+/u', ' ', $param); 1185 $param = core_text::substr(trim($param), 0, TAG_MAX_LENGTH); 1186 return $param; 1187 1188 case PARAM_TAGLIST: 1189 $param = (string)fix_utf8($param); 1190 $tags = explode(',', $param); 1191 $result = array(); 1192 foreach ($tags as $tag) { 1193 $res = clean_param($tag, PARAM_TAG); 1194 if ($res !== '') { 1195 $result[] = $res; 1196 } 1197 } 1198 if ($result) { 1199 return implode(',', $result); 1200 } else { 1201 return ''; 1202 } 1203 1204 case PARAM_CAPABILITY: 1205 if (get_capability_info($param)) { 1206 return $param; 1207 } else { 1208 return ''; 1209 } 1210 1211 case PARAM_PERMISSION: 1212 $param = (int)$param; 1213 if (in_array($param, array(CAP_INHERIT, CAP_ALLOW, CAP_PREVENT, CAP_PROHIBIT))) { 1214 return $param; 1215 } else { 1216 return CAP_INHERIT; 1217 } 1218 1219 case PARAM_AUTH: 1220 $param = clean_param($param, PARAM_PLUGIN); 1221 if (empty($param)) { 1222 return ''; 1223 } else if (exists_auth_plugin($param)) { 1224 return $param; 1225 } else { 1226 return ''; 1227 } 1228 1229 case PARAM_LANG: 1230 $param = clean_param($param, PARAM_SAFEDIR); 1231 if (get_string_manager()->translation_exists($param)) { 1232 return $param; 1233 } else { 1234 // Specified language is not installed or param malformed. 1235 return ''; 1236 } 1237 1238 case PARAM_THEME: 1239 $param = clean_param($param, PARAM_PLUGIN); 1240 if (empty($param)) { 1241 return ''; 1242 } else if (file_exists("$CFG->dirroot/theme/$param/config.php")) { 1243 return $param; 1244 } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$param/config.php")) { 1245 return $param; 1246 } else { 1247 // Specified theme is not installed. 1248 return ''; 1249 } 1250 1251 case PARAM_USERNAME: 1252 $param = (string)fix_utf8($param); 1253 $param = trim($param); 1254 // Convert uppercase to lowercase MDL-16919. 1255 $param = core_text::strtolower($param); 1256 if (empty($CFG->extendedusernamechars)) { 1257 $param = str_replace(" " , "", $param); 1258 // Regular expression, eliminate all chars EXCEPT: 1259 // alphanum, dash (-), underscore (_), at sign (@) and period (.) characters. 1260 $param = preg_replace('/[^-\.@_a-z0-9]/', '', $param); 1261 } 1262 return $param; 1263 1264 case PARAM_EMAIL: 1265 $param = fix_utf8($param); 1266 if (validate_email($param ?? '')) { 1267 return $param; 1268 } else { 1269 return ''; 1270 } 1271 1272 case PARAM_STRINGID: 1273 if (preg_match('|^[a-zA-Z][a-zA-Z0-9\.:/_-]*$|', (string)$param)) { 1274 return $param; 1275 } else { 1276 return ''; 1277 } 1278 1279 case PARAM_TIMEZONE: 1280 // Can be int, float(with .5 or .0) or string seperated by '/' and can have '-_'. 1281 $param = (string)fix_utf8($param); 1282 $timezonepattern = '/^(([+-]?(0?[0-9](\.[5|0])?|1[0-3](\.0)?|1[0-2]\.5))|(99)|[[:alnum:]]+(\/?[[:alpha:]_-])+)$/'; 1283 if (preg_match($timezonepattern, $param)) { 1284 return $param; 1285 } else { 1286 return ''; 1287 } 1288 1289 default: 1290 // Doh! throw error, switched parameters in optional_param or another serious problem. 1291 throw new \moodle_exception("unknownparamtype", '', '', $type); 1292 } 1293 } 1294 1295 /** 1296 * Whether the PARAM_* type is compatible in RTL. 1297 * 1298 * Being compatible with RTL means that the data they contain can flow 1299 * from right-to-left or left-to-right without compromising the user experience. 1300 * 1301 * Take URLs for example, they are not RTL compatible as they should always 1302 * flow from the left to the right. This also applies to numbers, email addresses, 1303 * configuration snippets, base64 strings, etc... 1304 * 1305 * This function tries to best guess which parameters can contain localised strings. 1306 * 1307 * @param string $paramtype Constant PARAM_*. 1308 * @return bool 1309 */ 1310 function is_rtl_compatible($paramtype) { 1311 return $paramtype == PARAM_TEXT || $paramtype == PARAM_NOTAGS; 1312 } 1313 1314 /** 1315 * Makes sure the data is using valid utf8, invalid characters are discarded. 1316 * 1317 * Note: this function is not intended for full objects with methods and private properties. 1318 * 1319 * @param mixed $value 1320 * @return mixed with proper utf-8 encoding 1321 */ 1322 function fix_utf8($value) { 1323 if (is_null($value) or $value === '') { 1324 return $value; 1325 1326 } else if (is_string($value)) { 1327 if ((string)(int)$value === $value) { 1328 // Shortcut. 1329 return $value; 1330 } 1331 1332 // Remove null bytes or invalid Unicode sequences from value. 1333 $value = str_replace(["\0", "\xef\xbf\xbe", "\xef\xbf\xbf"], '', $value); 1334 1335 // Note: this duplicates min_fix_utf8() intentionally. 1336 static $buggyiconv = null; 1337 if ($buggyiconv === null) { 1338 $buggyiconv = (!function_exists('iconv') or @iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€'); 1339 } 1340 1341 if ($buggyiconv) { 1342 if (function_exists('mb_convert_encoding')) { 1343 $subst = mb_substitute_character(); 1344 mb_substitute_character('none'); 1345 $result = mb_convert_encoding($value, 'utf-8', 'utf-8'); 1346 mb_substitute_character($subst); 1347 1348 } else { 1349 // Warn admins on admin/index.php page. 1350 $result = $value; 1351 } 1352 1353 } else { 1354 $result = @iconv('UTF-8', 'UTF-8//IGNORE', $value); 1355 } 1356 1357 return $result; 1358 1359 } else if (is_array($value)) { 1360 foreach ($value as $k => $v) { 1361 $value[$k] = fix_utf8($v); 1362 } 1363 return $value; 1364 1365 } else if (is_object($value)) { 1366 // Do not modify original. 1367 $value = clone($value); 1368 foreach ($value as $k => $v) { 1369 $value->$k = fix_utf8($v); 1370 } 1371 return $value; 1372 1373 } else { 1374 // This is some other type, no utf-8 here. 1375 return $value; 1376 } 1377 } 1378 1379 /** 1380 * Return true if given value is integer or string with integer value 1381 * 1382 * @param mixed $value String or Int 1383 * @return bool true if number, false if not 1384 */ 1385 function is_number($value) { 1386 if (is_int($value)) { 1387 return true; 1388 } else if (is_string($value)) { 1389 return ((string)(int)$value) === $value; 1390 } else { 1391 return false; 1392 } 1393 } 1394 1395 /** 1396 * Returns host part from url. 1397 * 1398 * @param string $url full url 1399 * @return string host, null if not found 1400 */ 1401 function get_host_from_url($url) { 1402 preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches); 1403 if ($matches) { 1404 return $matches[1]; 1405 } 1406 return null; 1407 } 1408 1409 /** 1410 * Tests whether anything was returned by text editor 1411 * 1412 * This function is useful for testing whether something you got back from 1413 * the HTML editor actually contains anything. Sometimes the HTML editor 1414 * appear to be empty, but actually you get back a <br> tag or something. 1415 * 1416 * @param string $string a string containing HTML. 1417 * @return boolean does the string contain any actual content - that is text, 1418 * images, objects, etc. 1419 */ 1420 function html_is_blank($string) { 1421 return trim(strip_tags((string)$string, '<img><object><applet><input><select><textarea><hr>')) == ''; 1422 } 1423 1424 /** 1425 * Set a key in global configuration 1426 * 1427 * Set a key/value pair in both this session's {@link $CFG} global variable 1428 * and in the 'config' database table for future sessions. 1429 * 1430 * Can also be used to update keys for plugin-scoped configs in config_plugin table. 1431 * In that case it doesn't affect $CFG. 1432 * 1433 * A NULL value will delete the entry. 1434 * 1435 * NOTE: this function is called from lib/db/upgrade.php 1436 * 1437 * @param string $name the key to set 1438 * @param string $value the value to set (without magic quotes) 1439 * @param string $plugin (optional) the plugin scope, default null 1440 * @return bool true or exception 1441 */ 1442 function set_config($name, $value, $plugin = null) { 1443 global $CFG, $DB; 1444 1445 // Redirect to appropriate handler when value is null. 1446 if ($value === null) { 1447 return unset_config($name, $plugin); 1448 } 1449 1450 // Set variables determining conditions and where to store the new config. 1451 // Plugin config goes to {config_plugins}, core config goes to {config}. 1452 $iscore = empty($plugin); 1453 if ($iscore) { 1454 // If it's for core config. 1455 $table = 'config'; 1456 $conditions = ['name' => $name]; 1457 $invalidatecachekey = 'core'; 1458 } else { 1459 // If it's a plugin. 1460 $table = 'config_plugins'; 1461 $conditions = ['name' => $name, 'plugin' => $plugin]; 1462 $invalidatecachekey = $plugin; 1463 } 1464 1465 // DB handling - checks for existing config, updating or inserting only if necessary. 1466 $invalidatecache = true; 1467 $inserted = false; 1468 $record = $DB->get_record($table, $conditions, 'id, value'); 1469 if ($record === false) { 1470 // Inserts a new config record. 1471 $config = new stdClass(); 1472 $config->name = $name; 1473 $config->value = $value; 1474 if (!$iscore) { 1475 $config->plugin = $plugin; 1476 } 1477 $inserted = $DB->insert_record($table, $config, false); 1478 } else if ($invalidatecache = ($record->value !== $value)) { 1479 // Record exists - Check and only set new value if it has changed. 1480 $DB->set_field($table, 'value', $value, ['id' => $record->id]); 1481 } 1482 1483 if ($iscore && !isset($CFG->config_php_settings[$name])) { 1484 // So it's defined for this invocation at least. 1485 // Settings from db are always strings. 1486 $CFG->$name = (string) $value; 1487 } 1488 1489 // When setting config during a Behat test (in the CLI script, not in the web browser 1490 // requests), remember which ones are set so that we can clear them later. 1491 if ($iscore && $inserted && defined('BEHAT_TEST')) { 1492 $CFG->behat_cli_added_config[$name] = true; 1493 } 1494 1495 // Update siteidentifier cache, if required. 1496 if ($iscore && $name === 'siteidentifier') { 1497 cache_helper::update_site_identifier($value); 1498 } 1499 1500 // Invalidate cache, if required. 1501 if ($invalidatecache) { 1502 cache_helper::invalidate_by_definition('core', 'config', [], $invalidatecachekey); 1503 } 1504 1505 return true; 1506 } 1507 1508 /** 1509 * Get configuration values from the global config table 1510 * or the config_plugins table. 1511 * 1512 * If called with one parameter, it will load all the config 1513 * variables for one plugin, and return them as an object. 1514 * 1515 * If called with 2 parameters it will return a string single 1516 * value or false if the value is not found. 1517 * 1518 * NOTE: this function is called from lib/db/upgrade.php 1519 * 1520 * @param string $plugin full component name 1521 * @param string $name default null 1522 * @return mixed hash-like object or single value, return false no config found 1523 * @throws dml_exception 1524 */ 1525 function get_config($plugin, $name = null) { 1526 global $CFG, $DB; 1527 1528 if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) { 1529 $forced =& $CFG->config_php_settings; 1530 $iscore = true; 1531 $plugin = 'core'; 1532 } else { 1533 if (array_key_exists($plugin, $CFG->forced_plugin_settings)) { 1534 $forced =& $CFG->forced_plugin_settings[$plugin]; 1535 } else { 1536 $forced = array(); 1537 } 1538 $iscore = false; 1539 } 1540 1541 if (!isset($CFG->siteidentifier)) { 1542 try { 1543 // This may throw an exception during installation, which is how we detect the 1544 // need to install the database. For more details see {@see initialise_cfg()}. 1545 $CFG->siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier')); 1546 } catch (dml_exception $ex) { 1547 // Set siteidentifier to false. We don't want to trip this continually. 1548 $siteidentifier = false; 1549 throw $ex; 1550 } 1551 } 1552 1553 if (!empty($name)) { 1554 if (array_key_exists($name, $forced)) { 1555 return (string)$forced[$name]; 1556 } else if ($name === 'siteidentifier' && $plugin == 'core') { 1557 return $CFG->siteidentifier; 1558 } 1559 } 1560 1561 $cache = cache::make('core', 'config'); 1562 $result = $cache->get($plugin); 1563 if ($result === false) { 1564 // The user is after a recordset. 1565 if (!$iscore) { 1566 $result = $DB->get_records_menu('config_plugins', array('plugin' => $plugin), '', 'name,value'); 1567 } else { 1568 // This part is not really used any more, but anyway... 1569 $result = $DB->get_records_menu('config', array(), '', 'name,value');; 1570 } 1571 $cache->set($plugin, $result); 1572 } 1573 1574 if (!empty($name)) { 1575 if (array_key_exists($name, $result)) { 1576 return $result[$name]; 1577 } 1578 return false; 1579 } 1580 1581 if ($plugin === 'core') { 1582 $result['siteidentifier'] = $CFG->siteidentifier; 1583 } 1584 1585 foreach ($forced as $key => $value) { 1586 if (is_null($value) or is_array($value) or is_object($value)) { 1587 // We do not want any extra mess here, just real settings that could be saved in db. 1588 unset($result[$key]); 1589 } else { 1590 // Convert to string as if it went through the DB. 1591 $result[$key] = (string)$value; 1592 } 1593 } 1594 1595 return (object)$result; 1596 } 1597 1598 /** 1599 * Removes a key from global configuration. 1600 * 1601 * NOTE: this function is called from lib/db/upgrade.php 1602 * 1603 * @param string $name the key to set 1604 * @param string $plugin (optional) the plugin scope 1605 * @return boolean whether the operation succeeded. 1606 */ 1607 function unset_config($name, $plugin=null) { 1608 global $CFG, $DB; 1609 1610 if (empty($plugin)) { 1611 unset($CFG->$name); 1612 $DB->delete_records('config', array('name' => $name)); 1613 cache_helper::invalidate_by_definition('core', 'config', array(), 'core'); 1614 } else { 1615 $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin)); 1616 cache_helper::invalidate_by_definition('core', 'config', array(), $plugin); 1617 } 1618 1619 return true; 1620 } 1621 1622 /** 1623 * Remove all the config variables for a given plugin. 1624 * 1625 * NOTE: this function is called from lib/db/upgrade.php 1626 * 1627 * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice'; 1628 * @return boolean whether the operation succeeded. 1629 */ 1630 function unset_all_config_for_plugin($plugin) { 1631 global $DB; 1632 // Delete from the obvious config_plugins first. 1633 $DB->delete_records('config_plugins', array('plugin' => $plugin)); 1634 // Next delete any suspect settings from config. 1635 $like = $DB->sql_like('name', '?', true, true, false, '|'); 1636 $params = array($DB->sql_like_escape($plugin.'_', '|') . '%'); 1637 $DB->delete_records_select('config', $like, $params); 1638 // Finally clear both the plugin cache and the core cache (suspect settings now removed from core). 1639 cache_helper::invalidate_by_definition('core', 'config', array(), array('core', $plugin)); 1640 1641 return true; 1642 } 1643 1644 /** 1645 * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability. 1646 * 1647 * All users are verified if they still have the necessary capability. 1648 * 1649 * @param string $value the value of the config setting. 1650 * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor. 1651 * @param bool $includeadmins include administrators. 1652 * @return array of user objects. 1653 */ 1654 function get_users_from_config($value, $capability, $includeadmins = true) { 1655 if (empty($value) or $value === '$@NONE@$') { 1656 return array(); 1657 } 1658 1659 // We have to make sure that users still have the necessary capability, 1660 // it should be faster to fetch them all first and then test if they are present 1661 // instead of validating them one-by-one. 1662 $users = get_users_by_capability(context_system::instance(), $capability); 1663 if ($includeadmins) { 1664 $admins = get_admins(); 1665 foreach ($admins as $admin) { 1666 $users[$admin->id] = $admin; 1667 } 1668 } 1669 1670 if ($value === '$@ALL@$') { 1671 return $users; 1672 } 1673 1674 $result = array(); // Result in correct order. 1675 $allowed = explode(',', $value); 1676 foreach ($allowed as $uid) { 1677 if (isset($users[$uid])) { 1678 $user = $users[$uid]; 1679 $result[$user->id] = $user; 1680 } 1681 } 1682 1683 return $result; 1684 } 1685 1686 1687 /** 1688 * Invalidates browser caches and cached data in temp. 1689 * 1690 * @return void 1691 */ 1692 function purge_all_caches() { 1693 purge_caches(); 1694 } 1695 1696 /** 1697 * Selectively invalidate different types of cache. 1698 * 1699 * Purges the cache areas specified. By default, this will purge all caches but can selectively purge specific 1700 * areas alone or in combination. 1701 * 1702 * @param bool[] $options Specific parts of the cache to purge. Valid options are: 1703 * 'muc' Purge MUC caches? 1704 * 'theme' Purge theme cache? 1705 * 'lang' Purge language string cache? 1706 * 'js' Purge javascript cache? 1707 * 'filter' Purge text filter cache? 1708 * 'other' Purge all other caches? 1709 */ 1710 function purge_caches($options = []) { 1711 $defaults = array_fill_keys(['muc', 'theme', 'lang', 'js', 'template', 'filter', 'other'], false); 1712 if (empty(array_filter($options))) { 1713 $options = array_fill_keys(array_keys($defaults), true); // Set all options to true. 1714 } else { 1715 $options = array_merge($defaults, array_intersect_key($options, $defaults)); // Override defaults with specified options. 1716 } 1717 if ($options['muc']) { 1718 cache_helper::purge_all(); 1719 } 1720 if ($options['theme']) { 1721 theme_reset_all_caches(); 1722 } 1723 if ($options['lang']) { 1724 get_string_manager()->reset_caches(); 1725 } 1726 if ($options['js']) { 1727 js_reset_all_caches(); 1728 } 1729 if ($options['template']) { 1730 template_reset_all_caches(); 1731 } 1732 if ($options['filter']) { 1733 reset_text_filters_cache(); 1734 } 1735 if ($options['other']) { 1736 purge_other_caches(); 1737 } 1738 } 1739 1740 /** 1741 * Purge all non-MUC caches not otherwise purged in purge_caches. 1742 * 1743 * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at 1744 * {@link phpunit_util::reset_dataroot()} 1745 */ 1746 function purge_other_caches() { 1747 global $DB, $CFG; 1748 if (class_exists('core_plugin_manager')) { 1749 core_plugin_manager::reset_caches(); 1750 } 1751 1752 // Bump up cacherev field for all courses. 1753 try { 1754 increment_revision_number('course', 'cacherev', ''); 1755 } catch (moodle_exception $e) { 1756 // Ignore exception since this function is also called before upgrade script when field course.cacherev does not exist yet. 1757 } 1758 1759 $DB->reset_caches(); 1760 1761 // Purge all other caches: rss, simplepie, etc. 1762 clearstatcache(); 1763 remove_dir($CFG->cachedir.'', true); 1764 1765 // Make sure cache dir is writable, throws exception if not. 1766 make_cache_directory(''); 1767 1768 // This is the only place where we purge local caches, we are only adding files there. 1769 // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes. 1770 remove_dir($CFG->localcachedir, true); 1771 set_config('localcachedirpurged', time()); 1772 make_localcache_directory('', true); 1773 \core\task\manager::clear_static_caches(); 1774 } 1775 1776 /** 1777 * Get volatile flags 1778 * 1779 * @param string $type 1780 * @param int $changedsince default null 1781 * @return array records array 1782 */ 1783 function get_cache_flags($type, $changedsince = null) { 1784 global $DB; 1785 1786 $params = array('type' => $type, 'expiry' => time()); 1787 $sqlwhere = "flagtype = :type AND expiry >= :expiry"; 1788 if ($changedsince !== null) { 1789 $params['changedsince'] = $changedsince; 1790 $sqlwhere .= " AND timemodified > :changedsince"; 1791 } 1792 $cf = array(); 1793 if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) { 1794 foreach ($flags as $flag) { 1795 $cf[$flag->name] = $flag->value; 1796 } 1797 } 1798 return $cf; 1799 } 1800 1801 /** 1802 * Get volatile flags 1803 * 1804 * @param string $type 1805 * @param string $name 1806 * @param int $changedsince default null 1807 * @return string|false The cache flag value or false 1808 */ 1809 function get_cache_flag($type, $name, $changedsince=null) { 1810 global $DB; 1811 1812 $params = array('type' => $type, 'name' => $name, 'expiry' => time()); 1813 1814 $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry"; 1815 if ($changedsince !== null) { 1816 $params['changedsince'] = $changedsince; 1817 $sqlwhere .= " AND timemodified > :changedsince"; 1818 } 1819 1820 return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params); 1821 } 1822 1823 /** 1824 * Set a volatile flag 1825 * 1826 * @param string $type the "type" namespace for the key 1827 * @param string $name the key to set 1828 * @param string $value the value to set (without magic quotes) - null will remove the flag 1829 * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs 1830 * @return bool Always returns true 1831 */ 1832 function set_cache_flag($type, $name, $value, $expiry = null) { 1833 global $DB; 1834 1835 $timemodified = time(); 1836 if ($expiry === null || $expiry < $timemodified) { 1837 $expiry = $timemodified + 24 * 60 * 60; 1838 } else { 1839 $expiry = (int)$expiry; 1840 } 1841 1842 if ($value === null) { 1843 unset_cache_flag($type, $name); 1844 return true; 1845 } 1846 1847 if ($f = $DB->get_record('cache_flags', array('name' => $name, 'flagtype' => $type), '*', IGNORE_MULTIPLE)) { 1848 // This is a potential problem in DEBUG_DEVELOPER. 1849 if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) { 1850 return true; // No need to update. 1851 } 1852 $f->value = $value; 1853 $f->expiry = $expiry; 1854 $f->timemodified = $timemodified; 1855 $DB->update_record('cache_flags', $f); 1856 } else { 1857 $f = new stdClass(); 1858 $f->flagtype = $type; 1859 $f->name = $name; 1860 $f->value = $value; 1861 $f->expiry = $expiry; 1862 $f->timemodified = $timemodified; 1863 $DB->insert_record('cache_flags', $f); 1864 } 1865 return true; 1866 } 1867 1868 /** 1869 * Removes a single volatile flag 1870 * 1871 * @param string $type the "type" namespace for the key 1872 * @param string $name the key to set 1873 * @return bool 1874 */ 1875 function unset_cache_flag($type, $name) { 1876 global $DB; 1877 $DB->delete_records('cache_flags', array('name' => $name, 'flagtype' => $type)); 1878 return true; 1879 } 1880 1881 /** 1882 * Garbage-collect volatile flags 1883 * 1884 * @return bool Always returns true 1885 */ 1886 function gc_cache_flags() { 1887 global $DB; 1888 $DB->delete_records_select('cache_flags', 'expiry < ?', array(time())); 1889 return true; 1890 } 1891 1892 // USER PREFERENCE API. 1893 1894 /** 1895 * Refresh user preference cache. This is used most often for $USER 1896 * object that is stored in session, but it also helps with performance in cron script. 1897 * 1898 * Preferences for each user are loaded on first use on every page, then again after the timeout expires. 1899 * 1900 * @package core 1901 * @category preference 1902 * @access public 1903 * @param stdClass $user User object. Preferences are preloaded into 'preference' property 1904 * @param int $cachelifetime Cache life time on the current page (in seconds) 1905 * @throws coding_exception 1906 * @return null 1907 */ 1908 function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120) { 1909 global $DB; 1910 // Static cache, we need to check on each page load, not only every 2 minutes. 1911 static $loadedusers = array(); 1912 1913 if (!isset($user->id)) { 1914 throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field'); 1915 } 1916 1917 if (empty($user->id) or isguestuser($user->id)) { 1918 // No permanent storage for not-logged-in users and guest. 1919 if (!isset($user->preference)) { 1920 $user->preference = array(); 1921 } 1922 return; 1923 } 1924 1925 $timenow = time(); 1926 1927 if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) { 1928 // Already loaded at least once on this page. Are we up to date? 1929 if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) { 1930 // No need to reload - we are on the same page and we loaded prefs just a moment ago. 1931 return; 1932 1933 } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) { 1934 // No change since the lastcheck on this page. 1935 $user->preference['_lastloaded'] = $timenow; 1936 return; 1937 } 1938 } 1939 1940 // OK, so we have to reload all preferences. 1941 $loadedusers[$user->id] = true; 1942 $user->preference = $DB->get_records_menu('user_preferences', array('userid' => $user->id), '', 'name,value'); // All values. 1943 $user->preference['_lastloaded'] = $timenow; 1944 } 1945 1946 /** 1947 * Called from set/unset_user_preferences, so that the prefs can be correctly reloaded in different sessions. 1948 * 1949 * NOTE: internal function, do not call from other code. 1950 * 1951 * @package core 1952 * @access private 1953 * @param integer $userid the user whose prefs were changed. 1954 */ 1955 function mark_user_preferences_changed($userid) { 1956 global $CFG; 1957 1958 if (empty($userid) or isguestuser($userid)) { 1959 // No cache flags for guest and not-logged-in users. 1960 return; 1961 } 1962 1963 set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout); 1964 } 1965 1966 /** 1967 * Sets a preference for the specified user. 1968 * 1969 * If a $user object is submitted it's 'preference' property is used for the preferences cache. 1970 * 1971 * When additional validation/permission check is needed it is better to use {@see useredit_update_user_preference()} 1972 * 1973 * @package core 1974 * @category preference 1975 * @access public 1976 * @param string $name The key to set as preference for the specified user 1977 * @param string $value The value to set for the $name key in the specified user's 1978 * record, null means delete current value. 1979 * @param stdClass|int|null $user A moodle user object or id, null means current user 1980 * @throws coding_exception 1981 * @return bool Always true or exception 1982 */ 1983 function set_user_preference($name, $value, $user = null) { 1984 global $USER, $DB; 1985 1986 if (empty($name) or is_numeric($name) or $name === '_lastloaded') { 1987 throw new coding_exception('Invalid preference name in set_user_preference() call'); 1988 } 1989 1990 if (is_null($value)) { 1991 // Null means delete current. 1992 return unset_user_preference($name, $user); 1993 } else if (is_object($value)) { 1994 throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed'); 1995 } else if (is_array($value)) { 1996 throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed'); 1997 } 1998 // Value column maximum length is 1333 characters. 1999 $value = (string)$value; 2000 if (core_text::strlen($value) > 1333) { 2001 throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column'); 2002 } 2003 2004 if (is_null($user)) { 2005 $user = $USER; 2006 } else if (isset($user->id)) { 2007 // It is a valid object. 2008 } else if (is_numeric($user)) { 2009 $user = (object)array('id' => (int)$user); 2010 } else { 2011 throw new coding_exception('Invalid $user parameter in set_user_preference() call'); 2012 } 2013 2014 check_user_preferences_loaded($user); 2015 2016 if (empty($user->id) or isguestuser($user->id)) { 2017 // No permanent storage for not-logged-in users and guest. 2018 $user->preference[$name] = $value; 2019 return true; 2020 } 2021 2022 if ($preference = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => $name))) { 2023 if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) { 2024 // Preference already set to this value. 2025 return true; 2026 } 2027 $DB->set_field('user_preferences', 'value', $value, array('id' => $preference->id)); 2028 2029 } else { 2030 $preference = new stdClass(); 2031 $preference->userid = $user->id; 2032 $preference->name = $name; 2033 $preference->value = $value; 2034 $DB->insert_record('user_preferences', $preference); 2035 } 2036 2037 // Update value in cache. 2038 $user->preference[$name] = $value; 2039 // Update the $USER in case where we've not a direct reference to $USER. 2040 if ($user !== $USER && $user->id == $USER->id) { 2041 $USER->preference[$name] = $value; 2042 } 2043 2044 // Set reload flag for other sessions. 2045 mark_user_preferences_changed($user->id); 2046 2047 return true; 2048 } 2049 2050 /** 2051 * Sets a whole array of preferences for the current user 2052 * 2053 * If a $user object is submitted it's 'preference' property is used for the preferences cache. 2054 * 2055 * @package core 2056 * @category preference 2057 * @access public 2058 * @param array $prefarray An array of key/value pairs to be set 2059 * @param stdClass|int|null $user A moodle user object or id, null means current user 2060 * @return bool Always true or exception 2061 */ 2062 function set_user_preferences(array $prefarray, $user = null) { 2063 foreach ($prefarray as $name => $value) { 2064 set_user_preference($name, $value, $user); 2065 } 2066 return true; 2067 } 2068 2069 /** 2070 * Unsets a preference completely by deleting it from the database 2071 * 2072 * If a $user object is submitted it's 'preference' property is used for the preferences cache. 2073 * 2074 * @package core 2075 * @category preference 2076 * @access public 2077 * @param string $name The key to unset as preference for the specified user 2078 * @param stdClass|int|null $user A moodle user object or id, null means current user 2079 * @throws coding_exception 2080 * @return bool Always true or exception 2081 */ 2082 function unset_user_preference($name, $user = null) { 2083 global $USER, $DB; 2084 2085 if (empty($name) or is_numeric($name) or $name === '_lastloaded') { 2086 throw new coding_exception('Invalid preference name in unset_user_preference() call'); 2087 } 2088 2089 if (is_null($user)) { 2090 $user = $USER; 2091 } else if (isset($user->id)) { 2092 // It is a valid object. 2093 } else if (is_numeric($user)) { 2094 $user = (object)array('id' => (int)$user); 2095 } else { 2096 throw new coding_exception('Invalid $user parameter in unset_user_preference() call'); 2097 } 2098 2099 check_user_preferences_loaded($user); 2100 2101 if (empty($user->id) or isguestuser($user->id)) { 2102 // No permanent storage for not-logged-in user and guest. 2103 unset($user->preference[$name]); 2104 return true; 2105 } 2106 2107 // Delete from DB. 2108 $DB->delete_records('user_preferences', array('userid' => $user->id, 'name' => $name)); 2109 2110 // Delete the preference from cache. 2111 unset($user->preference[$name]); 2112 // Update the $USER in case where we've not a direct reference to $USER. 2113 if ($user !== $USER && $user->id == $USER->id) { 2114 unset($USER->preference[$name]); 2115 } 2116 2117 // Set reload flag for other sessions. 2118 mark_user_preferences_changed($user->id); 2119 2120 return true; 2121 } 2122 2123 /** 2124 * Used to fetch user preference(s) 2125 * 2126 * If no arguments are supplied this function will return 2127 * all of the current user preferences as an array. 2128 * 2129 * If a name is specified then this function 2130 * attempts to return that particular preference value. If 2131 * none is found, then the optional value $default is returned, 2132 * otherwise null. 2133 * 2134 * If a $user object is submitted it's 'preference' property is used for the preferences cache. 2135 * 2136 * @package core 2137 * @category preference 2138 * @access public 2139 * @param string $name Name of the key to use in finding a preference value 2140 * @param mixed|null $default Value to be returned if the $name key is not set in the user preferences 2141 * @param stdClass|int|null $user A moodle user object or id, null means current user 2142 * @throws coding_exception 2143 * @return string|mixed|null A string containing the value of a single preference. An 2144 * array with all of the preferences or null 2145 */ 2146 function get_user_preferences($name = null, $default = null, $user = null) { 2147 global $USER; 2148 2149 if (is_null($name)) { 2150 // All prefs. 2151 } else if (is_numeric($name) or $name === '_lastloaded') { 2152 throw new coding_exception('Invalid preference name in get_user_preferences() call'); 2153 } 2154 2155 if (is_null($user)) { 2156 $user = $USER; 2157 } else if (isset($user->id)) { 2158 // Is a valid object. 2159 } else if (is_numeric($user)) { 2160 if ($USER->id == $user) { 2161 $user = $USER; 2162 } else { 2163 $user = (object)array('id' => (int)$user); 2164 } 2165 } else { 2166 throw new coding_exception('Invalid $user parameter in get_user_preferences() call'); 2167 } 2168 2169 check_user_preferences_loaded($user); 2170 2171 if (empty($name)) { 2172 // All values. 2173 return $user->preference; 2174 } else if (isset($user->preference[$name])) { 2175 // The single string value. 2176 return $user->preference[$name]; 2177 } else { 2178 // Default value (null if not specified). 2179 return $default; 2180 } 2181 } 2182 2183 // FUNCTIONS FOR HANDLING TIME. 2184 2185 /** 2186 * Given Gregorian date parts in user time produce a GMT timestamp. 2187 * 2188 * @package core 2189 * @category time 2190 * @param int $year The year part to create timestamp of 2191 * @param int $month The month part to create timestamp of 2192 * @param int $day The day part to create timestamp of 2193 * @param int $hour The hour part to create timestamp of 2194 * @param int $minute The minute part to create timestamp of 2195 * @param int $second The second part to create timestamp of 2196 * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset. 2197 * if 99 then default user's timezone is used {@link https://moodledev.io/docs/apis/subsystems/time#timezone} 2198 * @param bool $applydst Toggle Daylight Saving Time, default true, will be 2199 * applied only if timezone is 99 or string. 2200 * @return int GMT timestamp 2201 */ 2202 function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) { 2203 $date = new DateTime('now', core_date::get_user_timezone_object($timezone)); 2204 $date->setDate((int)$year, (int)$month, (int)$day); 2205 $date->setTime((int)$hour, (int)$minute, (int)$second); 2206 2207 $time = $date->getTimestamp(); 2208 2209 if ($time === false) { 2210 throw new coding_exception('getTimestamp() returned false, please ensure you have passed correct values.'. 2211 ' This can fail if year is more than 2038 and OS is 32 bit windows'); 2212 } 2213 2214 // Moodle BC DST stuff. 2215 if (!$applydst) { 2216 $time += dst_offset_on($time, $timezone); 2217 } 2218 2219 return $time; 2220 2221 } 2222 2223 /** 2224 * Format a date/time (seconds) as weeks, days, hours etc as needed 2225 * 2226 * Given an amount of time in seconds, returns string 2227 * formatted nicely as years, days, hours etc as needed 2228 * 2229 * @package core 2230 * @category time 2231 * @uses MINSECS 2232 * @uses HOURSECS 2233 * @uses DAYSECS 2234 * @uses YEARSECS 2235 * @param int $totalsecs Time in seconds 2236 * @param stdClass $str Should be a time object 2237 * @return string A nicely formatted date/time string 2238 */ 2239 function format_time($totalsecs, $str = null) { 2240 2241 $totalsecs = abs($totalsecs); 2242 2243 if (!$str) { 2244 // Create the str structure the slow way. 2245 $str = new stdClass(); 2246 $str->day = get_string('day'); 2247 $str->days = get_string('days'); 2248 $str->hour = get_string('hour'); 2249 $str->hours = get_string('hours'); 2250 $str->min = get_string('min'); 2251 $str->mins = get_string('mins'); 2252 $str->sec = get_string('sec'); 2253 $str->secs = get_string('secs'); 2254 $str->year = get_string('year'); 2255 $str->years = get_string('years'); 2256 } 2257 2258 $years = floor($totalsecs/YEARSECS); 2259 $remainder = $totalsecs - ($years*YEARSECS); 2260 $days = floor($remainder/DAYSECS); 2261 $remainder = $totalsecs - ($days*DAYSECS); 2262 $hours = floor($remainder/HOURSECS); 2263 $remainder = $remainder - ($hours*HOURSECS); 2264 $mins = floor($remainder/MINSECS); 2265 $secs = $remainder - ($mins*MINSECS); 2266 2267 $ss = ($secs == 1) ? $str->sec : $str->secs; 2268 $sm = ($mins == 1) ? $str->min : $str->mins; 2269 $sh = ($hours == 1) ? $str->hour : $str->hours; 2270 $sd = ($days == 1) ? $str->day : $str->days; 2271 $sy = ($years == 1) ? $str->year : $str->years; 2272 2273 $oyears = ''; 2274 $odays = ''; 2275 $ohours = ''; 2276 $omins = ''; 2277 $osecs = ''; 2278 2279 if ($years) { 2280 $oyears = $years .' '. $sy; 2281 } 2282 if ($days) { 2283 $odays = $days .' '. $sd; 2284 } 2285 if ($hours) { 2286 $ohours = $hours .' '. $sh; 2287 } 2288 if ($mins) { 2289 $omins = $mins .' '. $sm; 2290 } 2291 if ($secs) { 2292 $osecs = $secs .' '. $ss; 2293 } 2294 2295 if ($years) { 2296 return trim($oyears .' '. $odays); 2297 } 2298 if ($days) { 2299 return trim($odays .' '. $ohours); 2300 } 2301 if ($hours) { 2302 return trim($ohours .' '. $omins); 2303 } 2304 if ($mins) { 2305 return trim($omins .' '. $osecs); 2306 } 2307 if ($secs) { 2308 return $osecs; 2309 } 2310 return get_string('now'); 2311 } 2312 2313 /** 2314 * Returns a formatted string that represents a date in user time. 2315 * 2316 * @package core 2317 * @category time 2318 * @param int $date the timestamp in UTC, as obtained from the database. 2319 * @param string $format strftime format. You should probably get this using 2320 * get_string('strftime...', 'langconfig'); 2321 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and 2322 * not 99 then daylight saving will not be added. 2323 * {@link https://moodledev.io/docs/apis/subsystems/time#timezone} 2324 * @param bool $fixday If true (default) then the leading zero from %d is removed. 2325 * If false then the leading zero is maintained. 2326 * @param bool $fixhour If true (default) then the leading zero from %I is removed. 2327 * @return string the formatted date/time. 2328 */ 2329 function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) { 2330 $calendartype = \core_calendar\type_factory::get_calendar_instance(); 2331 return $calendartype->timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour); 2332 } 2333 2334 /** 2335 * Returns a html "time" tag with both the exact user date with timezone information 2336 * as a datetime attribute in the W3C format, and the user readable date and time as text. 2337 * 2338 * @package core 2339 * @category time 2340 * @param int $date the timestamp in UTC, as obtained from the database. 2341 * @param string $format strftime format. You should probably get this using 2342 * get_string('strftime...', 'langconfig'); 2343 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and 2344 * not 99 then daylight saving will not be added. 2345 * {@link https://moodledev.io/docs/apis/subsystems/time#timezone} 2346 * @param bool $fixday If true (default) then the leading zero from %d is removed. 2347 * If false then the leading zero is maintained. 2348 * @param bool $fixhour If true (default) then the leading zero from %I is removed. 2349 * @return string the formatted date/time. 2350 */ 2351 function userdate_htmltime($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) { 2352 $userdatestr = userdate($date, $format, $timezone, $fixday, $fixhour); 2353 if (CLI_SCRIPT && !PHPUNIT_TEST) { 2354 return $userdatestr; 2355 } 2356 $machinedate = new DateTime(); 2357 $machinedate->setTimestamp(intval($date)); 2358 $machinedate->setTimezone(core_date::get_user_timezone_object()); 2359 2360 return html_writer::tag('time', $userdatestr, ['datetime' => $machinedate->format(DateTime::W3C)]); 2361 } 2362 2363 /** 2364 * Returns a formatted date ensuring it is UTF-8. 2365 * 2366 * If we are running under Windows convert to Windows encoding and then back to UTF-8 2367 * (because it's impossible to specify UTF-8 to fetch locale info in Win32). 2368 * 2369 * @param int $date the timestamp - since Moodle 2.9 this is a real UTC timestamp 2370 * @param string $format strftime format. 2371 * @param int|float|string $tz the user timezone 2372 * @return string the formatted date/time. 2373 * @since Moodle 2.3.3 2374 */ 2375 function date_format_string($date, $format, $tz = 99) { 2376 2377 date_default_timezone_set(core_date::get_user_timezone($tz)); 2378 2379 if (date('A', 0) === date('A', HOURSECS * 18)) { 2380 $datearray = getdate($date); 2381 $format = str_replace([ 2382 '%P', 2383 '%p', 2384 ], [ 2385 $datearray['hours'] < 12 ? get_string('am', 'langconfig') : get_string('pm', 'langconfig'), 2386 $datearray['hours'] < 12 ? get_string('amcaps', 'langconfig') : get_string('pmcaps', 'langconfig'), 2387 ], $format); 2388 } 2389 2390 $datestring = core_date::strftime($format, $date); 2391 core_date::set_default_server_timezone(); 2392 2393 return $datestring; 2394 } 2395 2396 /** 2397 * Given a $time timestamp in GMT (seconds since epoch), 2398 * returns an array that represents the Gregorian date in user time 2399 * 2400 * @package core 2401 * @category time 2402 * @param int $time Timestamp in GMT 2403 * @param float|int|string $timezone user timezone 2404 * @return array An array that represents the date in user time 2405 */ 2406 function usergetdate($time, $timezone=99) { 2407 if ($time === null) { 2408 // PHP8 and PHP7 return different results when getdate(null) is called. 2409 // Display warning and cast to 0 to make sure the usergetdate() behaves consistently on all versions of PHP. 2410 // In the future versions of Moodle we may consider adding a strict typehint. 2411 debugging('usergetdate() expects parameter $time to be int, null given', DEBUG_DEVELOPER); 2412 $time = 0; 2413 } 2414 2415 date_default_timezone_set(core_date::get_user_timezone($timezone)); 2416 $result = getdate($time); 2417 core_date::set_default_server_timezone(); 2418 2419 return $result; 2420 } 2421 2422 /** 2423 * Given a GMT timestamp (seconds since epoch), offsets it by 2424 * the timezone. eg 3pm in India is 3pm GMT - 7 * 3600 seconds 2425 * 2426 * NOTE: this function does not include DST properly, 2427 * you should use the PHP date stuff instead! 2428 * 2429 * @package core 2430 * @category time 2431 * @param int $date Timestamp in GMT 2432 * @param float|int|string $timezone user timezone 2433 * @return int 2434 */ 2435 function usertime($date, $timezone=99) { 2436 $userdate = new DateTime('@' . $date); 2437 $userdate->setTimezone(core_date::get_user_timezone_object($timezone)); 2438 $dst = dst_offset_on($date, $timezone); 2439 2440 return $date - $userdate->getOffset() + $dst; 2441 } 2442 2443 /** 2444 * Get a formatted string representation of an interval between two unix timestamps. 2445 * 2446 * E.g. 2447 * $intervalstring = get_time_interval_string(12345600, 12345660); 2448 * Will produce the string: 2449 * '0d 0h 1m' 2450 * 2451 * @param int $time1 unix timestamp 2452 * @param int $time2 unix timestamp 2453 * @param string $format string (can be lang string) containing format chars: https://www.php.net/manual/en/dateinterval.format.php. 2454 * @param bool $dropzeroes If format is not provided and this is set to true, do not include zero time units. 2455 * e.g. a duration of 3 days and 2 hours will be displayed as '3d 2h' instead of '3d 2h 0s' 2456 * @param bool $fullformat If format is not provided and this is set to true, display time units in full format. 2457 * e.g. instead of showing "3d", "3 days" will be returned. 2458 * @return string the formatted string describing the time difference, e.g. '10d 11h 45m'. 2459 */ 2460 function get_time_interval_string(int $time1, int $time2, string $format = '', 2461 bool $dropzeroes = false, bool $fullformat = false): string { 2462 $dtdate = new DateTime(); 2463 $dtdate->setTimeStamp($time1); 2464 $dtdate2 = new DateTime(); 2465 $dtdate2->setTimeStamp($time2); 2466 $interval = $dtdate2->diff($dtdate); 2467 2468 if (empty(trim($format))) { 2469 // Default to this key. 2470 $formatkey = 'dateintervaldayhrmin'; 2471 2472 if ($dropzeroes) { 2473 $units = [ 2474 'y' => 'yr', 2475 'm' => 'mo', 2476 'd' => 'day', 2477 'h' => 'hr', 2478 'i' => 'min', 2479 's' => 'sec', 2480 ]; 2481 $formatunits = []; 2482 foreach ($units as $key => $unit) { 2483 if (empty($interval->$key)) { 2484 continue; 2485 } 2486 $formatunits[] = $unit; 2487 } 2488 if (!empty($formatunits)) { 2489 $formatkey = 'dateinterval' . implode("", $formatunits); 2490 } 2491 } 2492 2493 if ($fullformat) { 2494 $formatkey .= 'full'; 2495 } 2496 $format = get_string($formatkey, 'langconfig'); 2497 } 2498 return $interval->format($format); 2499 } 2500 2501 /** 2502 * Given a time, return the GMT timestamp of the most recent midnight 2503 * for the current user. 2504 * 2505 * @package core 2506 * @category time 2507 * @param int $date Timestamp in GMT 2508 * @param float|int|string $timezone user timezone 2509 * @return int Returns a GMT timestamp 2510 */ 2511 function usergetmidnight($date, $timezone=99) { 2512 2513 $userdate = usergetdate($date, $timezone); 2514 2515 // Time of midnight of this user's day, in GMT. 2516 return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone); 2517 2518 } 2519 2520 /** 2521 * Returns a string that prints the user's timezone 2522 * 2523 * @package core 2524 * @category time 2525 * @param float|int|string $timezone user timezone 2526 * @return string 2527 */ 2528 function usertimezone($timezone=99) { 2529 $tz = core_date::get_user_timezone($timezone); 2530 return core_date::get_localised_timezone($tz); 2531 } 2532 2533 /** 2534 * Returns a float or a string which denotes the user's timezone 2535 * 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) 2536 * means that for this timezone there are also DST rules to be taken into account 2537 * Checks various settings and picks the most dominant of those which have a value 2538 * 2539 * @package core 2540 * @category time 2541 * @param float|int|string $tz timezone to calculate GMT time offset before 2542 * calculating user timezone, 99 is default user timezone 2543 * {@link https://moodledev.io/docs/apis/subsystems/time#timezone} 2544 * @return float|string 2545 */ 2546 function get_user_timezone($tz = 99) { 2547 global $USER, $CFG; 2548 2549 $timezones = array( 2550 $tz, 2551 isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99, 2552 isset($USER->timezone) ? $USER->timezone : 99, 2553 isset($CFG->timezone) ? $CFG->timezone : 99, 2554 ); 2555 2556 $tz = 99; 2557 2558 // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array. 2559 foreach ($timezones as $nextvalue) { 2560 if ((empty($tz) && !is_numeric($tz)) || $tz == 99) { 2561 $tz = $nextvalue; 2562 } 2563 } 2564 return is_numeric($tz) ? (float) $tz : $tz; 2565 } 2566 2567 /** 2568 * Calculates the Daylight Saving Offset for a given date/time (timestamp) 2569 * - Note: Daylight saving only works for string timezones and not for float. 2570 * 2571 * @package core 2572 * @category time 2573 * @param int $time must NOT be compensated at all, it has to be a pure timestamp 2574 * @param int|float|string $strtimezone user timezone 2575 * @return int 2576 */ 2577 function dst_offset_on($time, $strtimezone = null) { 2578 $tz = core_date::get_user_timezone($strtimezone); 2579 $date = new DateTime('@' . $time); 2580 $date->setTimezone(new DateTimeZone($tz)); 2581 if ($date->format('I') == '1') { 2582 if ($tz === 'Australia/Lord_Howe') { 2583 return 1800; 2584 } 2585 return 3600; 2586 } 2587 return 0; 2588 } 2589 2590 /** 2591 * Calculates when the day appears in specific month 2592 * 2593 * @package core 2594 * @category time 2595 * @param int $startday starting day of the month 2596 * @param int $weekday The day when week starts (normally taken from user preferences) 2597 * @param int $month The month whose day is sought 2598 * @param int $year The year of the month whose day is sought 2599 * @return int 2600 */ 2601 function find_day_in_month($startday, $weekday, $month, $year) { 2602 $calendartype = \core_calendar\type_factory::get_calendar_instance(); 2603 2604 $daysinmonth = days_in_month($month, $year); 2605 $daysinweek = count($calendartype->get_weekdays()); 2606 2607 if ($weekday == -1) { 2608 // Don't care about weekday, so return: 2609 // abs($startday) if $startday != -1 2610 // $daysinmonth otherwise. 2611 return ($startday == -1) ? $daysinmonth : abs($startday); 2612 } 2613 2614 // From now on we 're looking for a specific weekday. 2615 // Give "end of month" its actual value, since we know it. 2616 if ($startday == -1) { 2617 $startday = -1 * $daysinmonth; 2618 } 2619 2620 // Starting from day $startday, the sign is the direction. 2621 if ($startday < 1) { 2622 $startday = abs($startday); 2623 $lastmonthweekday = dayofweek($daysinmonth, $month, $year); 2624 2625 // This is the last such weekday of the month. 2626 $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday; 2627 if ($lastinmonth > $daysinmonth) { 2628 $lastinmonth -= $daysinweek; 2629 } 2630 2631 // Find the first such weekday <= $startday. 2632 while ($lastinmonth > $startday) { 2633 $lastinmonth -= $daysinweek; 2634 } 2635 2636 return $lastinmonth; 2637 } else { 2638 $indexweekday = dayofweek($startday, $month, $year); 2639 2640 $diff = $weekday - $indexweekday; 2641 if ($diff < 0) { 2642 $diff += $daysinweek; 2643 } 2644 2645 // This is the first such weekday of the month equal to or after $startday. 2646 $firstfromindex = $startday + $diff; 2647 2648 return $firstfromindex; 2649 } 2650 } 2651 2652 /** 2653 * Calculate the number of days in a given month 2654 * 2655 * @package core 2656 * @category time 2657 * @param int $month The month whose day count is sought 2658 * @param int $year The year of the month whose day count is sought 2659 * @return int 2660 */ 2661 function days_in_month($month, $year) { 2662 $calendartype = \core_calendar\type_factory::get_calendar_instance(); 2663 return $calendartype->get_num_days_in_month($year, $month); 2664 } 2665 2666 /** 2667 * Calculate the position in the week of a specific calendar day 2668 * 2669 * @package core 2670 * @category time 2671 * @param int $day The day of the date whose position in the week is sought 2672 * @param int $month The month of the date whose position in the week is sought 2673 * @param int $year The year of the date whose position in the week is sought 2674 * @return int 2675 */ 2676 function dayofweek($day, $month, $year) { 2677 $calendartype = \core_calendar\type_factory::get_calendar_instance(); 2678 return $calendartype->get_weekday($year, $month, $day); 2679 } 2680 2681 // USER AUTHENTICATION AND LOGIN. 2682 2683 /** 2684 * Returns full login url. 2685 * 2686 * Any form submissions for authentication to this URL must include username, 2687 * password as well as a logintoken generated by \core\session\manager::get_login_token(). 2688 * 2689 * @return string login url 2690 */ 2691 function get_login_url() { 2692 global $CFG; 2693 2694 return "$CFG->wwwroot/login/index.php"; 2695 } 2696 2697 /** 2698 * This function checks that the current user is logged in and has the 2699 * required privileges 2700 * 2701 * This function checks that the current user is logged in, and optionally 2702 * whether they are allowed to be in a particular course and view a particular 2703 * course module. 2704 * If they are not logged in, then it redirects them to the site login unless 2705 * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which 2706 * case they are automatically logged in as guests. 2707 * If $courseid is given and the user is not enrolled in that course then the 2708 * user is redirected to the course enrolment page. 2709 * If $cm is given and the course module is hidden and the user is not a teacher 2710 * in the course then the user is redirected to the course home page. 2711 * 2712 * When $cm parameter specified, this function sets page layout to 'module'. 2713 * You need to change it manually later if some other layout needed. 2714 * 2715 * @package core_access 2716 * @category access 2717 * 2718 * @param mixed $courseorid id of the course or course object 2719 * @param bool $autologinguest default true 2720 * @param object $cm course module object 2721 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to 2722 * true. Used to avoid (=false) some scripts (file.php...) to set that variable, 2723 * in order to keep redirects working properly. MDL-14495 2724 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions 2725 * @return mixed Void, exit, and die depending on path 2726 * @throws coding_exception 2727 * @throws require_login_exception 2728 * @throws moodle_exception 2729 */ 2730 function require_login($courseorid = null, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) { 2731 global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT; 2732 2733 // Must not redirect when byteserving already started. 2734 if (!empty($_SERVER['HTTP_RANGE'])) { 2735 $preventredirect = true; 2736 } 2737 2738 if (AJAX_SCRIPT) { 2739 // We cannot redirect for AJAX scripts either. 2740 $preventredirect = true; 2741 } 2742 2743 // Setup global $COURSE, themes, language and locale. 2744 if (!empty($courseorid)) { 2745 if (is_object($courseorid)) { 2746 $course = $courseorid; 2747 } else if ($courseorid == SITEID) { 2748 $course = clone($SITE); 2749 } else { 2750 $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST); 2751 } 2752 if ($cm) { 2753 if ($cm->course != $course->id) { 2754 throw new coding_exception('course and cm parameters in require_login() call do not match!!'); 2755 } 2756 // Make sure we have a $cm from get_fast_modinfo as this contains activity access details. 2757 if (!($cm instanceof cm_info)) { 2758 // Note: nearly all pages call get_fast_modinfo anyway and it does not make any 2759 // db queries so this is not really a performance concern, however it is obviously 2760 // better if you use get_fast_modinfo to get the cm before calling this. 2761 $modinfo = get_fast_modinfo($course); 2762 $cm = $modinfo->get_cm($cm->id); 2763 } 2764 } 2765 } else { 2766 // Do not touch global $COURSE via $PAGE->set_course(), 2767 // the reasons is we need to be able to call require_login() at any time!! 2768 $course = $SITE; 2769 if ($cm) { 2770 throw new coding_exception('cm parameter in require_login() requires valid course parameter!'); 2771 } 2772 } 2773 2774 // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false. 2775 // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future 2776 // risk leading the user back to the AJAX request URL. 2777 if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) { 2778 $setwantsurltome = false; 2779 } 2780 2781 // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour. 2782 if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !empty($CFG->dbsessions)) { 2783 if ($preventredirect) { 2784 throw new require_login_session_timeout_exception(); 2785 } else { 2786 if ($setwantsurltome) { 2787 $SESSION->wantsurl = qualified_me(); 2788 } 2789 redirect(get_login_url()); 2790 } 2791 } 2792 2793 // If the user is not even logged in yet then make sure they are. 2794 if (!isloggedin()) { 2795 if ($autologinguest && !empty($CFG->autologinguests)) { 2796 if (!$guest = get_complete_user_data('id', $CFG->siteguest)) { 2797 // Misconfigured site guest, just redirect to login page. 2798 redirect(get_login_url()); 2799 exit; // Never reached. 2800 } 2801 $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang; 2802 complete_user_login($guest); 2803 $USER->autologinguest = true; 2804 $SESSION->lang = $lang; 2805 } else { 2806 // NOTE: $USER->site check was obsoleted by session test cookie, $USER->confirmed test is in login/index.php. 2807 if ($preventredirect) { 2808 throw new require_login_exception('You are not logged in'); 2809 } 2810 2811 if ($setwantsurltome) { 2812 $SESSION->wantsurl = qualified_me(); 2813 } 2814 2815 // Give auth plugins an opportunity to authenticate or redirect to an external login page 2816 $authsequence = get_enabled_auth_plugins(); // Auths, in sequence. 2817 foreach($authsequence as $authname) { 2818 $authplugin = get_auth_plugin($authname); 2819 $authplugin->pre_loginpage_hook(); 2820 if (isloggedin()) { 2821 if ($cm) { 2822 $modinfo = get_fast_modinfo($course); 2823 $cm = $modinfo->get_cm($cm->id); 2824 } 2825 set_access_log_user(); 2826 break; 2827 } 2828 } 2829 2830 // If we're still not logged in then go to the login page 2831 if (!isloggedin()) { 2832 redirect(get_login_url()); 2833 exit; // Never reached. 2834 } 2835 } 2836 } 2837 2838 // Loginas as redirection if needed. 2839 if ($course->id != SITEID and \core\session\manager::is_loggedinas()) { 2840 if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) { 2841 if ($USER->loginascontext->instanceid != $course->id) { 2842 throw new \moodle_exception('loginasonecourse', '', 2843 $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid); 2844 } 2845 } 2846 } 2847 2848 // Check whether the user should be changing password (but only if it is REALLY them). 2849 if (get_user_preferences('auth_forcepasswordchange') && !\core\session\manager::is_loggedinas()) { 2850 $userauth = get_auth_plugin($USER->auth); 2851 if ($userauth->can_change_password() and !$preventredirect) { 2852 if ($setwantsurltome) { 2853 $SESSION->wantsurl = qualified_me(); 2854 } 2855 if ($changeurl = $userauth->change_password_url()) { 2856 // Use plugin custom url. 2857 redirect($changeurl); 2858 } else { 2859 // Use moodle internal method. 2860 redirect($CFG->wwwroot .'/login/change_password.php'); 2861 } 2862 } else if ($userauth->can_change_password()) { 2863 throw new moodle_exception('forcepasswordchangenotice'); 2864 } else { 2865 throw new moodle_exception('nopasswordchangeforced', 'auth'); 2866 } 2867 } 2868 2869 // Check that the user account is properly set up. If we can't redirect to 2870 // edit their profile and this is not a WS request, perform just the lax check. 2871 // It will allow them to use filepicker on the profile edit page. 2872 2873 if ($preventredirect && !WS_SERVER) { 2874 $usernotfullysetup = user_not_fully_set_up($USER, false); 2875 } else { 2876 $usernotfullysetup = user_not_fully_set_up($USER, true); 2877 } 2878 2879 if ($usernotfullysetup) { 2880 if ($preventredirect) { 2881 throw new moodle_exception('usernotfullysetup'); 2882 } 2883 if ($setwantsurltome) { 2884 $SESSION->wantsurl = qualified_me(); 2885 } 2886 redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&course='. SITEID); 2887 } 2888 2889 // Make sure the USER has a sesskey set up. Used for CSRF protection. 2890 sesskey(); 2891 2892 if (\core\session\manager::is_loggedinas()) { 2893 // During a "logged in as" session we should force all content to be cleaned because the 2894 // logged in user will be viewing potentially malicious user generated content. 2895 // See MDL-63786 for more details. 2896 $CFG->forceclean = true; 2897 } 2898 2899 $afterlogins = get_plugins_with_function('after_require_login', 'lib.php'); 2900 2901 // Do not bother admins with any formalities, except for activities pending deletion. 2902 if (is_siteadmin() && !($cm && $cm->deletioninprogress)) { 2903 // Set the global $COURSE. 2904 if ($cm) { 2905 $PAGE->set_cm($cm, $course); 2906 $PAGE->set_pagelayout('incourse'); 2907 } else if (!empty($courseorid)) { 2908 $PAGE->set_course($course); 2909 } 2910 // Set accesstime or the user will appear offline which messes up messaging. 2911 // Do not update access time for webservice or ajax requests. 2912 if (!WS_SERVER && !AJAX_SCRIPT) { 2913 user_accesstime_log($course->id); 2914 } 2915 2916 foreach ($afterlogins as $plugintype => $plugins) { 2917 foreach ($plugins as $pluginfunction) { 2918 $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); 2919 } 2920 } 2921 return; 2922 } 2923 2924 // Scripts have a chance to declare that $USER->policyagreed should not be checked. 2925 // This is mostly for places where users are actually accepting the policies, to avoid the redirect loop. 2926 if (!defined('NO_SITEPOLICY_CHECK')) { 2927 define('NO_SITEPOLICY_CHECK', false); 2928 } 2929 2930 // Check that the user has agreed to a site policy if there is one - do not test in case of admins. 2931 // Do not test if the script explicitly asked for skipping the site policies check. 2932 // Or if the user auth type is webservice. 2933 if (!$USER->policyagreed && !is_siteadmin() && !NO_SITEPOLICY_CHECK && $USER->auth !== 'webservice') { 2934 $manager = new \core_privacy\local\sitepolicy\manager(); 2935 if ($policyurl = $manager->get_redirect_url(isguestuser())) { 2936 if ($preventredirect) { 2937 throw new moodle_exception('sitepolicynotagreed', 'error', '', $policyurl->out()); 2938 } 2939 if ($setwantsurltome) { 2940 $SESSION->wantsurl = qualified_me(); 2941 } 2942 redirect($policyurl); 2943 } 2944 } 2945 2946 // Fetch the system context, the course context, and prefetch its child contexts. 2947 $sysctx = context_system::instance(); 2948 $coursecontext = context_course::instance($course->id, MUST_EXIST); 2949 if ($cm) { 2950 $cmcontext = context_module::instance($cm->id, MUST_EXIST); 2951 } else { 2952 $cmcontext = null; 2953 } 2954 2955 // If the site is currently under maintenance, then print a message. 2956 if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:maintenanceaccess', $sysctx)) { 2957 if ($preventredirect) { 2958 throw new require_login_exception('Maintenance in progress'); 2959 } 2960 $PAGE->set_context(null); 2961 print_maintenance_message(); 2962 } 2963 2964 // Make sure the course itself is not hidden. 2965 if ($course->id == SITEID) { 2966 // Frontpage can not be hidden. 2967 } else { 2968 if (is_role_switched($course->id)) { 2969 // When switching roles ignore the hidden flag - user had to be in course to do the switch. 2970 } else { 2971 if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) { 2972 // Originally there was also test of parent category visibility, BUT is was very slow in complex queries 2973 // involving "my courses" now it is also possible to simply hide all courses user is not enrolled in :-). 2974 if ($preventredirect) { 2975 throw new require_login_exception('Course is hidden'); 2976 } 2977 $PAGE->set_context(null); 2978 // We need to override the navigation URL as the course won't have been added to the navigation and thus 2979 // the navigation will mess up when trying to find it. 2980 navigation_node::override_active_url(new moodle_url('/')); 2981 notice(get_string('coursehidden'), $CFG->wwwroot .'/'); 2982 } 2983 } 2984 } 2985 2986 // Is the user enrolled? 2987 if ($course->id == SITEID) { 2988 // Everybody is enrolled on the frontpage. 2989 } else { 2990 if (\core\session\manager::is_loggedinas()) { 2991 // Make sure the REAL person can access this course first. 2992 $realuser = \core\session\manager::get_realuser(); 2993 if (!is_enrolled($coursecontext, $realuser->id, '', true) and 2994 !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) { 2995 if ($preventredirect) { 2996 throw new require_login_exception('Invalid course login-as access'); 2997 } 2998 $PAGE->set_context(null); 2999 echo $OUTPUT->header(); 3000 notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/'); 3001 } 3002 } 3003 3004 $access = false; 3005 3006 if (is_role_switched($course->id)) { 3007 // Ok, user had to be inside this course before the switch. 3008 $access = true; 3009 3010 } else if (is_viewing($coursecontext, $USER)) { 3011 // Ok, no need to mess with enrol. 3012 $access = true; 3013 3014 } else { 3015 if (isset($USER->enrol['enrolled'][$course->id])) { 3016 if ($USER->enrol['enrolled'][$course->id] > time()) { 3017 $access = true; 3018 if (isset($USER->enrol['tempguest'][$course->id])) { 3019 unset($USER->enrol['tempguest'][$course->id]); 3020 remove_temp_course_roles($coursecontext); 3021 } 3022 } else { 3023 // Expired. 3024 unset($USER->enrol['enrolled'][$course->id]); 3025 } 3026 } 3027 if (isset($USER->enrol['tempguest'][$course->id])) { 3028 if ($USER->enrol['tempguest'][$course->id] == 0) { 3029 $access = true; 3030 } else if ($USER->enrol['tempguest'][$course->id] > time()) { 3031 $access = true; 3032 } else { 3033 // Expired. 3034 unset($USER->enrol['tempguest'][$course->id]); 3035 remove_temp_course_roles($coursecontext); 3036 } 3037 } 3038 3039 if (!$access) { 3040 // Cache not ok. 3041 $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id); 3042 if ($until !== false) { 3043 // Active participants may always access, a timestamp in the future, 0 (always) or false. 3044 if ($until == 0) { 3045 $until = ENROL_MAX_TIMESTAMP; 3046 } 3047 $USER->enrol['enrolled'][$course->id] = $until; 3048 $access = true; 3049 3050 } else if (core_course_category::can_view_course_info($course)) { 3051 $params = array('courseid' => $course->id, 'status' => ENROL_INSTANCE_ENABLED); 3052 $instances = $DB->get_records('enrol', $params, 'sortorder, id ASC'); 3053 $enrols = enrol_get_plugins(true); 3054 // First ask all enabled enrol instances in course if they want to auto enrol user. 3055 foreach ($instances as $instance) { 3056 if (!isset($enrols[$instance->enrol])) { 3057 continue; 3058 } 3059 // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false. 3060 $until = $enrols[$instance->enrol]->try_autoenrol($instance); 3061 if ($until !== false) { 3062 if ($until == 0) { 3063 $until = ENROL_MAX_TIMESTAMP; 3064 } 3065 $USER->enrol['enrolled'][$course->id] = $until; 3066 $access = true; 3067 break; 3068 } 3069 } 3070 // If not enrolled yet try to gain temporary guest access. 3071 if (!$access) { 3072 foreach ($instances as $instance) { 3073 if (!isset($enrols[$instance->enrol])) { 3074 continue; 3075 } 3076 // Get a duration for the guest access, a timestamp in the future or false. 3077 $until = $enrols[$instance->enrol]->try_guestaccess($instance); 3078 if ($until !== false and $until > time()) { 3079 $USER->enrol['tempguest'][$course->id] = $until; 3080 $access = true; 3081 break; 3082 } 3083 } 3084 } 3085 } else { 3086 // User is not enrolled and is not allowed to browse courses here. 3087 if ($preventredirect) { 3088 throw new require_login_exception('Course is not available'); 3089 } 3090 $PAGE->set_context(null); 3091 // We need to override the navigation URL as the course won't have been added to the navigation and thus 3092 // the navigation will mess up when trying to find it. 3093 navigation_node::override_active_url(new moodle_url('/')); 3094 notice(get_string('coursehidden'), $CFG->wwwroot .'/'); 3095 } 3096 } 3097 } 3098 3099 if (!$access) { 3100 if ($preventredirect) { 3101 throw new require_login_exception('Not enrolled'); 3102 } 3103 if ($setwantsurltome) { 3104 $SESSION->wantsurl = qualified_me(); 3105 } 3106 redirect($CFG->wwwroot .'/enrol/index.php?id='. $course->id); 3107 } 3108 } 3109 3110 // Check whether the activity has been scheduled for deletion. If so, then deny access, even for admins. 3111 if ($cm && $cm->deletioninprogress) { 3112 if ($preventredirect) { 3113 throw new moodle_exception('activityisscheduledfordeletion'); 3114 } 3115 require_once($CFG->dirroot . '/course/lib.php'); 3116 redirect(course_get_url($course), get_string('activityisscheduledfordeletion', 'error')); 3117 } 3118 3119 // Check visibility of activity to current user; includes visible flag, conditional availability, etc. 3120 if ($cm && !$cm->uservisible) { 3121 if ($preventredirect) { 3122 throw new require_login_exception('Activity is hidden'); 3123 } 3124 // Get the error message that activity is not available and why (if explanation can be shown to the user). 3125 $PAGE->set_course($course); 3126 $renderer = $PAGE->get_renderer('course'); 3127 $message = $renderer->course_section_cm_unavailable_error_message($cm); 3128 redirect(course_get_url($course), $message, null, \core\output\notification::NOTIFY_ERROR); 3129 } 3130 3131 // Set the global $COURSE. 3132 if ($cm) { 3133 $PAGE->set_cm($cm, $course); 3134 $PAGE->set_pagelayout('incourse'); 3135 } else if (!empty($courseorid)) { 3136 $PAGE->set_course($course); 3137 } 3138 3139 foreach ($afterlogins as $plugintype => $plugins) { 3140 foreach ($plugins as $pluginfunction) { 3141 $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); 3142 } 3143 } 3144 3145 // Finally access granted, update lastaccess times. 3146 // Do not update access time for webservice or ajax requests. 3147 if (!WS_SERVER && !AJAX_SCRIPT) { 3148 user_accesstime_log($course->id); 3149 } 3150 } 3151 3152 /** 3153 * A convenience function for where we must be logged in as admin 3154 * @return void 3155 */ 3156 function require_admin() { 3157 require_login(null, false); 3158 require_capability('moodle/site:config', context_system::instance()); 3159 } 3160 3161 /** 3162 * This function just makes sure a user is logged out. 3163 * 3164 * @package core_access 3165 * @category access 3166 */ 3167 function require_logout() { 3168 global $USER, $DB; 3169 3170 if (!isloggedin()) { 3171 // This should not happen often, no need for hooks or events here. 3172 \core\session\manager::terminate_current(); 3173 return; 3174 } 3175 3176 // Execute hooks before action. 3177 $authplugins = array(); 3178 $authsequence = get_enabled_auth_plugins(); 3179 foreach ($authsequence as $authname) { 3180 $authplugins[$authname] = get_auth_plugin($authname); 3181 $authplugins[$authname]->prelogout_hook(); 3182 } 3183 3184 // Store info that gets removed during logout. 3185 $sid = session_id(); 3186 $event = \core\event\user_loggedout::create( 3187 array( 3188 'userid' => $USER->id, 3189 'objectid' => $USER->id, 3190 'other' => array('sessionid' => $sid), 3191 ) 3192 ); 3193 if ($session = $DB->get_record('sessions', array('sid'=>$sid))) { 3194 $event->add_record_snapshot('sessions', $session); 3195 } 3196 3197 // Clone of $USER object to be used by auth plugins. 3198 $user = fullclone($USER); 3199 3200 // Delete session record and drop $_SESSION content. 3201 \core\session\manager::terminate_current(); 3202 3203 // Trigger event AFTER action. 3204 $event->trigger(); 3205 3206 // Hook to execute auth plugins redirection after event trigger. 3207 foreach ($authplugins as $authplugin) { 3208 $authplugin->postlogout_hook($user); 3209 } 3210 } 3211 3212 /** 3213 * Weaker version of require_login() 3214 * 3215 * This is a weaker version of {@link require_login()} which only requires login 3216 * when called from within a course rather than the site page, unless 3217 * the forcelogin option is turned on. 3218 * @see require_login() 3219 * 3220 * @package core_access 3221 * @category access 3222 * 3223 * @param mixed $courseorid The course object or id in question 3224 * @param bool $autologinguest Allow autologin guests if that is wanted 3225 * @param object $cm Course activity module if known 3226 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to 3227 * true. Used to avoid (=false) some scripts (file.php...) to set that variable, 3228 * in order to keep redirects working properly. MDL-14495 3229 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions 3230 * @return void 3231 * @throws coding_exception 3232 */ 3233 function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) { 3234 global $CFG, $PAGE, $SITE; 3235 $issite = ((is_object($courseorid) and $courseorid->id == SITEID) 3236 or (!is_object($courseorid) and $courseorid == SITEID)); 3237 if ($issite && !empty($cm) && !($cm instanceof cm_info)) { 3238 // Note: nearly all pages call get_fast_modinfo anyway and it does not make any 3239 // db queries so this is not really a performance concern, however it is obviously 3240 // better if you use get_fast_modinfo to get the cm before calling this. 3241 if (is_object($courseorid)) { 3242 $course = $courseorid; 3243 } else { 3244 $course = clone($SITE); 3245 } 3246 $modinfo = get_fast_modinfo($course); 3247 $cm = $modinfo->get_cm($cm->id); 3248 } 3249 if (!empty($CFG->forcelogin)) { 3250 // Login required for both SITE and courses. 3251 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); 3252 3253 } else if ($issite && !empty($cm) and !$cm->uservisible) { 3254 // Always login for hidden activities. 3255 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); 3256 3257 } else if (isloggedin() && !isguestuser()) { 3258 // User is already logged in. Make sure the login is complete (user is fully setup, policies agreed). 3259 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); 3260 3261 } else if ($issite) { 3262 // Login for SITE not required. 3263 // We still need to instatiate PAGE vars properly so that things that rely on it like navigation function correctly. 3264 if (!empty($courseorid)) { 3265 if (is_object($courseorid)) { 3266 $course = $courseorid; 3267 } else { 3268 $course = clone $SITE; 3269 } 3270 if ($cm) { 3271 if ($cm->course != $course->id) { 3272 throw new coding_exception('course and cm parameters in require_course_login() call do not match!!'); 3273 } 3274 $PAGE->set_cm($cm, $course); 3275 $PAGE->set_pagelayout('incourse'); 3276 } else { 3277 $PAGE->set_course($course); 3278 } 3279 } else { 3280 // If $PAGE->course, and hence $PAGE->context, have not already been set up properly, set them up now. 3281 $PAGE->set_course($PAGE->course); 3282 } 3283 // Do not update access time for webservice or ajax requests. 3284 if (!WS_SERVER && !AJAX_SCRIPT) { 3285 user_accesstime_log(SITEID); 3286 } 3287 return; 3288 3289 } else { 3290 // Course login always required. 3291 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); 3292 } 3293 } 3294 3295 /** 3296 * Validates a user key, checking if the key exists, is not expired and the remote ip is correct. 3297 * 3298 * @param string $keyvalue the key value 3299 * @param string $script unique script identifier 3300 * @param int $instance instance id 3301 * @return stdClass the key entry in the user_private_key table 3302 * @since Moodle 3.2 3303 * @throws moodle_exception 3304 */ 3305 function validate_user_key($keyvalue, $script, $instance) { 3306 global $DB; 3307 3308 if (!$key = $DB->get_record('user_private_key', array('script' => $script, 'value' => $keyvalue, 'instance' => $instance))) { 3309 throw new \moodle_exception('invalidkey'); 3310 } 3311 3312 if (!empty($key->validuntil) and $key->validuntil < time()) { 3313 throw new \moodle_exception('expiredkey'); 3314 } 3315 3316 if ($key->iprestriction) { 3317 $remoteaddr = getremoteaddr(null); 3318 if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) { 3319 throw new \moodle_exception('ipmismatch'); 3320 } 3321 } 3322 return $key; 3323 } 3324 3325 /** 3326 * Require key login. Function terminates with error if key not found or incorrect. 3327 * 3328 * @uses NO_MOODLE_COOKIES 3329 * @uses PARAM_ALPHANUM 3330 * @param string $script unique script identifier 3331 * @param int $instance optional instance id 3332 * @param string $keyvalue The key. If not supplied, this will be fetched from the current session. 3333 * @return int Instance ID 3334 */ 3335 function require_user_key_login($script, $instance = null, $keyvalue = null) { 3336 global $DB; 3337 3338 if (!NO_MOODLE_COOKIES) { 3339 throw new \moodle_exception('sessioncookiesdisable'); 3340 } 3341 3342 // Extra safety. 3343 \core\session\manager::write_close(); 3344 3345 if (null === $keyvalue) { 3346 $keyvalue = required_param('key', PARAM_ALPHANUM); 3347 } 3348 3349 $key = validate_user_key($keyvalue, $script, $instance); 3350 3351 if (!$user = $DB->get_record('user', array('id' => $key->userid))) { 3352 throw new \moodle_exception('invaliduserid'); 3353 } 3354 3355 core_user::require_active_user($user, true, true); 3356 3357 // Emulate normal session. 3358 enrol_check_plugins($user, false); 3359 \core\session\manager::set_user($user); 3360 3361 // Note we are not using normal login. 3362 if (!defined('USER_KEY_LOGIN')) { 3363 define('USER_KEY_LOGIN', true); 3364 } 3365 3366 // Return instance id - it might be empty. 3367 return $key->instance; 3368 } 3369 3370 /** 3371 * Creates a new private user access key. 3372 * 3373 * @param string $script unique target identifier 3374 * @param int $userid 3375 * @param int $instance optional instance id 3376 * @param string $iprestriction optional ip restricted access 3377 * @param int $validuntil key valid only until given data 3378 * @return string access key value 3379 */ 3380 function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) { 3381 global $DB; 3382 3383 $key = new stdClass(); 3384 $key->script = $script; 3385 $key->userid = $userid; 3386 $key->instance = $instance; 3387 $key->iprestriction = $iprestriction; 3388 $key->validuntil = $validuntil; 3389 $key->timecreated = time(); 3390 3391 // Something long and unique. 3392 $key->value = md5($userid.'_'.time().random_string(40)); 3393 while ($DB->record_exists('user_private_key', array('value' => $key->value))) { 3394 // Must be unique. 3395 $key->value = md5($userid.'_'.time().random_string(40)); 3396 } 3397 $DB->insert_record('user_private_key', $key); 3398 return $key->value; 3399 } 3400 3401 /** 3402 * Delete the user's new private user access keys for a particular script. 3403 * 3404 * @param string $script unique target identifier 3405 * @param int $userid 3406 * @return void 3407 */ 3408 function delete_user_key($script, $userid) { 3409 global $DB; 3410 $DB->delete_records('user_private_key', array('script' => $script, 'userid' => $userid)); 3411 } 3412 3413 /** 3414 * Gets a private user access key (and creates one if one doesn't exist). 3415 * 3416 * @param string $script unique target identifier 3417 * @param int $userid 3418 * @param int $instance optional instance id 3419 * @param string $iprestriction optional ip restricted access 3420 * @param int $validuntil key valid only until given date 3421 * @return string access key value 3422 */ 3423 function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) { 3424 global $DB; 3425 3426 if ($key = $DB->get_record('user_private_key', array('script' => $script, 'userid' => $userid, 3427 'instance' => $instance, 'iprestriction' => $iprestriction, 3428 'validuntil' => $validuntil))) { 3429 return $key->value; 3430 } else { 3431 return create_user_key($script, $userid, $instance, $iprestriction, $validuntil); 3432 } 3433 } 3434 3435 3436 /** 3437 * Modify the user table by setting the currently logged in user's last login to now. 3438 * 3439 * @return bool Always returns true 3440 */ 3441 function update_user_login_times() { 3442 global $USER, $DB, $SESSION; 3443 3444 if (isguestuser()) { 3445 // Do not update guest access times/ips for performance. 3446 return true; 3447 } 3448 3449 if (defined('USER_KEY_LOGIN') && USER_KEY_LOGIN === true) { 3450 // Do not update user login time when using user key login. 3451 return true; 3452 } 3453 3454 $now = time(); 3455 3456 $user = new stdClass(); 3457 $user->id = $USER->id; 3458 3459 // Make sure all users that logged in have some firstaccess. 3460 if ($USER->firstaccess == 0) { 3461 $USER->firstaccess = $user->firstaccess = $now; 3462 } 3463 3464 // Store the previous current as lastlogin. 3465 $USER->lastlogin = $user->lastlogin = $USER->currentlogin; 3466 3467 $USER->currentlogin = $user->currentlogin = $now; 3468 3469 // Function user_accesstime_log() may not update immediately, better do it here. 3470 $USER->lastaccess = $user->lastaccess = $now; 3471 $SESSION->userpreviousip = $USER->lastip; 3472 $USER->lastip = $user->lastip = getremoteaddr(); 3473 3474 // Note: do not call user_update_user() here because this is part of the login process, 3475 // the login event means that these fields were updated. 3476 $DB->update_record('user', $user); 3477 return true; 3478 } 3479 3480 /** 3481 * Determines if a user has completed setting up their account. 3482 * 3483 * The lax mode (with $strict = false) has been introduced for special cases 3484 * only where we want to skip certain checks intentionally. This is valid in 3485 * certain mnet or ajax scenarios when the user cannot / should not be 3486 * redirected to edit their profile. In most cases, you should perform the 3487 * strict check. 3488 * 3489 * @param stdClass $user A {@link $USER} object to test for the existence of a valid name and email 3490 * @param bool $strict Be more strict and assert id and custom profile fields set, too 3491 * @return bool 3492 */ 3493 function user_not_fully_set_up($user, $strict = true) { 3494 global $CFG, $SESSION, $USER; 3495 require_once($CFG->dirroot.'/user/profile/lib.php'); 3496 3497 // If the user is setup then store this in the session to avoid re-checking. 3498 // Some edge cases are when the users email starts to bounce or the 3499 // configuration for custom fields has changed while they are logged in so 3500 // we re-check this fully every hour for the rare cases it has changed. 3501 if (isset($USER->id) && isset($user->id) && $USER->id === $user->id && 3502 isset($SESSION->fullysetupstrict) && (time() - $SESSION->fullysetupstrict) < HOURSECS) { 3503 return false; 3504 } 3505 3506 if (isguestuser($user)) { 3507 return false; 3508 } 3509 3510 if (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user)) { 3511 return true; 3512 } 3513 3514 if ($strict) { 3515 if (empty($user->id)) { 3516 // Strict mode can be used with existing accounts only. 3517 return true; 3518 } 3519 if (!profile_has_required_custom_fields_set($user->id)) { 3520 return true; 3521 } 3522 if (isset($USER->id) && isset($user->id) && $USER->id === $user->id) { 3523 $SESSION->fullysetupstrict = time(); 3524 } 3525 } 3526 3527 return false; 3528 } 3529 3530 /** 3531 * Check whether the user has exceeded the bounce threshold 3532 * 3533 * @param stdClass $user A {@link $USER} object 3534 * @return bool true => User has exceeded bounce threshold 3535 */ 3536 function over_bounce_threshold($user) { 3537 global $CFG, $DB; 3538 3539 if (empty($CFG->handlebounces)) { 3540 return false; 3541 } 3542 3543 if (empty($user->id)) { 3544 // No real (DB) user, nothing to do here. 3545 return false; 3546 } 3547 3548 // Set sensible defaults. 3549 if (empty($CFG->minbounces)) { 3550 $CFG->minbounces = 10; 3551 } 3552 if (empty($CFG->bounceratio)) { 3553 $CFG->bounceratio = .20; 3554 } 3555 $bouncecount = 0; 3556 $sendcount = 0; 3557 if ($bounce = $DB->get_record('user_preferences', array ('userid' => $user->id, 'name' => 'email_bounce_count'))) { 3558 $bouncecount = $bounce->value; 3559 } 3560 if ($send = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) { 3561 $sendcount = $send->value; 3562 } 3563 return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio); 3564 } 3565 3566 /** 3567 * Used to increment or reset email sent count 3568 * 3569 * @param stdClass $user object containing an id 3570 * @param bool $reset will reset the count to 0 3571 * @return void 3572 */ 3573 function set_send_count($user, $reset=false) { 3574 global $DB; 3575 3576 if (empty($user->id)) { 3577 // No real (DB) user, nothing to do here. 3578 return; 3579 } 3580 3581 if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) { 3582 $pref->value = (!empty($reset)) ? 0 : $pref->value+1; 3583 $DB->update_record('user_preferences', $pref); 3584 } else if (!empty($reset)) { 3585 // If it's not there and we're resetting, don't bother. Make a new one. 3586 $pref = new stdClass(); 3587 $pref->name = 'email_send_count'; 3588 $pref->value = 1; 3589 $pref->userid = $user->id; 3590 $DB->insert_record('user_preferences', $pref, false); 3591 } 3592 } 3593 3594 /** 3595 * Increment or reset user's email bounce count 3596 * 3597 * @param stdClass $user object containing an id 3598 * @param bool $reset will reset the count to 0 3599 */ 3600 function set_bounce_count($user, $reset=false) { 3601 global $DB; 3602 3603 if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_bounce_count'))) { 3604 $pref->value = (!empty($reset)) ? 0 : $pref->value+1; 3605 $DB->update_record('user_preferences', $pref); 3606 } else if (!empty($reset)) { 3607 // If it's not there and we're resetting, don't bother. Make a new one. 3608 $pref = new stdClass(); 3609 $pref->name = 'email_bounce_count'; 3610 $pref->value = 1; 3611 $pref->userid = $user->id; 3612 $DB->insert_record('user_preferences', $pref, false); 3613 } 3614 } 3615 3616 /** 3617 * Determines if the logged in user is currently moving an activity 3618 * 3619 * @param int $courseid The id of the course being tested 3620 * @return bool 3621 */ 3622 function ismoving($courseid) { 3623 global $USER; 3624 3625 if (!empty($USER->activitycopy)) { 3626 return ($USER->activitycopycourse == $courseid); 3627 } 3628 return false; 3629 } 3630 3631 /** 3632 * Returns a persons full name 3633 * 3634 * Given an object containing all of the users name values, this function returns a string with the full name of the person. 3635 * The result may depend on system settings or language. 'override' will force the alternativefullnameformat to be used. In 3636 * English, fullname as well as alternativefullnameformat is set to 'firstname lastname' by default. But you could have 3637 * fullname set to 'firstname lastname' and alternativefullnameformat set to 'firstname middlename alternatename lastname'. 3638 * 3639 * @param stdClass $user A {@link $USER} object to get full name of. 3640 * @param bool $override If true then the alternativefullnameformat format rather than fullnamedisplay format will be used. 3641 * @return string 3642 */ 3643 function fullname($user, $override=false) { 3644 // Note: We do not intend to deprecate this function any time soon as it is too widely used at this time. 3645 // Uses of it should be updated to use the new API and pass updated arguments. 3646 3647 // Return an empty string if there is no user. 3648 if (empty($user)) { 3649 return ''; 3650 } 3651 3652 $options = ['override' => $override]; 3653 return core_user::get_fullname($user, null, $options); 3654 } 3655 3656 /** 3657 * Reduces lines of duplicated code for getting user name fields. 3658 * 3659 * See also {@link user_picture::unalias()} 3660 * 3661 * @param object $addtoobject Object to add user name fields to. 3662 * @param object $secondobject Object that contains user name field information. 3663 * @param string $prefix prefix to be added to all fields (including $additionalfields) e.g. authorfirstname. 3664 * @param array $additionalfields Additional fields to be matched with data in the second object. 3665 * The key can be set to the user table field name. 3666 * @return object User name fields. 3667 */ 3668 function username_load_fields_from_object($addtoobject, $secondobject, $prefix = null, $additionalfields = null) { 3669 $fields = []; 3670 foreach (\core_user\fields::get_name_fields() as $field) { 3671 $fields[$field] = $prefix . $field; 3672 } 3673 if ($additionalfields) { 3674 // Additional fields can specify their own 'alias' such as 'id' => 'userid'. This checks to see if 3675 // the key is a number and then sets the key to the array value. 3676 foreach ($additionalfields as $key => $value) { 3677 if (is_numeric($key)) { 3678 $additionalfields[$value] = $prefix . $value; 3679 unset($additionalfields[$key]); 3680 } else { 3681 $additionalfields[$key] = $prefix . $value; 3682 } 3683 } 3684 $fields = array_merge($fields, $additionalfields); 3685 } 3686 foreach ($fields as $key => $field) { 3687 // Important that we have all of the user name fields present in the object that we are sending back. 3688 $addtoobject->$key = ''; 3689 if (isset($secondobject->$field)) { 3690 $addtoobject->$key = $secondobject->$field; 3691 } 3692 } 3693 return $addtoobject; 3694 } 3695 3696 /** 3697 * Returns an array of values in order of occurance in a provided string. 3698 * The key in the result is the character postion in the string. 3699 * 3700 * @param array $values Values to be found in the string format 3701 * @param string $stringformat The string which may contain values being searched for. 3702 * @return array An array of values in order according to placement in the string format. 3703 */ 3704 function order_in_string($values, $stringformat) { 3705 $valuearray = array(); 3706 foreach ($values as $value) { 3707 $pattern = "/$value\b/"; 3708 // Using preg_match as strpos() may match values that are similar e.g. firstname and firstnamephonetic. 3709 if (preg_match($pattern, $stringformat)) { 3710 $replacement = "thing"; 3711 // Replace the value with something more unique to ensure we get the right position when using strpos(). 3712 $newformat = preg_replace($pattern, $replacement, $stringformat); 3713 $position = strpos($newformat, $replacement); 3714 $valuearray[$position] = $value; 3715 } 3716 } 3717 ksort($valuearray); 3718 return $valuearray; 3719 } 3720 3721 /** 3722 * Returns whether a given authentication plugin exists. 3723 * 3724 * @param string $auth Form of authentication to check for. Defaults to the global setting in {@link $CFG}. 3725 * @return boolean Whether the plugin is available. 3726 */ 3727 function exists_auth_plugin($auth) { 3728 global $CFG; 3729 3730 if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) { 3731 return is_readable("{$CFG->dirroot}/auth/$auth/auth.php"); 3732 } 3733 return false; 3734 } 3735 3736 /** 3737 * Checks if a given plugin is in the list of enabled authentication plugins. 3738 * 3739 * @param string $auth Authentication plugin. 3740 * @return boolean Whether the plugin is enabled. 3741 */ 3742 function is_enabled_auth($auth) { 3743 if (empty($auth)) { 3744 return false; 3745 } 3746 3747 $enabled = get_enabled_auth_plugins(); 3748 3749 return in_array($auth, $enabled); 3750 } 3751 3752 /** 3753 * Returns an authentication plugin instance. 3754 * 3755 * @param string $auth name of authentication plugin 3756 * @return auth_plugin_base An instance of the required authentication plugin. 3757 */ 3758 function get_auth_plugin($auth) { 3759 global $CFG; 3760 3761 // Check the plugin exists first. 3762 if (! exists_auth_plugin($auth)) { 3763 throw new \moodle_exception('authpluginnotfound', 'debug', '', $auth); 3764 } 3765 3766 // Return auth plugin instance. 3767 require_once("{$CFG->dirroot}/auth/$auth/auth.php"); 3768 $class = "auth_plugin_$auth"; 3769 return new $class; 3770 } 3771 3772 /** 3773 * Returns array of active auth plugins. 3774 * 3775 * @param bool $fix fix $CFG->auth if needed. Only set if logged in as admin. 3776 * @return array 3777 */ 3778 function get_enabled_auth_plugins($fix=false) { 3779 global $CFG; 3780 3781 $default = array('manual', 'nologin'); 3782 3783 if (empty($CFG->auth)) { 3784 $auths = array(); 3785 } else { 3786 $auths = explode(',', $CFG->auth); 3787 } 3788 3789 $auths = array_unique($auths); 3790 $oldauthconfig = implode(',', $auths); 3791 foreach ($auths as $k => $authname) { 3792 if (in_array($authname, $default)) { 3793 // The manual and nologin plugin never need to be stored. 3794 unset($auths[$k]); 3795 } else if (!exists_auth_plugin($authname)) { 3796 debugging(get_string('authpluginnotfound', 'debug', $authname)); 3797 unset($auths[$k]); 3798 } 3799 } 3800 3801 // Ideally only explicit interaction from a human admin should trigger a 3802 // change in auth config, see MDL-70424 for details. 3803 if ($fix) { 3804 $newconfig = implode(',', $auths); 3805 if (!isset($CFG->auth) or $newconfig != $CFG->auth) { 3806 add_to_config_log('auth', $oldauthconfig, $newconfig, 'core'); 3807 set_config('auth', $newconfig); 3808 } 3809 } 3810 3811 return (array_merge($default, $auths)); 3812 } 3813 3814 /** 3815 * Returns true if an internal authentication method is being used. 3816 * if method not specified then, global default is assumed 3817 * 3818 * @param string $auth Form of authentication required 3819 * @return bool 3820 */ 3821 function is_internal_auth($auth) { 3822 // Throws error if bad $auth. 3823 $authplugin = get_auth_plugin($auth); 3824 return $authplugin->is_internal(); 3825 } 3826 3827 /** 3828 * Returns true if the user is a 'restored' one. 3829 * 3830 * Used in the login process to inform the user and allow him/her to reset the password 3831 * 3832 * @param string $username username to be checked 3833 * @return bool 3834 */ 3835 function is_restored_user($username) { 3836 global $CFG, $DB; 3837 3838 return $DB->record_exists('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'password' => 'restored')); 3839 } 3840 3841 /** 3842 * Returns an array of user fields 3843 * 3844 * @return array User field/column names 3845 */ 3846 function get_user_fieldnames() { 3847 global $DB; 3848 3849 $fieldarray = $DB->get_columns('user'); 3850 unset($fieldarray['id']); 3851 $fieldarray = array_keys($fieldarray); 3852 3853 return $fieldarray; 3854 } 3855 3856 /** 3857 * Returns the string of the language for the new user. 3858 * 3859 * @return string language for the new user 3860 */ 3861 function get_newuser_language() { 3862 global $CFG, $SESSION; 3863 return (!empty($CFG->autolangusercreation) && !empty($SESSION->lang)) ? $SESSION->lang : $CFG->lang; 3864 } 3865 3866 /** 3867 * Creates a bare-bones user record 3868 * 3869 * @todo Outline auth types and provide code example 3870 * 3871 * @param string $username New user's username to add to record 3872 * @param string $password New user's password to add to record 3873 * @param string $auth Form of authentication required 3874 * @return stdClass A complete user object 3875 */ 3876 function create_user_record($username, $password, $auth = 'manual') { 3877 global $CFG, $DB, $SESSION; 3878 require_once($CFG->dirroot.'/user/profile/lib.php'); 3879 require_once($CFG->dirroot.'/user/lib.php'); 3880 3881 // Just in case check text case. 3882 $username = trim(core_text::strtolower($username)); 3883 3884 $authplugin = get_auth_plugin($auth); 3885 $customfields = $authplugin->get_custom_user_profile_fields(); 3886 $newuser = new stdClass(); 3887 if ($newinfo = $authplugin->get_userinfo($username)) { 3888 $newinfo = truncate_userinfo($newinfo); 3889 foreach ($newinfo as $key => $value) { 3890 if (in_array($key, $authplugin->userfields) || (in_array($key, $customfields))) { 3891 $newuser->$key = $value; 3892 } 3893 } 3894 } 3895 3896 if (!empty($newuser->email)) { 3897 if (email_is_not_allowed($newuser->email)) { 3898 unset($newuser->email); 3899 } 3900 } 3901 3902 $newuser->auth = $auth; 3903 $newuser->username = $username; 3904 3905 // Fix for MDL-8480 3906 // user CFG lang for user if $newuser->lang is empty 3907 // or $user->lang is not an installed language. 3908 if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) { 3909 $newuser->lang = get_newuser_language(); 3910 } 3911 $newuser->confirmed = 1; 3912 $newuser->lastip = getremoteaddr(); 3913 $newuser->timecreated = time(); 3914 $newuser->timemodified = $newuser->timecreated; 3915 $newuser->mnethostid = $CFG->mnet_localhost_id; 3916 3917 $newuser->id = user_create_user($newuser, false, false); 3918 3919 // Save user profile data. 3920 profile_save_data($newuser); 3921 3922 $user = get_complete_user_data('id', $newuser->id); 3923 if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})) { 3924 set_user_preference('auth_forcepasswordchange', 1, $user); 3925 } 3926 // Set the password. 3927 update_internal_user_password($user, $password); 3928 3929 // Trigger event. 3930 \core\event\user_created::create_from_userid($newuser->id)->trigger(); 3931 3932 return $user; 3933 } 3934 3935 /** 3936 * Will update a local user record from an external source (MNET users can not be updated using this method!). 3937 * 3938 * @param string $username user's username to update the record 3939 * @return stdClass A complete user object 3940 */ 3941 function update_user_record($username) { 3942 global $DB, $CFG; 3943 // Just in case check text case. 3944 $username = trim(core_text::strtolower($username)); 3945 3946 $oldinfo = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id), '*', MUST_EXIST); 3947 return update_user_record_by_id($oldinfo->id); 3948 } 3949 3950 /** 3951 * Will update a local user record from an external source (MNET users can not be updated using this method!). 3952 * 3953 * @param int $id user id 3954 * @return stdClass A complete user object 3955 */ 3956 function update_user_record_by_id($id) { 3957 global $DB, $CFG; 3958 require_once($CFG->dirroot."/user/profile/lib.php"); 3959 require_once($CFG->dirroot.'/user/lib.php'); 3960 3961 $params = array('mnethostid' => $CFG->mnet_localhost_id, 'id' => $id, 'deleted' => 0); 3962 $oldinfo = $DB->get_record('user', $params, '*', MUST_EXIST); 3963 3964 $newuser = array(); 3965 $userauth = get_auth_plugin($oldinfo->auth); 3966 3967 if ($newinfo = $userauth->get_userinfo($oldinfo->username)) { 3968 $newinfo = truncate_userinfo($newinfo); 3969 $customfields = $userauth->get_custom_user_profile_fields(); 3970 3971 foreach ($newinfo as $key => $value) { 3972 $iscustom = in_array($key, $customfields); 3973 if (!$iscustom) { 3974 $key = strtolower($key); 3975 } 3976 if ((!property_exists($oldinfo, $key) && !$iscustom) or $key === 'username' or $key === 'id' 3977 or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') { 3978 // Unknown or must not be changed. 3979 continue; 3980 } 3981 if (empty($userauth->config->{'field_updatelocal_' . $key}) || empty($userauth->config->{'field_lock_' . $key})) { 3982 continue; 3983 } 3984 $confval = $userauth->config->{'field_updatelocal_' . $key}; 3985 $lockval = $userauth->config->{'field_lock_' . $key}; 3986 if ($confval === 'onlogin') { 3987 // MDL-4207 Don't overwrite modified user profile values with 3988 // empty LDAP values when 'unlocked if empty' is set. The purpose 3989 // of the setting 'unlocked if empty' is to allow the user to fill 3990 // in a value for the selected field _if LDAP is giving 3991 // nothing_ for this field. Thus it makes sense to let this value 3992 // stand in until LDAP is giving a value for this field. 3993 if (!(empty($value) && $lockval === 'unlockedifempty')) { 3994 if ($iscustom || (in_array($key, $userauth->userfields) && 3995 ((string)$oldinfo->$key !== (string)$value))) { 3996 $newuser[$key] = (string)$value; 3997 } 3998 } 3999 } 4000 } 4001 if ($newuser) { 4002 $newuser['id'] = $oldinfo->id; 4003 $newuser['timemodified'] = time(); 4004 user_update_user((object) $newuser, false, false); 4005 4006 // Save user profile data. 4007 profile_save_data((object) $newuser); 4008 4009 // Trigger event. 4010 \core\event\user_updated::create_from_userid($newuser['id'])->trigger(); 4011 } 4012 } 4013 4014 return get_complete_user_data('id', $oldinfo->id); 4015 } 4016 4017 /** 4018 * Will truncate userinfo as it comes from auth_get_userinfo (from external auth) which may have large fields. 4019 * 4020 * @param array $info Array of user properties to truncate if needed 4021 * @return array The now truncated information that was passed in 4022 */ 4023 function truncate_userinfo(array $info) { 4024 // Define the limits. 4025 $limit = array( 4026 'username' => 100, 4027 'idnumber' => 255, 4028 'firstname' => 100, 4029 'lastname' => 100, 4030 'email' => 100, 4031 'phone1' => 20, 4032 'phone2' => 20, 4033 'institution' => 255, 4034 'department' => 255, 4035 'address' => 255, 4036 'city' => 120, 4037 'country' => 2, 4038 ); 4039 4040 // Apply where needed. 4041 foreach (array_keys($info) as $key) { 4042 if (!empty($limit[$key])) { 4043 $info[$key] = trim(core_text::substr($info[$key], 0, $limit[$key])); 4044 } 4045 } 4046 4047 return $info; 4048 } 4049 4050 /** 4051 * Marks user deleted in internal user database and notifies the auth plugin. 4052 * Also unenrols user from all roles and does other cleanup. 4053 * 4054 * Any plugin that needs to purge user data should register the 'user_deleted' event. 4055 * 4056 * @param stdClass $user full user object before delete 4057 * @return boolean success 4058 * @throws coding_exception if invalid $user parameter detected 4059 */ 4060 function delete_user(stdClass $user) { 4061 global $CFG, $DB, $SESSION; 4062 require_once($CFG->libdir.'/grouplib.php'); 4063 require_once($CFG->libdir.'/gradelib.php'); 4064 require_once($CFG->dirroot.'/message/lib.php'); 4065 require_once($CFG->dirroot.'/user/lib.php'); 4066 4067 // Make sure nobody sends bogus record type as parameter. 4068 if (!property_exists($user, 'id') or !property_exists($user, 'username')) { 4069 throw new coding_exception('Invalid $user parameter in delete_user() detected'); 4070 } 4071 4072 // Better not trust the parameter and fetch the latest info this will be very expensive anyway. 4073 if (!$user = $DB->get_record('user', array('id' => $user->id))) { 4074 debugging('Attempt to delete unknown user account.'); 4075 return false; 4076 } 4077 4078 // There must be always exactly one guest record, originally the guest account was identified by username only, 4079 // now we use $CFG->siteguest for performance reasons. 4080 if ($user->username === 'guest' or isguestuser($user)) { 4081 debugging('Guest user account can not be deleted.'); 4082 return false; 4083 } 4084 4085 // Admin can be theoretically from different auth plugin, but we want to prevent deletion of internal accoutns only, 4086 // if anything goes wrong ppl may force somebody to be admin via config.php setting $CFG->siteadmins. 4087 if ($user->auth === 'manual' and is_siteadmin($user)) { 4088 debugging('Local administrator accounts can not be deleted.'); 4089 return false; 4090 } 4091 4092 // Allow plugins to use this user object before we completely delete it. 4093 if ($pluginsfunction = get_plugins_with_function('pre_user_delete')) { 4094 foreach ($pluginsfunction as $plugintype => $plugins) { 4095 foreach ($plugins as $pluginfunction) { 4096 $pluginfunction($user); 4097 } 4098 } 4099 } 4100 4101 // Keep user record before updating it, as we have to pass this to user_deleted event. 4102 $olduser = clone $user; 4103 4104 // Keep a copy of user context, we need it for event. 4105 $usercontext = context_user::instance($user->id); 4106 4107 // Remove user from communication rooms immediately. 4108 if (core_communication\api::is_available()) { 4109 foreach (enrol_get_users_courses($user->id) as $course) { 4110 $communication = \core_communication\processor::load_by_instance( 4111 context: \core\context\course::instance($course->id), 4112 component: 'core_course', 4113 instancetype: 'coursecommunication', 4114 instanceid: $course->id, 4115 ); 4116 if ($communication !== null) { 4117 $communication->get_room_user_provider()->remove_members_from_room([$user->id]); 4118 $communication->delete_instance_user_mapping([$user->id]); 4119 } 4120 } 4121 } 4122 4123 // Delete all grades - backup is kept in grade_grades_history table. 4124 grade_user_delete($user->id); 4125 4126 // TODO: remove from cohorts using standard API here. 4127 4128 // Remove user tags. 4129 core_tag_tag::remove_all_item_tags('core', 'user', $user->id); 4130 4131 // Unconditionally unenrol from all courses. 4132 enrol_user_delete($user); 4133 4134 // Unenrol from all roles in all contexts. 4135 // This might be slow but it is really needed - modules might do some extra cleanup! 4136 role_unassign_all(array('userid' => $user->id)); 4137 4138 // Notify the competency subsystem. 4139 \core_competency\api::hook_user_deleted($user->id); 4140 4141 // Now do a brute force cleanup. 4142 4143 // Delete all user events and subscription events. 4144 $DB->delete_records_select('event', 'userid = :userid AND subscriptionid IS NOT NULL', ['userid' => $user->id]); 4145 4146 // Now, delete all calendar subscription from the user. 4147 $DB->delete_records('event_subscriptions', ['userid' => $user->id]); 4148 4149 // Remove from all cohorts. 4150 $DB->delete_records('cohort_members', array('userid' => $user->id)); 4151 4152 // Remove from all groups. 4153 $DB->delete_records('groups_members', array('userid' => $user->id)); 4154 4155 // Brute force unenrol from all courses. 4156 $DB->delete_records('user_enrolments', array('userid' => $user->id)); 4157 4158 // Purge user preferences. 4159 $DB->delete_records('user_preferences', array('userid' => $user->id)); 4160 4161 // Purge user extra profile info. 4162 $DB->delete_records('user_info_data', array('userid' => $user->id)); 4163 4164 // Purge log of previous password hashes. 4165 $DB->delete_records('user_password_history', array('userid' => $user->id)); 4166 4167 // Last course access not necessary either. 4168 $DB->delete_records('user_lastaccess', array('userid' => $user->id)); 4169 // Remove all user tokens. 4170 $DB->delete_records('external_tokens', array('userid' => $user->id)); 4171 4172 // Unauthorise the user for all services. 4173 $DB->delete_records('external_services_users', array('userid' => $user->id)); 4174 4175 // Remove users private keys. 4176 $DB->delete_records('user_private_key', array('userid' => $user->id)); 4177 4178 // Remove users customised pages. 4179 $DB->delete_records('my_pages', array('userid' => $user->id, 'private' => 1)); 4180 4181 // Remove user's oauth2 refresh tokens, if present. 4182 $DB->delete_records('oauth2_refresh_token', array('userid' => $user->id)); 4183 4184 // Delete user from $SESSION->bulk_users. 4185 if (isset($SESSION->bulk_users[$user->id])) { 4186 unset($SESSION->bulk_users[$user->id]); 4187 } 4188 4189 // Force logout - may fail if file based sessions used, sorry. 4190 \core\session\manager::kill_user_sessions($user->id); 4191 4192 // Generate username from email address, or a fake email. 4193 $delemail = !empty($user->email) ? $user->email : $user->username . '.' . $user->id . '@unknownemail.invalid'; 4194 4195 $deltime = time(); 4196 $deltimelength = core_text::strlen((string) $deltime); 4197 4198 // Max username length is 100 chars. Select up to limit - (length of current time + 1 [period character]) from users email. 4199 $delname = clean_param($delemail, PARAM_USERNAME); 4200 $delname = core_text::substr($delname, 0, 100 - ($deltimelength + 1)) . ".{$deltime}"; 4201 4202 // Workaround for bulk deletes of users with the same email address. 4203 while ($DB->record_exists('user', array('username' => $delname))) { // No need to use mnethostid here. 4204 $delname++; 4205 } 4206 4207 // Mark internal user record as "deleted". 4208 $updateuser = new stdClass(); 4209 $updateuser->id = $user->id; 4210 $updateuser->deleted = 1; 4211 $updateuser->username = $delname; // Remember it just in case. 4212 $updateuser->email = md5($user->username);// Store hash of username, useful importing/restoring users. 4213 $updateuser->idnumber = ''; // Clear this field to free it up. 4214 $updateuser->picture = 0; 4215 $updateuser->timemodified = $deltime; 4216 4217 // Don't trigger update event, as user is being deleted. 4218 user_update_user($updateuser, false, false); 4219 4220 // Delete all content associated with the user context, but not the context itself. 4221 $usercontext->delete_content(); 4222 4223 // Delete any search data. 4224 \core_search\manager::context_deleted($usercontext); 4225 4226 // Any plugin that needs to cleanup should register this event. 4227 // Trigger event. 4228 $event = \core\event\user_deleted::create( 4229 array( 4230 'objectid' => $user->id, 4231 'relateduserid' => $user->id, 4232 'context' => $usercontext, 4233 'other' => array( 4234 'username' => $user->username, 4235 'email' => $user->email, 4236 'idnumber' => $user->idnumber, 4237 'picture' => $user->picture, 4238 'mnethostid' => $user->mnethostid 4239 ) 4240 ) 4241 ); 4242 $event->add_record_snapshot('user', $olduser); 4243 $event->trigger(); 4244 4245 // We will update the user's timemodified, as it will be passed to the user_deleted event, which 4246 // should know about this updated property persisted to the user's table. 4247 $user->timemodified = $updateuser->timemodified; 4248 4249 // Notify auth plugin - do not block the delete even when plugin fails. 4250 $authplugin = get_auth_plugin($user->auth); 4251 $authplugin->user_delete($user); 4252 4253 return true; 4254 } 4255 4256 /** 4257 * Retrieve the guest user object. 4258 * 4259 * @return stdClass A {@link $USER} object 4260 */ 4261 function guest_user() { 4262 global $CFG, $DB; 4263 4264 if ($newuser = $DB->get_record('user', array('id' => $CFG->siteguest))) { 4265 $newuser->confirmed = 1; 4266 $newuser->lang = get_newuser_language(); 4267 $newuser->lastip = getremoteaddr(); 4268 } 4269 4270 return $newuser; 4271 } 4272 4273 /** 4274 * Authenticates a user against the chosen authentication mechanism 4275 * 4276 * Given a username and password, this function looks them 4277 * up using the currently selected authentication mechanism, 4278 * and if the authentication is successful, it returns a 4279 * valid $user object from the 'user' table. 4280 * 4281 * Uses auth_ functions from the currently active auth module 4282 * 4283 * After authenticate_user_login() returns success, you will need to 4284 * log that the user has logged in, and call complete_user_login() to set 4285 * the session up. 4286 * 4287 * Note: this function works only with non-mnet accounts! 4288 * 4289 * @param string $username User's username (or also email if $CFG->authloginviaemail enabled) 4290 * @param string $password User's password 4291 * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO 4292 * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists) 4293 * @param string|bool $logintoken If this is set to a string it is validated against the login token for the session. 4294 * @param string|bool $loginrecaptcha If this is set to a string it is validated against Google reCaptcha. 4295 * @return stdClass|false A {@link $USER} object or false if error 4296 */ 4297 function authenticate_user_login( 4298 $username, 4299 $password, 4300 $ignorelockout = false, 4301 &$failurereason = null, 4302 $logintoken = false, 4303 string|bool $loginrecaptcha = false, 4304 ) { 4305 global $CFG, $DB, $PAGE, $SESSION; 4306 require_once("$CFG->libdir/authlib.php"); 4307 4308 if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) { 4309 // we have found the user 4310 4311 } else if (!empty($CFG->authloginviaemail)) { 4312 if ($email = clean_param($username, PARAM_EMAIL)) { 4313 $select = "mnethostid = :mnethostid AND LOWER(email) = LOWER(:email) AND deleted = 0"; 4314 $params = array('mnethostid' => $CFG->mnet_localhost_id, 'email' => $email); 4315 $users = $DB->get_records_select('user', $select, $params, 'id', 'id', 0, 2); 4316 if (count($users) === 1) { 4317 // Use email for login only if unique. 4318 $user = reset($users); 4319 $user = get_complete_user_data('id', $user->id); 4320 $username = $user->username; 4321 } 4322 unset($users); 4323 } 4324 } 4325 4326 // Make sure this request came from the login form. 4327 if (!\core\session\manager::validate_login_token($logintoken)) { 4328 $failurereason = AUTH_LOGIN_FAILED; 4329 4330 // Trigger login failed event (specifying the ID of the found user, if available). 4331 \core\event\user_login_failed::create([ 4332 'userid' => ($user->id ?? 0), 4333 'other' => [ 4334 'username' => $username, 4335 'reason' => $failurereason, 4336 ], 4337 ])->trigger(); 4338 4339 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Invalid Login Token: $username ".$_SERVER['HTTP_USER_AGENT']); 4340 return false; 4341 } 4342 4343 // Login reCaptcha. 4344 if (login_captcha_enabled() && !validate_login_captcha($loginrecaptcha)) { 4345 $failurereason = AUTH_LOGIN_FAILED_RECAPTCHA; 4346 // Trigger login failed event (specifying the ID of the found user, if available). 4347 \core\event\user_login_failed::create([ 4348 'userid' => ($user->id ?? 0), 4349 'other' => [ 4350 'username' => $username, 4351 'reason' => $failurereason, 4352 ], 4353 ])->trigger(); 4354 return false; 4355 } 4356 4357 $authsenabled = get_enabled_auth_plugins(); 4358 4359 if ($user) { 4360 // Use manual if auth not set. 4361 $auth = empty($user->auth) ? 'manual' : $user->auth; 4362 4363 if (in_array($user->auth, $authsenabled)) { 4364 $authplugin = get_auth_plugin($user->auth); 4365 $authplugin->pre_user_login_hook($user); 4366 } 4367 4368 if (!empty($user->suspended)) { 4369 $failurereason = AUTH_LOGIN_SUSPENDED; 4370 4371 // Trigger login failed event. 4372 $event = \core\event\user_login_failed::create(array('userid' => $user->id, 4373 'other' => array('username' => $username, 'reason' => $failurereason))); 4374 $event->trigger(); 4375 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']); 4376 return false; 4377 } 4378 if ($auth=='nologin' or !is_enabled_auth($auth)) { 4379 // Legacy way to suspend user. 4380 $failurereason = AUTH_LOGIN_SUSPENDED; 4381 4382 // Trigger login failed event. 4383 $event = \core\event\user_login_failed::create(array('userid' => $user->id, 4384 'other' => array('username' => $username, 'reason' => $failurereason))); 4385 $event->trigger(); 4386 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Disabled Login: $username ".$_SERVER['HTTP_USER_AGENT']); 4387 return false; 4388 } 4389 $auths = array($auth); 4390 4391 } else { 4392 // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user(). 4393 if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'deleted' => 1))) { 4394 $failurereason = AUTH_LOGIN_NOUSER; 4395 4396 // Trigger login failed event. 4397 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, 4398 'reason' => $failurereason))); 4399 $event->trigger(); 4400 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Deleted Login: $username ".$_SERVER['HTTP_USER_AGENT']); 4401 return false; 4402 } 4403 4404 // User does not exist. 4405 $auths = $authsenabled; 4406 $user = new stdClass(); 4407 $user->id = 0; 4408 } 4409 4410 if ($ignorelockout) { 4411 // Some other mechanism protects against brute force password guessing, for example login form might include reCAPTCHA 4412 // or this function is called from a SSO script. 4413 } else if ($user->id) { 4414 // Verify login lockout after other ways that may prevent user login. 4415 if (login_is_lockedout($user)) { 4416 $failurereason = AUTH_LOGIN_LOCKOUT; 4417 4418 // Trigger login failed event. 4419 $event = \core\event\user_login_failed::create(array('userid' => $user->id, 4420 'other' => array('username' => $username, 'reason' => $failurereason))); 4421 $event->trigger(); 4422 4423 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Login lockout: $username ".$_SERVER['HTTP_USER_AGENT']); 4424 $SESSION->loginerrormsg = get_string('accountlocked', 'admin'); 4425 4426 return false; 4427 } 4428 } else { 4429 // We can not lockout non-existing accounts. 4430 } 4431 4432 foreach ($auths as $auth) { 4433 $authplugin = get_auth_plugin($auth); 4434 4435 // On auth fail fall through to the next plugin. 4436 if (!$authplugin->user_login($username, $password)) { 4437 continue; 4438 } 4439 4440 // Before performing login actions, check if user still passes password policy, if admin setting is enabled. 4441 if (!empty($CFG->passwordpolicycheckonlogin)) { 4442 $errmsg = ''; 4443 $passed = check_password_policy($password, $errmsg, $user); 4444 if (!$passed) { 4445 // First trigger event for failure. 4446 $failedevent = \core\event\user_password_policy_failed::create_from_user($user); 4447 $failedevent->trigger(); 4448 4449 // If able to change password, set flag and move on. 4450 if ($authplugin->can_change_password()) { 4451 // Check if we are on internal change password page, or service is external, don't show notification. 4452 $internalchangeurl = new moodle_url('/login/change_password.php'); 4453 if (!($PAGE->has_set_url() && $internalchangeurl->compare($PAGE->url)) && $authplugin->is_internal()) { 4454 \core\notification::error(get_string('passwordpolicynomatch', '', $errmsg)); 4455 } 4456 set_user_preference('auth_forcepasswordchange', 1, $user); 4457 } else if ($authplugin->can_reset_password()) { 4458 // Else force a reset if possible. 4459 \core\notification::error(get_string('forcepasswordresetnotice', '', $errmsg)); 4460 redirect(new moodle_url('/login/forgot_password.php')); 4461 } else { 4462 $notifymsg = get_string('forcepasswordresetfailurenotice', '', $errmsg); 4463 // If support page is set, add link for help. 4464 if (!empty($CFG->supportpage)) { 4465 $link = \html_writer::link($CFG->supportpage, $CFG->supportpage); 4466 $link = \html_writer::tag('p', $link); 4467 $notifymsg .= $link; 4468 } 4469 4470 // If no change or reset is possible, add a notification for user. 4471 \core\notification::error($notifymsg); 4472 } 4473 } 4474 } 4475 4476 // Successful authentication. 4477 if ($user->id) { 4478 // User already exists in database. 4479 if (empty($user->auth)) { 4480 // For some reason auth isn't set yet. 4481 $DB->set_field('user', 'auth', $auth, array('id' => $user->id)); 4482 $user->auth = $auth; 4483 } 4484 4485 // If the existing hash is using an out-of-date algorithm (or the legacy md5 algorithm), then we should update to 4486 // the current hash algorithm while we have access to the user's password. 4487 update_internal_user_password($user, $password); 4488 4489 if ($authplugin->is_synchronised_with_external()) { 4490 // Update user record from external DB. 4491 $user = update_user_record_by_id($user->id); 4492 } 4493 } else { 4494 // The user is authenticated but user creation may be disabled. 4495 if (!empty($CFG->authpreventaccountcreation)) { 4496 $failurereason = AUTH_LOGIN_UNAUTHORISED; 4497 4498 // Trigger login failed event. 4499 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, 4500 'reason' => $failurereason))); 4501 $event->trigger(); 4502 4503 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Unknown user, can not create new accounts: $username ". 4504 $_SERVER['HTTP_USER_AGENT']); 4505 return false; 4506 } else { 4507 $user = create_user_record($username, $password, $auth); 4508 } 4509 } 4510 4511 $authplugin->sync_roles($user); 4512 4513 foreach ($authsenabled as $hau) { 4514 $hauth = get_auth_plugin($hau); 4515 $hauth->user_authenticated_hook($user, $username, $password); 4516 } 4517 4518 if (empty($user->id)) { 4519 $failurereason = AUTH_LOGIN_NOUSER; 4520 // Trigger login failed event. 4521 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, 4522 'reason' => $failurereason))); 4523 $event->trigger(); 4524 return false; 4525 } 4526 4527 if (!empty($user->suspended)) { 4528 // Just in case some auth plugin suspended account. 4529 $failurereason = AUTH_LOGIN_SUSPENDED; 4530 // Trigger login failed event. 4531 $event = \core\event\user_login_failed::create(array('userid' => $user->id, 4532 'other' => array('username' => $username, 'reason' => $failurereason))); 4533 $event->trigger(); 4534 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']); 4535 return false; 4536 } 4537 4538 login_attempt_valid($user); 4539 $failurereason = AUTH_LOGIN_OK; 4540 return $user; 4541 } 4542 4543 // Failed if all the plugins have failed. 4544 if (debugging('', DEBUG_ALL)) { 4545 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Failed Login: $username ".$_SERVER['HTTP_USER_AGENT']); 4546 } 4547 4548 if ($user->id) { 4549 login_attempt_failed($user); 4550 $failurereason = AUTH_LOGIN_FAILED; 4551 // Trigger login failed event. 4552 $event = \core\event\user_login_failed::create(array('userid' => $user->id, 4553 'other' => array('username' => $username, 'reason' => $failurereason))); 4554 $event->trigger(); 4555 } else { 4556 $failurereason = AUTH_LOGIN_NOUSER; 4557 // Trigger login failed event. 4558 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, 4559 'reason' => $failurereason))); 4560 $event->trigger(); 4561 } 4562 4563 return false; 4564 } 4565 4566 /** 4567 * Call to complete the user login process after authenticate_user_login() 4568 * has succeeded. It will setup the $USER variable and other required bits 4569 * and pieces. 4570 * 4571 * NOTE: 4572 * - It will NOT log anything -- up to the caller to decide what to log. 4573 * - this function does not set any cookies any more! 4574 * 4575 * @param stdClass $user 4576 * @param array $extrauserinfo 4577 * @return stdClass A {@link $USER} object - BC only, do not use 4578 */ 4579 function complete_user_login($user, array $extrauserinfo = []) { 4580 global $CFG, $DB, $USER, $SESSION; 4581 4582 \core\session\manager::login_user($user); 4583 4584 // Reload preferences from DB. 4585 unset($USER->preference); 4586 check_user_preferences_loaded($USER); 4587 4588 // Update login times. 4589 update_user_login_times(); 4590 4591 // Extra session prefs init. 4592 set_login_session_preferences(); 4593 4594 // Trigger login event. 4595 $event = \core\event\user_loggedin::create( 4596 array( 4597 'userid' => $USER->id, 4598 'objectid' => $USER->id, 4599 'other' => [ 4600 'username' => $USER->username, 4601 'extrauserinfo' => $extrauserinfo 4602 ] 4603 ) 4604 ); 4605 $event->trigger(); 4606 4607 // Check if the user is using a new browser or session (a new MoodleSession cookie is set in that case). 4608 // If the user is accessing from the same IP, ignore everything (most of the time will be a new session in the same browser). 4609 // Skip Web Service requests, CLI scripts, AJAX scripts, and request from the mobile app itself. 4610 $loginip = getremoteaddr(); 4611 $isnewip = isset($SESSION->userpreviousip) && $SESSION->userpreviousip != $loginip; 4612 $isvalidenv = (!WS_SERVER && !CLI_SCRIPT && !NO_MOODLE_COOKIES) || PHPUNIT_TEST; 4613 4614 if (!empty($SESSION->isnewsessioncookie) && $isnewip && $isvalidenv && !\core_useragent::is_moodle_app()) { 4615 4616 $logintime = time(); 4617 $ismoodleapp = false; 4618 $useragent = \core_useragent::get_user_agent_string(); 4619 4620 // Schedule adhoc task to sent a login notification to the user. 4621 $task = new \core\task\send_login_notifications(); 4622 $task->set_userid($USER->id); 4623 $task->set_custom_data(compact('ismoodleapp', 'useragent', 'loginip', 'logintime')); 4624 $task->set_component('core'); 4625 \core\task\manager::queue_adhoc_task($task); 4626 } 4627 4628 // Queue migrating the messaging data, if we need to. 4629 if (!get_user_preferences('core_message_migrate_data', false, $USER->id)) { 4630 // Check if there are any legacy messages to migrate. 4631 if (\core_message\helper::legacy_messages_exist($USER->id)) { 4632 \core_message\task\migrate_message_data::queue_task($USER->id); 4633 } else { 4634 set_user_preference('core_message_migrate_data', true, $USER->id); 4635 } 4636 } 4637 4638 if (isguestuser()) { 4639 // No need to continue when user is THE guest. 4640 return $USER; 4641 } 4642 4643 if (CLI_SCRIPT) { 4644 // We can redirect to password change URL only in browser. 4645 return $USER; 4646 } 4647 4648 // Select password change url. 4649 $userauth = get_auth_plugin($USER->auth); 4650 4651 // Check whether the user should be changing password. 4652 if (get_user_preferences('auth_forcepasswordchange', false)) { 4653 if ($userauth->can_change_password()) { 4654 if ($changeurl = $userauth->change_password_url()) { 4655 redirect($changeurl); 4656 } else { 4657 require_once($CFG->dirroot . '/login/lib.php'); 4658 $SESSION->wantsurl = core_login_get_return_url(); 4659 redirect($CFG->wwwroot.'/login/change_password.php'); 4660 } 4661 } else { 4662 throw new \moodle_exception('nopasswordchangeforced', 'auth'); 4663 } 4664 } 4665 return $USER; 4666 } 4667 4668 /** 4669 * Check a password hash to see if it was hashed using the legacy hash algorithm (bcrypt). 4670 * 4671 * @param string $password String to check. 4672 * @return bool True if the $password matches the format of a bcrypt hash. 4673 */ 4674 function password_is_legacy_hash(#[\SensitiveParameter] string $password): bool { 4675 return (bool) preg_match('/^\$2y\$[\d]{2}\$[A-Za-z0-9\.\/]{53}$/', $password); 4676 } 4677 4678 /** 4679 * Calculate the Shannon entropy of a string. 4680 * 4681 * @param string $pepper The pepper to calculate the entropy of. 4682 * @return float The Shannon entropy of the string. 4683 */ 4684 function calculate_entropy(#[\SensitiveParameter] string $pepper): float { 4685 // Initialize entropy. 4686 $h = 0; 4687 4688 // Calculate the length of the string. 4689 $size = strlen($pepper); 4690 4691 // For each unique character in the string. 4692 foreach (count_chars($pepper, 1) as $v) { 4693 // Calculate the probability of the character. 4694 $p = $v / $size; 4695 4696 // Add the character's contribution to the total entropy. 4697 // This uses the formula for the entropy of a discrete random variable. 4698 $h -= $p * log($p) / log(2); 4699 } 4700 4701 // Instead of returning the average entropy per symbol (Shannon entropy), 4702 // we multiply by the length of the string to get total entropy. 4703 return $h * $size; 4704 } 4705 4706 /** 4707 * Get the available password peppers. 4708 * The latest pepper is checked for minimum entropy as part of this function. 4709 * We only calculate the entropy of the most recent pepper, 4710 * because passwords are always updated to the latest pepper, 4711 * and in the past we may have enforced a lower minimum entropy. 4712 * Also, we allow the latest pepper to be empty, to allow admins to migrate off peppers. 4713 * 4714 * @return array The password peppers. 4715 * @throws coding_exception If the entropy of the password pepper is less than the recommended minimum. 4716 */ 4717 function get_password_peppers(): array { 4718 global $CFG; 4719 4720 // Get all available peppers. 4721 if (isset($CFG->passwordpeppers) && is_array($CFG->passwordpeppers)) { 4722 // Sort the array in descending order of keys (numerical). 4723 $peppers = $CFG->passwordpeppers; 4724 krsort($peppers, SORT_NUMERIC); 4725 } else { 4726 $peppers = []; // Set an empty array if no peppers are found. 4727 } 4728 4729 // Check if the entropy of the most recent pepper is less than the minimum. 4730 // Also, we allow the most recent pepper to be empty, to allow admins to migrate off peppers. 4731 $lastpepper = reset($peppers); 4732 if (!empty($peppers) && $lastpepper !== '' && calculate_entropy($lastpepper) < PEPPER_ENTROPY) { 4733 throw new coding_exception( 4734 'password pepper below minimum', 4735 'The entropy of the password pepper is less than the recommended minimum.'); 4736 } 4737 return $peppers; 4738 } 4739 4740 /** 4741 * Compare password against hash stored in user object to determine if it is valid. 4742 * 4743 * If necessary it also updates the stored hash to the current format. 4744 * 4745 * @param stdClass $user (Password property may be updated). 4746 * @param string $password Plain text password. 4747 * @return bool True if password is valid. 4748 */ 4749 function validate_internal_user_password(stdClass $user, #[\SensitiveParameter] string $password): bool { 4750 4751 if (exceeds_password_length($password)) { 4752 // Password cannot be more than MAX_PASSWORD_CHARACTERS characters. 4753 return false; 4754 } 4755 4756 if ($user->password === AUTH_PASSWORD_NOT_CACHED) { 4757 // Internal password is not used at all, it can not validate. 4758 return false; 4759 } 4760 4761 $peppers = get_password_peppers(); // Get the array of available peppers. 4762 $islegacy = password_is_legacy_hash($user->password); // Check if the password is a legacy bcrypt hash. 4763 4764 // If the password is a legacy hash, no peppers were used, so verify and update directly. 4765 if ($islegacy && password_verify($password, $user->password)) { 4766 update_internal_user_password($user, $password); 4767 return true; 4768 } 4769 4770 // If the password is not a legacy hash, iterate through the peppers. 4771 $latestpepper = reset($peppers); 4772 // Add an empty pepper to the beginning of the array. To make it easier to check if the password matches without any pepper. 4773 $peppers = [-1 => ''] + $peppers; 4774 foreach ($peppers as $pepper) { 4775 $pepperedpassword = $password . $pepper; 4776 4777 // If the peppered password is correct, update (if necessary) and return true. 4778 if (password_verify($pepperedpassword, $user->password)) { 4779 // If the pepper used is not the latest one, update the password. 4780 if ($pepper !== $latestpepper) { 4781 update_internal_user_password($user, $password); 4782 } 4783 return true; 4784 } 4785 } 4786 4787 // If no peppered password was correct, the password is wrong. 4788 return false; 4789 } 4790 4791 /** 4792 * Calculate hash for a plain text password. 4793 * 4794 * @param string $password Plain text password to be hashed. 4795 * @param bool $fasthash If true, use a low number of rounds when generating the hash 4796 * This is faster to generate but makes the hash less secure. 4797 * It is used when lots of hashes need to be generated quickly. 4798 * @param int $pepperlength Lenght of the peppers 4799 * @return string The hashed password. 4800 * 4801 * @throws moodle_exception If a problem occurs while generating the hash. 4802 */ 4803 function hash_internal_user_password(#[\SensitiveParameter] string $password, $fasthash = false, $pepperlength = 0): string { 4804 if (exceeds_password_length($password, $pepperlength)) { 4805 // Password cannot be more than MAX_PASSWORD_CHARACTERS. 4806 throw new \moodle_exception(get_string("passwordexceeded", 'error', MAX_PASSWORD_CHARACTERS)); 4807 } 4808 4809 // Set the cost factor to 5000 for fast hashing, otherwise use default cost. 4810 $rounds = $fasthash ? 5000 : 10000; 4811 4812 // First generate a cryptographically suitable salt. 4813 $randombytes = random_bytes(16); 4814 $salt = substr(strtr(base64_encode($randombytes), '+', '.'), 0, 16); 4815 4816 // Now construct the password string with the salt and number of rounds. 4817 // The password string is in the format $algorithm$rounds$salt$hash. ($6 is the SHA512 algorithm). 4818 $generatedhash = crypt($password, implode('$', [ 4819 '', 4820 // The SHA512 Algorithm 4821 '6', 4822 "rounds={$rounds}", 4823 $salt, 4824 '', 4825 ])); 4826 4827 if ($generatedhash === false || $generatedhash === null) { 4828 throw new moodle_exception('Failed to generate password hash.'); 4829 } 4830 4831 return $generatedhash; 4832 } 4833 4834 /** 4835 * Update password hash in user object (if necessary). 4836 * 4837 * The password is updated if: 4838 * 1. The password has changed (the hash of $user->password is different 4839 * to the hash of $password). 4840 * 2. The existing hash is using an out-of-date algorithm (or the legacy 4841 * md5 algorithm). 4842 * 4843 * The password is peppered with the latest pepper before hashing, 4844 * if peppers are available. 4845 * Updating the password will modify the $user object and the database 4846 * record to use the current hashing algorithm. 4847 * It will remove Web Services user tokens too. 4848 * 4849 * @param stdClass $user User object (password property may be updated). 4850 * @param string $password Plain text password. 4851 * @param bool $fasthash If true, use a low cost factor when generating the hash 4852 * This is much faster to generate but makes the hash 4853 * less secure. It is used when lots of hashes need to 4854 * be generated quickly. 4855 * @return bool Always returns true. 4856 */ 4857 function update_internal_user_password( 4858 stdClass $user, 4859 #[\SensitiveParameter] string $password, 4860 bool $fasthash = false 4861 ): bool { 4862 global $CFG, $DB; 4863 4864 // Add the latest password pepper to the password before further processing. 4865 $peppers = get_password_peppers(); 4866 if (!empty($peppers)) { 4867 $password = $password . reset($peppers); 4868 } 4869 4870 // Figure out what the hashed password should be. 4871 if (!isset($user->auth)) { 4872 debugging('User record in update_internal_user_password() must include field auth', 4873 DEBUG_DEVELOPER); 4874 $user->auth = $DB->get_field('user', 'auth', array('id' => $user->id)); 4875 } 4876 $authplugin = get_auth_plugin($user->auth); 4877 if ($authplugin->prevent_local_passwords()) { 4878 $hashedpassword = AUTH_PASSWORD_NOT_CACHED; 4879 } else { 4880 $hashedpassword = hash_internal_user_password($password, $fasthash); 4881 } 4882 4883 $algorithmchanged = false; 4884 4885 if ($hashedpassword === AUTH_PASSWORD_NOT_CACHED) { 4886 // Password is not cached, update it if not set to AUTH_PASSWORD_NOT_CACHED. 4887 $passwordchanged = ($user->password !== $hashedpassword); 4888 4889 } else if (isset($user->password)) { 4890 // If verification fails then it means the password has changed. 4891 $passwordchanged = !password_verify($password, $user->password); 4892 $algorithmchanged = password_is_legacy_hash($user->password); 4893 } else { 4894 // While creating new user, password in unset in $user object, to avoid 4895 // saving it with user_create() 4896 $passwordchanged = true; 4897 } 4898 4899 if ($passwordchanged || $algorithmchanged) { 4900 $DB->set_field('user', 'password', $hashedpassword, array('id' => $user->id)); 4901 $user->password = $hashedpassword; 4902 4903 // Trigger event. 4904 $user = $DB->get_record('user', array('id' => $user->id)); 4905 \core\event\user_password_updated::create_from_user($user)->trigger(); 4906 4907 // Remove WS user tokens. 4908 if (!empty($CFG->passwordchangetokendeletion)) { 4909 require_once($CFG->dirroot.'/webservice/lib.php'); 4910 webservice::delete_user_ws_tokens($user->id); 4911 } 4912 } 4913 4914 return true; 4915 } 4916 4917 /** 4918 * Get a complete user record, which includes all the info in the user record. 4919 * 4920 * Intended for setting as $USER session variable 4921 * 4922 * @param string $field The user field to be checked for a given value. 4923 * @param string $value The value to match for $field. 4924 * @param int $mnethostid 4925 * @param bool $throwexception If true, it will throw an exception when there's no record found or when there are multiple records 4926 * found. Otherwise, it will just return false. 4927 * @return mixed False, or A {@link $USER} object. 4928 */ 4929 function get_complete_user_data($field, $value, $mnethostid = null, $throwexception = false) { 4930 global $CFG, $DB; 4931 4932 if (!$field || !$value) { 4933 return false; 4934 } 4935 4936 // Change the field to lowercase. 4937 $field = core_text::strtolower($field); 4938 4939 // List of case insensitive fields. 4940 $caseinsensitivefields = ['email']; 4941 4942 // Username input is forced to lowercase and should be case sensitive. 4943 if ($field == 'username') { 4944 $value = core_text::strtolower($value); 4945 } 4946 4947 // Build the WHERE clause for an SQL query. 4948 $params = array('fieldval' => $value); 4949 4950 // Do a case-insensitive query, if necessary. These are generally very expensive. The performance can be improved on some DBs 4951 // such as MySQL by pre-filtering users with accent-insensitive subselect. 4952 if (in_array($field, $caseinsensitivefields)) { 4953 $fieldselect = $DB->sql_equal($field, ':fieldval', false); 4954 $idsubselect = $DB->sql_equal($field, ':fieldval2', false, false); 4955 $params['fieldval2'] = $value; 4956 } else { 4957 $fieldselect = "$field = :fieldval"; 4958 $idsubselect = ''; 4959 } 4960 $constraints = "$fieldselect AND deleted <> 1"; 4961 4962 // If we are loading user data based on anything other than id, 4963 // we must also restrict our search based on mnet host. 4964 if ($field != 'id') { 4965 if (empty($mnethostid)) { 4966 // If empty, we restrict to local users. 4967 $mnethostid = $CFG->mnet_localhost_id; 4968 } 4969 } 4970 if (!empty($mnethostid)) { 4971 $params['mnethostid'] = $mnethostid; 4972 $constraints .= " AND mnethostid = :mnethostid"; 4973 } 4974 4975 if ($idsubselect) { 4976 $constraints .= " AND id IN (SELECT id FROM {user} WHERE {$idsubselect})"; 4977 } 4978 4979 // Get all the basic user data. 4980 try { 4981 // Make sure that there's only a single record that matches our query. 4982 // For example, when fetching by email, multiple records might match the query as there's no guarantee that email addresses 4983 // are unique. Therefore we can't reliably tell whether the user profile data that we're fetching is the correct one. 4984 $user = $DB->get_record_select('user', $constraints, $params, '*', MUST_EXIST); 4985 } catch (dml_exception $exception) { 4986 if ($throwexception) { 4987 throw $exception; 4988 } else { 4989 // Return false when no records or multiple records were found. 4990 return false; 4991 } 4992 } 4993 4994 // Get various settings and preferences. 4995 4996 // Preload preference cache. 4997 check_user_preferences_loaded($user); 4998 4999 // Load course enrolment related stuff. 5000 $user->lastcourseaccess = array(); // During last session. 5001 $user->currentcourseaccess = array(); // During current session. 5002 if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid' => $user->id))) { 5003 foreach ($lastaccesses as $lastaccess) { 5004 $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess; 5005 } 5006 } 5007 5008 // Add cohort theme. 5009 if (!empty($CFG->allowcohortthemes)) { 5010 require_once($CFG->dirroot . '/cohort/lib.php'); 5011 if ($cohorttheme = cohort_get_user_cohort_theme($user->id)) { 5012 $user->cohorttheme = $cohorttheme; 5013 } 5014 } 5015 5016 // Add the custom profile fields to the user record. 5017 $user->profile = array(); 5018 if (!isguestuser($user)) { 5019 require_once($CFG->dirroot.'/user/profile/lib.php'); 5020 profile_load_custom_fields($user); 5021 } 5022 5023 // Rewrite some variables if necessary. 5024 if (!empty($user->description)) { 5025 // No need to cart all of it around. 5026 $user->description = true; 5027 } 5028 if (isguestuser($user)) { 5029 // Guest language always same as site. 5030 $user->lang = get_newuser_language(); 5031 // Name always in current language. 5032 $user->firstname = get_string('guestuser'); 5033 $user->lastname = ' '; 5034 } 5035 5036 return $user; 5037 } 5038 5039 /** 5040 * Validate a password against the configured password policy 5041 * 5042 * @param string $password the password to be checked against the password policy 5043 * @param string $errmsg the error message to display when the password doesn't comply with the policy. 5044 * @param stdClass $user the user object to perform password validation against. Defaults to null if not provided. 5045 * 5046 * @return bool true if the password is valid according to the policy. false otherwise. 5047 */ 5048 function check_password_policy($password, &$errmsg, $user = null) { 5049 global $CFG; 5050 5051 if (!empty($CFG->passwordpolicy) && !isguestuser($user)) { 5052 $errmsg = ''; 5053 if (core_text::strlen($password) < $CFG->minpasswordlength) { 5054 $errmsg .= '<div>'. get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength) .'</div>'; 5055 } 5056 if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) { 5057 $errmsg .= '<div>'. get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits) .'</div>'; 5058 } 5059 if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) { 5060 $errmsg .= '<div>'. get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower) .'</div>'; 5061 } 5062 if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) { 5063 $errmsg .= '<div>'. get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper) .'</div>'; 5064 } 5065 if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) { 5066 $errmsg .= '<div>'. get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum) .'</div>'; 5067 } 5068 if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) { 5069 $errmsg .= '<div>'. get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars) .'</div>'; 5070 } 5071 5072 // Fire any additional password policy functions from plugins. 5073 // Plugin functions should output an error message string or empty string for success. 5074 $pluginsfunction = get_plugins_with_function('check_password_policy'); 5075 foreach ($pluginsfunction as $plugintype => $plugins) { 5076 foreach ($plugins as $pluginfunction) { 5077 $pluginerr = $pluginfunction($password, $user); 5078 if ($pluginerr) { 5079 $errmsg .= '<div>'. $pluginerr .'</div>'; 5080 } 5081 } 5082 } 5083 } 5084 5085 if ($errmsg == '') { 5086 return true; 5087 } else { 5088 return false; 5089 } 5090 } 5091 5092 5093 /** 5094 * When logging in, this function is run to set certain preferences for the current SESSION. 5095 */ 5096 function set_login_session_preferences() { 5097 global $SESSION; 5098 5099 $SESSION->justloggedin = true; 5100 5101 unset($SESSION->lang); 5102 unset($SESSION->forcelang); 5103 unset($SESSION->load_navigation_admin); 5104 } 5105 5106 5107 /** 5108 * Delete a course, including all related data from the database, and any associated files. 5109 * 5110 * @param mixed $courseorid The id of the course or course object to delete. 5111 * @param bool $showfeedback Whether to display notifications of each action the function performs. 5112 * @return bool true if all the removals succeeded. false if there were any failures. If this 5113 * method returns false, some of the removals will probably have succeeded, and others 5114 * failed, but you have no way of knowing which. 5115 */ 5116 function delete_course($courseorid, $showfeedback = true) { 5117 global $DB, $CFG; 5118 5119 if (is_object($courseorid)) { 5120 $courseid = $courseorid->id; 5121 $course = $courseorid; 5122 } else { 5123 $courseid = $courseorid; 5124 if (!$course = $DB->get_record('course', array('id' => $courseid))) { 5125 return false; 5126 } 5127 } 5128 $context = context_course::instance($courseid); 5129 5130 // Frontpage course can not be deleted!! 5131 if ($courseid == SITEID) { 5132 return false; 5133 } 5134 5135 // Allow plugins to use this course before we completely delete it. 5136 if ($pluginsfunction = get_plugins_with_function('pre_course_delete')) { 5137 foreach ($pluginsfunction as $plugintype => $plugins) { 5138 foreach ($plugins as $pluginfunction) { 5139 $pluginfunction($course); 5140 } 5141 } 5142 } 5143 5144 // Tell the search manager we are about to delete a course. This prevents us sending updates 5145 // for each individual context being deleted. 5146 \core_search\manager::course_deleting_start($courseid); 5147 5148 $handler = core_course\customfield\course_handler::create(); 5149 $handler->delete_instance($courseid); 5150 5151 // Make the course completely empty. 5152 remove_course_contents($courseid, $showfeedback); 5153 5154 // Communication provider delete associated information. 5155 $communication = \core_communication\api::load_by_instance( 5156 $context, 5157 'core_course', 5158 'coursecommunication', 5159 $course->id 5160 ); 5161 5162 // Delete the course and related context instance. 5163 context_helper::delete_instance(CONTEXT_COURSE, $courseid); 5164 5165 // Update communication room membership of enrolled users. 5166 require_once($CFG->libdir . '/enrollib.php'); 5167 $courseusers = enrol_get_course_users($courseid); 5168 $enrolledusers = []; 5169 5170 foreach ($courseusers as $user) { 5171 $enrolledusers[] = $user->id; 5172 } 5173 5174 $communication->remove_members_from_room($enrolledusers); 5175 5176 $communication->delete_room(); 5177 5178 $DB->delete_records("course", array("id" => $courseid)); 5179 $DB->delete_records("course_format_options", array("courseid" => $courseid)); 5180 5181 // Reset all course related caches here. 5182 core_courseformat\base::reset_course_cache($courseid); 5183 5184 // Tell search that we have deleted the course so it can delete course data from the index. 5185 \core_search\manager::course_deleting_finish($courseid); 5186 5187 // Trigger a course deleted event. 5188 $event = \core\event\course_deleted::create(array( 5189 'objectid' => $course->id, 5190 'context' => $context, 5191 'other' => array( 5192 'shortname' => $course->shortname, 5193 'fullname' => $course->fullname, 5194 'idnumber' => $course->idnumber 5195 ) 5196 )); 5197 $event->add_record_snapshot('course', $course); 5198 $event->trigger(); 5199 5200 return true; 5201 } 5202 5203 /** 5204 * Clear a course out completely, deleting all content but don't delete the course itself. 5205 * 5206 * This function does not verify any permissions. 5207 * 5208 * Please note this function also deletes all user enrolments, 5209 * enrolment instances and role assignments by default. 5210 * 5211 * $options: 5212 * - 'keep_roles_and_enrolments' - false by default 5213 * - 'keep_groups_and_groupings' - false by default 5214 * 5215 * @param int $courseid The id of the course that is being deleted 5216 * @param bool $showfeedback Whether to display notifications of each action the function performs. 5217 * @param array $options extra options 5218 * @return bool true if all the removals succeeded. false if there were any failures. If this 5219 * method returns false, some of the removals will probably have succeeded, and others 5220 * failed, but you have no way of knowing which. 5221 */ 5222 function remove_course_contents($courseid, $showfeedback = true, array $options = null) { 5223 global $CFG, $DB, $OUTPUT; 5224 5225 require_once($CFG->libdir.'/badgeslib.php'); 5226 require_once($CFG->libdir.'/completionlib.php'); 5227 require_once($CFG->libdir.'/questionlib.php'); 5228 require_once($CFG->libdir.'/gradelib.php'); 5229 require_once($CFG->dirroot.'/group/lib.php'); 5230 require_once($CFG->dirroot.'/comment/lib.php'); 5231 require_once($CFG->dirroot.'/rating/lib.php'); 5232 require_once($CFG->dirroot.'/notes/lib.php'); 5233 5234 // Handle course badges. 5235 badges_handle_course_deletion($courseid); 5236 5237 // NOTE: these concatenated strings are suboptimal, but it is just extra info... 5238 $strdeleted = get_string('deleted').' - '; 5239 5240 // Some crazy wishlist of stuff we should skip during purging of course content. 5241 $options = (array)$options; 5242 5243 $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); 5244 $coursecontext = context_course::instance($courseid); 5245 $fs = get_file_storage(); 5246 5247 // Delete course completion information, this has to be done before grades and enrols. 5248 $cc = new completion_info($course); 5249 $cc->clear_criteria(); 5250 if ($showfeedback) { 5251 echo $OUTPUT->notification($strdeleted.get_string('completion', 'completion'), 'notifysuccess'); 5252 } 5253 5254 // Remove all data from gradebook - this needs to be done before course modules 5255 // because while deleting this information, the system may need to reference 5256 // the course modules that own the grades. 5257 remove_course_grades($courseid, $showfeedback); 5258 remove_grade_letters($coursecontext, $showfeedback); 5259 5260 // Delete course blocks in any all child contexts, 5261 // they may depend on modules so delete them first. 5262 $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2. 5263 foreach ($childcontexts as $childcontext) { 5264 blocks_delete_all_for_context($childcontext->id); 5265 } 5266 unset($childcontexts); 5267 blocks_delete_all_for_context($coursecontext->id); 5268 if ($showfeedback) { 5269 echo $OUTPUT->notification($strdeleted.get_string('type_block_plural', 'plugin'), 'notifysuccess'); 5270 } 5271 5272 $DB->set_field('course_modules', 'deletioninprogress', '1', ['course' => $courseid]); 5273 rebuild_course_cache($courseid, true); 5274 5275 // Get the list of all modules that are properly installed. 5276 $allmodules = $DB->get_records_menu('modules', array(), '', 'name, id'); 5277 5278 // Delete every instance of every module, 5279 // this has to be done before deleting of course level stuff. 5280 $locations = core_component::get_plugin_list('mod'); 5281 foreach ($locations as $modname => $moddir) { 5282 if ($modname === 'NEWMODULE') { 5283 continue; 5284 } 5285 if (array_key_exists($modname, $allmodules)) { 5286 $sql = "SELECT cm.*, m.id AS modinstance, m.name, '$modname' AS modname 5287 FROM {".$modname."} m 5288 LEFT JOIN {course_modules} cm ON cm.instance = m.id AND cm.module = :moduleid 5289 WHERE m.course = :courseid"; 5290 $instances = $DB->get_records_sql($sql, array('courseid' => $course->id, 5291 'modulename' => $modname, 'moduleid' => $allmodules[$modname])); 5292 5293 include_once("$moddir/lib.php"); // Shows php warning only if plugin defective. 5294 $moddelete = $modname .'_delete_instance'; // Delete everything connected to an instance. 5295 5296 if ($instances) { 5297 foreach ($instances as $cm) { 5298 if ($cm->id) { 5299 // Delete activity context questions and question categories. 5300 question_delete_activity($cm); 5301 // Notify the competency subsystem. 5302 \core_competency\api::hook_course_module_deleted($cm); 5303 5304 // Delete all tag instances associated with the instance of this module. 5305 core_tag_tag::delete_instances("mod_{$modname}", null, context_module::instance($cm->id)->id); 5306 core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id); 5307 } 5308 if (function_exists($moddelete)) { 5309 // This purges all module data in related tables, extra user prefs, settings, etc. 5310 $moddelete($cm->modinstance); 5311 } else { 5312 // NOTE: we should not allow installation of modules with missing delete support! 5313 debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!"); 5314 $DB->delete_records($modname, array('id' => $cm->modinstance)); 5315 } 5316 5317 if ($cm->id) { 5318 // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition. 5319 context_helper::delete_instance(CONTEXT_MODULE, $cm->id); 5320 $DB->delete_records('course_modules_completion', ['coursemoduleid' => $cm->id]); 5321 $DB->delete_records('course_modules_viewed', ['coursemoduleid' => $cm->id]); 5322 $DB->delete_records('course_modules', array('id' => $cm->id)); 5323 rebuild_course_cache($cm->course, true); 5324 } 5325 } 5326 } 5327 if ($instances and $showfeedback) { 5328 echo $OUTPUT->notification($strdeleted.get_string('pluginname', $modname), 'notifysuccess'); 5329 } 5330 } else { 5331 // Ooops, this module is not properly installed, force-delete it in the next block. 5332 } 5333 } 5334 5335 // We have tried to delete everything the nice way - now let's force-delete any remaining module data. 5336 5337 // Delete completion defaults. 5338 $DB->delete_records("course_completion_defaults", array("course" => $courseid)); 5339 5340 // Remove all data from availability and completion tables that is associated 5341 // with course-modules belonging to this course. Note this is done even if the 5342 // features are not enabled now, in case they were enabled previously. 5343 $DB->delete_records_subquery('course_modules_completion', 'coursemoduleid', 'id', 5344 'SELECT id from {course_modules} WHERE course = ?', [$courseid]); 5345 $DB->delete_records_subquery('course_modules_viewed', 'coursemoduleid', 'id', 5346 'SELECT id from {course_modules} WHERE course = ?', [$courseid]); 5347 5348 // Remove course-module data that has not been removed in modules' _delete_instance callbacks. 5349 $cms = $DB->get_records('course_modules', array('course' => $course->id)); 5350 $allmodulesbyid = array_flip($allmodules); 5351 foreach ($cms as $cm) { 5352 if (array_key_exists($cm->module, $allmodulesbyid)) { 5353 try { 5354 $DB->delete_records($allmodulesbyid[$cm->module], array('id' => $cm->instance)); 5355 } catch (Exception $e) { 5356 // Ignore weird or missing table problems. 5357 } 5358 } 5359 context_helper::delete_instance(CONTEXT_MODULE, $cm->id); 5360 $DB->delete_records('course_modules', array('id' => $cm->id)); 5361 rebuild_course_cache($cm->course, true); 5362 } 5363 5364 if ($showfeedback) { 5365 echo $OUTPUT->notification($strdeleted.get_string('type_mod_plural', 'plugin'), 'notifysuccess'); 5366 } 5367 5368 // Delete questions and question categories. 5369 question_delete_course($course); 5370 if ($showfeedback) { 5371 echo $OUTPUT->notification($strdeleted.get_string('questions', 'question'), 'notifysuccess'); 5372 } 5373 5374 // Delete content bank contents. 5375 $cb = new \core_contentbank\contentbank(); 5376 $cbdeleted = $cb->delete_contents($coursecontext); 5377 if ($showfeedback && $cbdeleted) { 5378 echo $OUTPUT->notification($strdeleted.get_string('contentbank', 'contentbank'), 'notifysuccess'); 5379 } 5380 5381 // Make sure there are no subcontexts left - all valid blocks and modules should be already gone. 5382 $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2. 5383 foreach ($childcontexts as $childcontext) { 5384 $childcontext->delete(); 5385 } 5386 unset($childcontexts); 5387 5388 // Remove roles and enrolments by default. 5389 if (empty($options['keep_roles_and_enrolments'])) { 5390 // This hack is used in restore when deleting contents of existing course. 5391 // During restore, we should remove only enrolment related data that the user performing the restore has a 5392 // permission to remove. 5393 $userid = $options['userid'] ?? null; 5394 enrol_course_delete($course, $userid); 5395 role_unassign_all(array('contextid' => $coursecontext->id, 'component' => ''), true); 5396 if ($showfeedback) { 5397 echo $OUTPUT->notification($strdeleted.get_string('type_enrol_plural', 'plugin'), 'notifysuccess'); 5398 } 5399 } 5400 5401 // Delete any groups, removing members and grouping/course links first. 5402 if (empty($options['keep_groups_and_groupings'])) { 5403 groups_delete_groupings($course->id, $showfeedback); 5404 groups_delete_groups($course->id, $showfeedback); 5405 } 5406 5407 // Filters be gone! 5408 filter_delete_all_for_context($coursecontext->id); 5409 5410 // Notes, you shall not pass! 5411 note_delete_all($course->id); 5412 5413 // Die comments! 5414 comment::delete_comments($coursecontext->id); 5415 5416 // Ratings are history too. 5417 $delopt = new stdclass(); 5418 $delopt->contextid = $coursecontext->id; 5419 $rm = new rating_manager(); 5420 $rm->delete_ratings($delopt); 5421 5422 // Delete course tags. 5423 core_tag_tag::remove_all_item_tags('core', 'course', $course->id); 5424 5425 // Give the course format the opportunity to remove its obscure data. 5426 $format = course_get_format($course); 5427 $format->delete_format_data(); 5428 5429 // Notify the competency subsystem. 5430 \core_competency\api::hook_course_deleted($course); 5431 5432 // Delete calendar events. 5433 $DB->delete_records('event', array('courseid' => $course->id)); 5434 $fs->delete_area_files($coursecontext->id, 'calendar'); 5435 5436 // Delete all related records in other core tables that may have a courseid 5437 // This array stores the tables that need to be cleared, as 5438 // table_name => column_name that contains the course id. 5439 $tablestoclear = array( 5440 'backup_courses' => 'courseid', // Scheduled backup stuff. 5441 'user_lastaccess' => 'courseid', // User access info. 5442 ); 5443 foreach ($tablestoclear as $table => $col) { 5444 $DB->delete_records($table, array($col => $course->id)); 5445 } 5446 5447 // Delete all course backup files. 5448 $fs->delete_area_files($coursecontext->id, 'backup'); 5449 5450 // Cleanup course record - remove links to deleted stuff. 5451 // Do not wipe cacherev, as this course might be reused and we need to ensure that it keeps 5452 // increasing. 5453 $oldcourse = new stdClass(); 5454 $oldcourse->id = $course->id; 5455 $oldcourse->summary = ''; 5456 $oldcourse->legacyfiles = 0; 5457 if (!empty($options['keep_groups_and_groupings'])) { 5458 $oldcourse->defaultgroupingid = 0; 5459 } 5460 $DB->update_record('course', $oldcourse); 5461 5462 // Delete course sections. 5463 $DB->delete_records('course_sections', array('course' => $course->id)); 5464 5465 // Delete legacy, section and any other course files. 5466 $fs->delete_area_files($coursecontext->id, 'course'); // Files from summary and section. 5467 5468 // Delete all remaining stuff linked to context such as files, comments, ratings, etc. 5469 if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) { 5470 // Easy, do not delete the context itself... 5471 $coursecontext->delete_content(); 5472 } else { 5473 // Hack alert!!!! 5474 // We can not drop all context stuff because it would bork enrolments and roles, 5475 // there might be also files used by enrol plugins... 5476 } 5477 5478 // Delete legacy files - just in case some files are still left there after conversion to new file api, 5479 // also some non-standard unsupported plugins may try to store something there. 5480 fulldelete($CFG->dataroot.'/'.$course->id); 5481 5482 // Delete from cache to reduce the cache size especially makes sense in case of bulk course deletion. 5483 course_modinfo::purge_course_cache($courseid); 5484 5485 // Trigger a course content deleted event. 5486 $event = \core\event\course_content_deleted::create(array( 5487 'objectid' => $course->id, 5488 'context' => $coursecontext, 5489 'other' => array('shortname' => $course->shortname, 5490 'fullname' => $course->fullname, 5491 'options' => $options) // Passing this for legacy reasons. 5492 )); 5493 $event->add_record_snapshot('course', $course); 5494 $event->trigger(); 5495 5496 return true; 5497 } 5498 5499 /** 5500 * Change dates in module - used from course reset. 5501 * 5502 * @param string $modname forum, assignment, etc 5503 * @param array $fields array of date fields from mod table 5504 * @param int $timeshift time difference 5505 * @param int $courseid 5506 * @param int $modid (Optional) passed if specific mod instance in course needs to be updated. 5507 * @return bool success 5508 */ 5509 function shift_course_mod_dates($modname, $fields, $timeshift, $courseid, $modid = 0) { 5510 global $CFG, $DB; 5511 include_once($CFG->dirroot.'/mod/'.$modname.'/lib.php'); 5512 5513 $return = true; 5514 $params = array($timeshift, $courseid); 5515 foreach ($fields as $field) { 5516 $updatesql = "UPDATE {".$modname."} 5517 SET $field = $field + ? 5518 WHERE course=? AND $field<>0"; 5519 if ($modid) { 5520 $updatesql .= ' AND id=?'; 5521 $params[] = $modid; 5522 } 5523 $return = $DB->execute($updatesql, $params) && $return; 5524 } 5525 5526 return $return; 5527 } 5528 5529 /** 5530 * This function will empty a course of user data. 5531 * It will retain the activities and the structure of the course. 5532 * 5533 * @param object $data an object containing all the settings including courseid (without magic quotes) 5534 * @return array status array of array component, item, error 5535 */ 5536 function reset_course_userdata($data) { 5537 global $CFG, $DB; 5538 require_once($CFG->libdir.'/gradelib.php'); 5539 require_once($CFG->libdir.'/completionlib.php'); 5540 require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php'); 5541 require_once($CFG->dirroot.'/group/lib.php'); 5542 5543 $data->courseid = $data->id; 5544 $context = context_course::instance($data->courseid); 5545 5546 $eventparams = array( 5547 'context' => $context, 5548 'courseid' => $data->id, 5549 'other' => array( 5550 'reset_options' => (array) $data 5551 ) 5552 ); 5553 $event = \core\event\course_reset_started::create($eventparams); 5554 $event->trigger(); 5555 5556 // Calculate the time shift of dates. 5557 if (!empty($data->reset_start_date)) { 5558 // Time part of course startdate should be zero. 5559 $data->timeshift = $data->reset_start_date - usergetmidnight($data->reset_start_date_old); 5560 } else { 5561 $data->timeshift = 0; 5562 } 5563 5564 // Result array: component, item, error. 5565 $status = array(); 5566 5567 // Start the resetting. 5568 $componentstr = get_string('general'); 5569 5570 // Move the course start time. 5571 if (!empty($data->reset_start_date) and $data->timeshift) { 5572 // Change course start data. 5573 $DB->set_field('course', 'startdate', $data->reset_start_date, array('id' => $data->courseid)); 5574 // Update all course and group events - do not move activity events. 5575 $updatesql = "UPDATE {event} 5576 SET timestart = timestart + ? 5577 WHERE courseid=? AND instance=0"; 5578 $DB->execute($updatesql, array($data->timeshift, $data->courseid)); 5579 5580 // Update any date activity restrictions. 5581 if ($CFG->enableavailability) { 5582 \availability_date\condition::update_all_dates($data->courseid, $data->timeshift); 5583 } 5584 5585 // Update completion expected dates. 5586 if ($CFG->enablecompletion) { 5587 $modinfo = get_fast_modinfo($data->courseid); 5588 $changed = false; 5589 foreach ($modinfo->get_cms() as $cm) { 5590 if ($cm->completion && !empty($cm->completionexpected)) { 5591 $DB->set_field('course_modules', 'completionexpected', $cm->completionexpected + $data->timeshift, 5592 array('id' => $cm->id)); 5593 $changed = true; 5594 } 5595 } 5596 5597 // Clear course cache if changes made. 5598 if ($changed) { 5599 rebuild_course_cache($data->courseid, true); 5600 } 5601 5602 // Update course date completion criteria. 5603 \completion_criteria_date::update_date($data->courseid, $data->timeshift); 5604 } 5605 5606 $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false); 5607 } 5608 5609 if (!empty($data->reset_end_date)) { 5610 // If the user set a end date value respect it. 5611 $DB->set_field('course', 'enddate', $data->reset_end_date, array('id' => $data->courseid)); 5612 } else if ($data->timeshift > 0 && $data->reset_end_date_old) { 5613 // If there is a time shift apply it to the end date as well. 5614 $enddate = $data->reset_end_date_old + $data->timeshift; 5615 $DB->set_field('course', 'enddate', $enddate, array('id' => $data->courseid)); 5616 } 5617 5618 if (!empty($data->reset_events)) { 5619 $DB->delete_records('event', array('courseid' => $data->courseid)); 5620 $status[] = array('component' => $componentstr, 'item' => get_string('deleteevents', 'calendar'), 'error' => false); 5621 } 5622 5623 if (!empty($data->reset_notes)) { 5624 require_once($CFG->dirroot.'/notes/lib.php'); 5625 note_delete_all($data->courseid); 5626 $status[] = array('component' => $componentstr, 'item' => get_string('deletenotes', 'notes'), 'error' => false); 5627 } 5628 5629 if (!empty($data->delete_blog_associations)) { 5630 require_once($CFG->dirroot.'/blog/lib.php'); 5631 blog_remove_associations_for_course($data->courseid); 5632 $status[] = array('component' => $componentstr, 'item' => get_string('deleteblogassociations', 'blog'), 'error' => false); 5633 } 5634 5635 if (!empty($data->reset_completion)) { 5636 // Delete course and activity completion information. 5637 $course = $DB->get_record('course', array('id' => $data->courseid)); 5638 $cc = new completion_info($course); 5639 $cc->delete_all_completion_data(); 5640 $status[] = array('component' => $componentstr, 5641 'item' => get_string('deletecompletiondata', 'completion'), 'error' => false); 5642 } 5643 5644 if (!empty($data->reset_competency_ratings)) { 5645 \core_competency\api::hook_course_reset_competency_ratings($data->courseid); 5646 $status[] = array('component' => $componentstr, 5647 'item' => get_string('deletecompetencyratings', 'core_competency'), 'error' => false); 5648 } 5649 5650 $componentstr = get_string('roles'); 5651 5652 if (!empty($data->reset_roles_overrides)) { 5653 $children = $context->get_child_contexts(); 5654 foreach ($children as $child) { 5655 $child->delete_capabilities(); 5656 } 5657 $context->delete_capabilities(); 5658 $status[] = array('component' => $componentstr, 'item' => get_string('deletecourseoverrides', 'role'), 'error' => false); 5659 } 5660 5661 if (!empty($data->reset_roles_local)) { 5662 $children = $context->get_child_contexts(); 5663 foreach ($children as $child) { 5664 role_unassign_all(array('contextid' => $child->id)); 5665 } 5666 $status[] = array('component' => $componentstr, 'item' => get_string('deletelocalroles', 'role'), 'error' => false); 5667 } 5668 5669 // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc. 5670 $data->unenrolled = array(); 5671 if (!empty($data->unenrol_users)) { 5672 $plugins = enrol_get_plugins(true); 5673 $instances = enrol_get_instances($data->courseid, true); 5674 foreach ($instances as $key => $instance) { 5675 if (!isset($plugins[$instance->enrol])) { 5676 unset($instances[$key]); 5677 continue; 5678 } 5679 } 5680 5681 $usersroles = enrol_get_course_users_roles($data->courseid); 5682 foreach ($data->unenrol_users as $withroleid) { 5683 if ($withroleid) { 5684 $sql = "SELECT ue.* 5685 FROM {user_enrolments} ue 5686 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid) 5687 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid) 5688 JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)"; 5689 $params = array('courseid' => $data->courseid, 'roleid' => $withroleid, 'courselevel' => CONTEXT_COURSE); 5690 5691 } else { 5692 // Without any role assigned at course context. 5693 $sql = "SELECT ue.* 5694 FROM {user_enrolments} ue 5695 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid) 5696 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid) 5697 LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid) 5698 WHERE ra.id IS null"; 5699 $params = array('courseid' => $data->courseid, 'courselevel' => CONTEXT_COURSE); 5700 } 5701 5702 $rs = $DB->get_recordset_sql($sql, $params); 5703 foreach ($rs as $ue) { 5704 if (!isset($instances[$ue->enrolid])) { 5705 continue; 5706 } 5707 $instance = $instances[$ue->enrolid]; 5708 $plugin = $plugins[$instance->enrol]; 5709 if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) { 5710 continue; 5711 } 5712 5713 if ($withroleid && count($usersroles[$ue->userid]) > 1) { 5714 // If we don't remove all roles and user has more than one role, just remove this role. 5715 role_unassign($withroleid, $ue->userid, $context->id); 5716 5717 unset($usersroles[$ue->userid][$withroleid]); 5718 } else { 5719 // If we remove all roles or user has only one role, unenrol user from course. 5720 $plugin->unenrol_user($instance, $ue->userid); 5721 } 5722 $data->unenrolled[$ue->userid] = $ue->userid; 5723 } 5724 $rs->close(); 5725 } 5726 } 5727 if (!empty($data->unenrolled)) { 5728 $status[] = array( 5729 'component' => $componentstr, 5730 'item' => get_string('unenrol', 'enrol').' ('.count($data->unenrolled).')', 5731 'error' => false 5732 ); 5733 } 5734 5735 $componentstr = get_string('groups'); 5736 5737 // Remove all group members. 5738 if (!empty($data->reset_groups_members)) { 5739 groups_delete_group_members($data->courseid); 5740 $status[] = array('component' => $componentstr, 'item' => get_string('removegroupsmembers', 'group'), 'error' => false); 5741 } 5742 5743 // Remove all groups. 5744 if (!empty($data->reset_groups_remove)) { 5745 groups_delete_groups($data->courseid, false); 5746 $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroups', 'group'), 'error' => false); 5747 } 5748 5749 // Remove all grouping members. 5750 if (!empty($data->reset_groupings_members)) { 5751 groups_delete_groupings_groups($data->courseid, false); 5752 $status[] = array('component' => $componentstr, 'item' => get_string('removegroupingsmembers', 'group'), 'error' => false); 5753 } 5754 5755 // Remove all groupings. 5756 if (!empty($data->reset_groupings_remove)) { 5757 groups_delete_groupings($data->courseid, false); 5758 $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroupings', 'group'), 'error' => false); 5759 } 5760 5761 // Look in every instance of every module for data to delete. 5762 $unsupportedmods = array(); 5763 if ($allmods = $DB->get_records('modules') ) { 5764 foreach ($allmods as $mod) { 5765 $modname = $mod->name; 5766 $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php'; 5767 $moddeleteuserdata = $modname.'_reset_userdata'; // Function to delete user data. 5768 if (file_exists($modfile)) { 5769 if (!$DB->count_records($modname, array('course' => $data->courseid))) { 5770 continue; // Skip mods with no instances. 5771 } 5772 include_once($modfile); 5773 if (function_exists($moddeleteuserdata)) { 5774 $modstatus = $moddeleteuserdata($data); 5775 if (is_array($modstatus)) { 5776 $status = array_merge($status, $modstatus); 5777 } else { 5778 debugging('Module '.$modname.' returned incorrect staus - must be an array!'); 5779 } 5780 } else { 5781 $unsupportedmods[] = $mod; 5782 } 5783 } else { 5784 debugging('Missing lib.php in '.$modname.' module!'); 5785 } 5786 // Update calendar events for all modules. 5787 course_module_bulk_update_calendar_events($modname, $data->courseid); 5788 } 5789 // Purge the course cache after resetting course start date. MDL-76936 5790 if ($data->timeshift) { 5791 course_modinfo::purge_course_cache($data->courseid); 5792 } 5793 } 5794 5795 // Mention unsupported mods. 5796 if (!empty($unsupportedmods)) { 5797 foreach ($unsupportedmods as $mod) { 5798 $status[] = array( 5799 'component' => get_string('modulenameplural', $mod->name), 5800 'item' => '', 5801 'error' => get_string('resetnotimplemented') 5802 ); 5803 } 5804 } 5805 5806 $componentstr = get_string('gradebook', 'grades'); 5807 // Reset gradebook,. 5808 if (!empty($data->reset_gradebook_items)) { 5809 remove_course_grades($data->courseid, false); 5810 grade_grab_course_grades($data->courseid); 5811 grade_regrade_final_grades($data->courseid); 5812 $status[] = array('component' => $componentstr, 'item' => get_string('removeallcourseitems', 'grades'), 'error' => false); 5813 5814 } else if (!empty($data->reset_gradebook_grades)) { 5815 grade_course_reset($data->courseid); 5816 $status[] = array('component' => $componentstr, 'item' => get_string('removeallcoursegrades', 'grades'), 'error' => false); 5817 } 5818 // Reset comments. 5819 if (!empty($data->reset_comments)) { 5820 require_once($CFG->dirroot.'/comment/lib.php'); 5821 comment::reset_course_page_comments($context); 5822 } 5823 5824 $event = \core\event\course_reset_ended::create($eventparams); 5825 $event->trigger(); 5826 5827 return $status; 5828 } 5829 5830 /** 5831 * Generate an email processing address. 5832 * 5833 * @param int $modid 5834 * @param string $modargs 5835 * @return string Returns email processing address 5836 */ 5837 function generate_email_processing_address($modid, $modargs) { 5838 global $CFG; 5839 5840 $header = $CFG->mailprefix . substr(base64_encode(pack('C', $modid)), 0, 2).$modargs; 5841 return $header . substr(md5($header.get_site_identifier()), 0, 16).'@'.$CFG->maildomain; 5842 } 5843 5844 /** 5845 * ? 5846 * 5847 * @todo Finish documenting this function 5848 * 5849 * @param string $modargs 5850 * @param string $body Currently unused 5851 */ 5852 function moodle_process_email($modargs, $body) { 5853 global $DB; 5854 5855 // The first char should be an unencoded letter. We'll take this as an action. 5856 switch ($modargs[0]) { 5857 case 'B': { // Bounce. 5858 list(, $userid) = unpack('V', base64_decode(substr($modargs, 1, 8))); 5859 if ($user = $DB->get_record("user", array('id' => $userid), "id,email")) { 5860 // Check the half md5 of their email. 5861 $md5check = substr(md5($user->email), 0, 16); 5862 if ($md5check == substr($modargs, -16)) { 5863 set_bounce_count($user); 5864 } 5865 // Else maybe they've already changed it? 5866 } 5867 } 5868 break; 5869 // Maybe more later? 5870 } 5871 } 5872 5873 // CORRESPONDENCE. 5874 5875 /** 5876 * Get mailer instance, enable buffering, flush buffer or disable buffering. 5877 * 5878 * @param string $action 'get', 'buffer', 'close' or 'flush' 5879 * @return moodle_phpmailer|null mailer instance if 'get' used or nothing 5880 */ 5881 function get_mailer($action='get') { 5882 global $CFG; 5883 5884 /** @var moodle_phpmailer $mailer */ 5885 static $mailer = null; 5886 static $counter = 0; 5887 5888 if (!isset($CFG->smtpmaxbulk)) { 5889 $CFG->smtpmaxbulk = 1; 5890 } 5891 5892 if ($action == 'get') { 5893 $prevkeepalive = false; 5894 5895 if (isset($mailer) and $mailer->Mailer == 'smtp') { 5896 if ($counter < $CFG->smtpmaxbulk and !$mailer->isError()) { 5897 $counter++; 5898 // Reset the mailer. 5899 $mailer->Priority = 3; 5900 $mailer->CharSet = 'UTF-8'; // Our default. 5901 $mailer->ContentType = "text/plain"; 5902 $mailer->Encoding = "8bit"; 5903 $mailer->From = "root@localhost"; 5904 $mailer->FromName = "Root User"; 5905 $mailer->Sender = ""; 5906 $mailer->Subject = ""; 5907 $mailer->Body = ""; 5908 $mailer->AltBody = ""; 5909 $mailer->ConfirmReadingTo = ""; 5910 5911 $mailer->clearAllRecipients(); 5912 $mailer->clearReplyTos(); 5913 $mailer->clearAttachments(); 5914 $mailer->clearCustomHeaders(); 5915 return $mailer; 5916 } 5917 5918 $prevkeepalive = $mailer->SMTPKeepAlive; 5919 get_mailer('flush'); 5920 } 5921 5922 require_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php'); 5923 $mailer = new moodle_phpmailer(); 5924 5925 $counter = 1; 5926 5927 if ($CFG->smtphosts == 'qmail') { 5928 // Use Qmail system. 5929 $mailer->isQmail(); 5930 5931 } else if (empty($CFG->smtphosts)) { 5932 // Use PHP mail() = sendmail. 5933 $mailer->isMail(); 5934 5935 } else { 5936 // Use SMTP directly. 5937 $mailer->isSMTP(); 5938 if (!empty($CFG->debugsmtp) && (!empty($CFG->debugdeveloper))) { 5939 $mailer->SMTPDebug = 3; 5940 } 5941 // Specify main and backup servers. 5942 $mailer->Host = $CFG->smtphosts; 5943 // Specify secure connection protocol. 5944 $mailer->SMTPSecure = $CFG->smtpsecure; 5945 // Use previous keepalive. 5946 $mailer->SMTPKeepAlive = $prevkeepalive; 5947 5948 if ($CFG->smtpuser) { 5949 // Use SMTP authentication. 5950 $mailer->SMTPAuth = true; 5951 $mailer->Username = $CFG->smtpuser; 5952 $mailer->Password = $CFG->smtppass; 5953 } 5954 } 5955 5956 return $mailer; 5957 } 5958 5959 $nothing = null; 5960 5961 // Keep smtp session open after sending. 5962 if ($action == 'buffer') { 5963 if (!empty($CFG->smtpmaxbulk)) { 5964 get_mailer('flush'); 5965 $m = get_mailer(); 5966 if ($m->Mailer == 'smtp') { 5967 $m->SMTPKeepAlive = true; 5968 } 5969 } 5970 return $nothing; 5971 } 5972 5973 // Close smtp session, but continue buffering. 5974 if ($action == 'flush') { 5975 if (isset($mailer) and $mailer->Mailer == 'smtp') { 5976 if (!empty($mailer->SMTPDebug)) { 5977 echo '<pre>'."\n"; 5978 } 5979 $mailer->SmtpClose(); 5980 if (!empty($mailer->SMTPDebug)) { 5981 echo '</pre>'; 5982 } 5983 } 5984 return $nothing; 5985 } 5986 5987 // Close smtp session, do not buffer anymore. 5988 if ($action == 'close') { 5989 if (isset($mailer) and $mailer->Mailer == 'smtp') { 5990 get_mailer('flush'); 5991 $mailer->SMTPKeepAlive = false; 5992 } 5993 $mailer = null; // Better force new instance. 5994 return $nothing; 5995 } 5996 } 5997 5998 /** 5999 * A helper function to test for email diversion 6000 * 6001 * @param string $email 6002 * @return bool Returns true if the email should be diverted 6003 */ 6004 function email_should_be_diverted($email) { 6005 global $CFG; 6006 6007 if (empty($CFG->divertallemailsto)) { 6008 return false; 6009 } 6010 6011 if (empty($CFG->divertallemailsexcept)) { 6012 return true; 6013 } 6014 6015 $patterns = array_map('trim', preg_split("/[\s,]+/", $CFG->divertallemailsexcept, -1, PREG_SPLIT_NO_EMPTY)); 6016 foreach ($patterns as $pattern) { 6017 if (preg_match("/{$pattern}/i", $email)) { 6018 return false; 6019 } 6020 } 6021 6022 return true; 6023 } 6024 6025 /** 6026 * Generate a unique email Message-ID using the moodle domain and install path 6027 * 6028 * @param string $localpart An optional unique message id prefix. 6029 * @return string The formatted ID ready for appending to the email headers. 6030 */ 6031 function generate_email_messageid($localpart = null) { 6032 global $CFG; 6033 6034 $urlinfo = parse_url($CFG->wwwroot); 6035 $base = '@' . $urlinfo['host']; 6036 6037 // If multiple moodles are on the same domain we want to tell them 6038 // apart so we add the install path to the local part. This means 6039 // that the id local part should never contain a / character so 6040 // we can correctly parse the id to reassemble the wwwroot. 6041 if (isset($urlinfo['path'])) { 6042 $base = $urlinfo['path'] . $base; 6043 } 6044 6045 if (empty($localpart)) { 6046 $localpart = uniqid('', true); 6047 } 6048 6049 // Because we may have an option /installpath suffix to the local part 6050 // of the id we need to escape any / chars which are in the $localpart. 6051 $localpart = str_replace('/', '%2F', $localpart); 6052 6053 return '<' . $localpart . $base . '>'; 6054 } 6055 6056 /** 6057 * Send an email to a specified user 6058 * 6059 * @param stdClass $user A {@link $USER} object 6060 * @param stdClass $from A {@link $USER} object 6061 * @param string $subject plain text subject line of the email 6062 * @param string $messagetext plain text version of the message 6063 * @param string $messagehtml complete html version of the message (optional) 6064 * @param string $attachment a file on the filesystem, either relative to $CFG->dataroot or a full path to a file in one of 6065 * the following directories: $CFG->cachedir, $CFG->dataroot, $CFG->dirroot, $CFG->localcachedir, $CFG->tempdir 6066 * @param string $attachname the name of the file (extension indicates MIME) 6067 * @param bool $usetrueaddress determines whether $from email address should 6068 * be sent out. Will be overruled by user profile setting for maildisplay 6069 * @param string $replyto Email address to reply to 6070 * @param string $replytoname Name of reply to recipient 6071 * @param int $wordwrapwidth custom word wrap width, default 79 6072 * @return bool Returns true if mail was sent OK and false if there was an error. 6073 */ 6074 function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '', $attachment = '', $attachname = '', 6075 $usetrueaddress = true, $replyto = '', $replytoname = '', $wordwrapwidth = 79) { 6076 6077 global $CFG, $PAGE, $SITE; 6078 6079 if (empty($user) or empty($user->id)) { 6080 debugging('Can not send email to null user', DEBUG_DEVELOPER); 6081 return false; 6082 } 6083 6084 if (empty($user->email)) { 6085 debugging('Can not send email to user without email: '.$user->id, DEBUG_DEVELOPER); 6086 return false; 6087 } 6088 6089 if (!empty($user->deleted)) { 6090 debugging('Can not send email to deleted user: '.$user->id, DEBUG_DEVELOPER); 6091 return false; 6092 } 6093 6094 if (defined('BEHAT_SITE_RUNNING')) { 6095 // Fake email sending in behat. 6096 return true; 6097 } 6098 6099 if (!empty($CFG->noemailever)) { 6100 // Hidden setting for development sites, set in config.php if needed. 6101 debugging('Not sending email due to $CFG->noemailever config setting', DEBUG_NORMAL); 6102 return true; 6103 } 6104 6105 if (email_should_be_diverted($user->email)) { 6106 $subject = "[DIVERTED {$user->email}] $subject"; 6107 $user = clone($user); 6108 $user->email = $CFG->divertallemailsto; 6109 } 6110 6111 // Skip mail to suspended users. 6112 if ((isset($user->auth) && $user->auth=='nologin') or (isset($user->suspended) && $user->suspended)) { 6113 return true; 6114 } 6115 6116 if (!validate_email($user->email)) { 6117 // We can not send emails to invalid addresses - it might create security issue or confuse the mailer. 6118 debugging("email_to_user: User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending."); 6119 return false; 6120 } 6121 6122 if (over_bounce_threshold($user)) { 6123 debugging("email_to_user: User $user->id (".fullname($user).") is over bounce threshold! Not sending."); 6124 return false; 6125 } 6126 6127 // TLD .invalid is specifically reserved for invalid domain names. 6128 // For More information, see {@link http://tools.ietf.org/html/rfc2606#section-2}. 6129 if (substr($user->email, -8) == '.invalid') { 6130 debugging("email_to_user: User $user->id (".fullname($user).") email domain ($user->email) is invalid! Not sending."); 6131 return true; // This is not an error. 6132 } 6133 6134 // If the user is a remote mnet user, parse the email text for URL to the 6135 // wwwroot and modify the url to direct the user's browser to login at their 6136 // home site (identity provider - idp) before hitting the link itself. 6137 if (is_mnet_remote_user($user)) { 6138 require_once($CFG->dirroot.'/mnet/lib.php'); 6139 6140 $jumpurl = mnet_get_idp_jump_url($user); 6141 $callback = partial('mnet_sso_apply_indirection', $jumpurl); 6142 6143 $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%", 6144 $callback, 6145 $messagetext); 6146 $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%", 6147 $callback, 6148 $messagehtml); 6149 } 6150 $mail = get_mailer(); 6151 6152 if (!empty($mail->SMTPDebug)) { 6153 echo '<pre>' . "\n"; 6154 } 6155 6156 $temprecipients = array(); 6157 $tempreplyto = array(); 6158 6159 // Make sure that we fall back onto some reasonable no-reply address. 6160 $noreplyaddressdefault = 'noreply@' . get_host_from_url($CFG->wwwroot); 6161 $noreplyaddress = empty($CFG->noreplyaddress) ? $noreplyaddressdefault : $CFG->noreplyaddress; 6162 6163 if (!validate_email($noreplyaddress)) { 6164 debugging('email_to_user: Invalid noreply-email '.s($noreplyaddress)); 6165 $noreplyaddress = $noreplyaddressdefault; 6166 } 6167 6168 // Make up an email address for handling bounces. 6169 if (!empty($CFG->handlebounces)) { 6170 $modargs = 'B'.base64_encode(pack('V', $user->id)).substr(md5($user->email), 0, 16); 6171 $mail->Sender = generate_email_processing_address(0, $modargs); 6172 } else { 6173 $mail->Sender = $noreplyaddress; 6174 } 6175 6176 // Make sure that the explicit replyto is valid, fall back to the implicit one. 6177 if (!empty($replyto) && !validate_email($replyto)) { 6178 debugging('email_to_user: Invalid replyto-email '.s($replyto)); 6179 $replyto = $noreplyaddress; 6180 } 6181 6182 if (is_string($from)) { // So we can pass whatever we want if there is need. 6183 $mail->From = $noreplyaddress; 6184 $mail->FromName = $from; 6185 // Check if using the true address is true, and the email is in the list of allowed domains for sending email, 6186 // and that the senders email setting is either displayed to everyone, or display to only other users that are enrolled 6187 // in a course with the sender. 6188 } else if ($usetrueaddress && can_send_from_real_email_address($from, $user)) { 6189 if (!validate_email($from->email)) { 6190 debugging('email_to_user: Invalid from-email '.s($from->email).' - not sending'); 6191 // Better not to use $noreplyaddress in this case. 6192 return false; 6193 } 6194 $mail->From = $from->email; 6195 $fromdetails = new stdClass(); 6196 $fromdetails->name = fullname($from); 6197 $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot); 6198 $fromdetails->siteshortname = format_string($SITE->shortname); 6199 $fromstring = $fromdetails->name; 6200 if ($CFG->emailfromvia == EMAIL_VIA_ALWAYS) { 6201 $fromstring = get_string('emailvia', 'core', $fromdetails); 6202 } 6203 $mail->FromName = $fromstring; 6204 if (empty($replyto)) { 6205 $tempreplyto[] = array($from->email, fullname($from)); 6206 } 6207 } else { 6208 $mail->From = $noreplyaddress; 6209 $fromdetails = new stdClass(); 6210 $fromdetails->name = fullname($from); 6211 $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot); 6212 $fromdetails->siteshortname = format_string($SITE->shortname); 6213 $fromstring = $fromdetails->name; 6214 if ($CFG->emailfromvia != EMAIL_VIA_NEVER) { 6215 $fromstring = get_string('emailvia', 'core', $fromdetails); 6216 } 6217 $mail->FromName = $fromstring; 6218 if (empty($replyto)) { 6219 $tempreplyto[] = array($noreplyaddress, get_string('noreplyname')); 6220 } 6221 } 6222 6223 if (!empty($replyto)) { 6224 $tempreplyto[] = array($replyto, $replytoname); 6225 } 6226 6227 $temprecipients[] = array($user->email, fullname($user)); 6228 6229 // Set word wrap. 6230 $mail->WordWrap = $wordwrapwidth; 6231 6232 if (!empty($from->customheaders)) { 6233 // Add custom headers. 6234 if (is_array($from->customheaders)) { 6235 foreach ($from->customheaders as $customheader) { 6236 $mail->addCustomHeader($customheader); 6237 } 6238 } else { 6239 $mail->addCustomHeader($from->customheaders); 6240 } 6241 } 6242 6243 // If the X-PHP-Originating-Script email header is on then also add an additional 6244 // header with details of where exactly in moodle the email was triggered from, 6245 // either a call to message_send() or to email_to_user(). 6246 if (ini_get('mail.add_x_header')) { 6247 6248 $stack = debug_backtrace(false); 6249 $origin = $stack[0]; 6250 6251 foreach ($stack as $depth => $call) { 6252 if ($call['function'] == 'message_send') { 6253 $origin = $call; 6254 } 6255 } 6256 6257 $originheader = $CFG->wwwroot . ' => ' . gethostname() . ':' 6258 . str_replace($CFG->dirroot . '/', '', $origin['file']) . ':' . $origin['line']; 6259 $mail->addCustomHeader('X-Moodle-Originating-Script: ' . $originheader); 6260 } 6261 6262 if (!empty($CFG->emailheaders)) { 6263 $headers = array_map('trim', explode("\n", $CFG->emailheaders)); 6264 foreach ($headers as $header) { 6265 if (!empty($header)) { 6266 $mail->addCustomHeader($header); 6267 } 6268 } 6269 } 6270 6271 if (!empty($from->priority)) { 6272 $mail->Priority = $from->priority; 6273 } 6274 6275 $renderer = $PAGE->get_renderer('core'); 6276 $context = array( 6277 'sitefullname' => $SITE->fullname, 6278 'siteshortname' => $SITE->shortname, 6279 'sitewwwroot' => $CFG->wwwroot, 6280 'subject' => $subject, 6281 'prefix' => $CFG->emailsubjectprefix, 6282 'to' => $user->email, 6283 'toname' => fullname($user), 6284 'from' => $mail->From, 6285 'fromname' => $mail->FromName, 6286 ); 6287 if (!empty($tempreplyto[0])) { 6288 $context['replyto'] = $tempreplyto[0][0]; 6289 $context['replytoname'] = $tempreplyto[0][1]; 6290 } 6291 if ($user->id > 0) { 6292 $context['touserid'] = $user->id; 6293 $context['tousername'] = $user->username; 6294 } 6295 6296 if (!empty($user->mailformat) && $user->mailformat == 1) { 6297 // Only process html templates if the user preferences allow html email. 6298 6299 if (!$messagehtml) { 6300 // If no html has been given, BUT there is an html wrapping template then 6301 // auto convert the text to html and then wrap it. 6302 $messagehtml = trim(text_to_html($messagetext)); 6303 } 6304 $context['body'] = $messagehtml; 6305 $messagehtml = $renderer->render_from_template('core/email_html', $context); 6306 } 6307 6308 $context['body'] = html_to_text(nl2br($messagetext)); 6309 $mail->Subject = $renderer->render_from_template('core/email_subject', $context); 6310 $mail->FromName = $renderer->render_from_template('core/email_fromname', $context); 6311 $messagetext = $renderer->render_from_template('core/email_text', $context); 6312 6313 // Autogenerate a MessageID if it's missing. 6314 if (empty($mail->MessageID)) { 6315 $mail->MessageID = generate_email_messageid(); 6316 } 6317 6318 if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) { 6319 // Don't ever send HTML to users who don't want it. 6320 $mail->isHTML(true); 6321 $mail->Encoding = 'quoted-printable'; 6322 $mail->Body = $messagehtml; 6323 $mail->AltBody = "\n$messagetext\n"; 6324 } else { 6325 $mail->IsHTML(false); 6326 $mail->Body = "\n$messagetext\n"; 6327 } 6328 6329 if ($attachment && $attachname) { 6330 if (preg_match( "~\\.\\.~" , $attachment )) { 6331 // Security check for ".." in dir path. 6332 $supportuser = core_user::get_support_user(); 6333 $temprecipients[] = array($supportuser->email, fullname($supportuser, true)); 6334 $mail->addStringAttachment('Error in attachment. User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain'); 6335 } else { 6336 require_once($CFG->libdir.'/filelib.php'); 6337 $mimetype = mimeinfo('type', $attachname); 6338 6339 // Before doing the comparison, make sure that the paths are correct (Windows uses slashes in the other direction). 6340 // The absolute (real) path is also fetched to ensure that comparisons to allowed paths are compared equally. 6341 $attachpath = str_replace('\\', '/', realpath($attachment)); 6342 6343 // Build an array of all filepaths from which attachments can be added (normalised slashes, absolute/real path). 6344 $allowedpaths = array_map(function(string $path): string { 6345 return str_replace('\\', '/', realpath($path)); 6346 }, [ 6347 $CFG->cachedir, 6348 $CFG->dataroot, 6349 $CFG->dirroot, 6350 $CFG->localcachedir, 6351 $CFG->tempdir, 6352 $CFG->localrequestdir, 6353 ]); 6354 6355 // Set addpath to true. 6356 $addpath = true; 6357 6358 // Check if attachment includes one of the allowed paths. 6359 foreach (array_filter($allowedpaths) as $allowedpath) { 6360 // Set addpath to false if the attachment includes one of the allowed paths. 6361 if (strpos($attachpath, $allowedpath) === 0) { 6362 $addpath = false; 6363 break; 6364 } 6365 } 6366 6367 // If the attachment is a full path to a file in the multiple allowed paths, use it as is, 6368 // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons). 6369 if ($addpath == true) { 6370 $attachment = $CFG->dataroot . '/' . $attachment; 6371 } 6372 6373 $mail->addAttachment($attachment, $attachname, 'base64', $mimetype); 6374 } 6375 } 6376 6377 // Check if the email should be sent in an other charset then the default UTF-8. 6378 if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) { 6379 6380 // Use the defined site mail charset or eventually the one preferred by the recipient. 6381 $charset = $CFG->sitemailcharset; 6382 if (!empty($CFG->allowusermailcharset)) { 6383 if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) { 6384 $charset = $useremailcharset; 6385 } 6386 } 6387 6388 // Convert all the necessary strings if the charset is supported. 6389 $charsets = get_list_of_charsets(); 6390 unset($charsets['UTF-8']); 6391 if (in_array($charset, $charsets)) { 6392 $mail->CharSet = $charset; 6393 $mail->FromName = core_text::convert($mail->FromName, 'utf-8', strtolower($charset)); 6394 $mail->Subject = core_text::convert($mail->Subject, 'utf-8', strtolower($charset)); 6395 $mail->Body = core_text::convert($mail->Body, 'utf-8', strtolower($charset)); 6396 $mail->AltBody = core_text::convert($mail->AltBody, 'utf-8', strtolower($charset)); 6397 6398 foreach ($temprecipients as $key => $values) { 6399 $temprecipients[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset)); 6400 } 6401 foreach ($tempreplyto as $key => $values) { 6402 $tempreplyto[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset)); 6403 } 6404 } 6405 } 6406 6407 foreach ($temprecipients as $values) { 6408 $mail->addAddress($values[0], $values[1]); 6409 } 6410 foreach ($tempreplyto as $values) { 6411 $mail->addReplyTo($values[0], $values[1]); 6412 } 6413 6414 if (!empty($CFG->emaildkimselector)) { 6415 $domain = substr(strrchr($mail->From, "@"), 1); 6416 $pempath = "{$CFG->dataroot}/dkim/{$domain}/{$CFG->emaildkimselector}.private"; 6417 if (file_exists($pempath)) { 6418 $mail->DKIM_domain = $domain; 6419 $mail->DKIM_private = $pempath; 6420 $mail->DKIM_selector = $CFG->emaildkimselector; 6421 $mail->DKIM_identity = $mail->From; 6422 } else { 6423 debugging("Email DKIM selector chosen due to {$mail->From} but no certificate found at $pempath", DEBUG_DEVELOPER); 6424 } 6425 } 6426 6427 if ($mail->send()) { 6428 set_send_count($user); 6429 if (!empty($mail->SMTPDebug)) { 6430 echo '</pre>'; 6431 } 6432 return true; 6433 } else { 6434 // Trigger event for failing to send email. 6435 $event = \core\event\email_failed::create(array( 6436 'context' => context_system::instance(), 6437 'userid' => $from->id, 6438 'relateduserid' => $user->id, 6439 'other' => array( 6440 'subject' => $subject, 6441 'message' => $messagetext, 6442 'errorinfo' => $mail->ErrorInfo 6443 ) 6444 )); 6445 $event->trigger(); 6446 if (CLI_SCRIPT) { 6447 mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo); 6448 } 6449 if (!empty($mail->SMTPDebug)) { 6450 echo '</pre>'; 6451 } 6452 return false; 6453 } 6454 } 6455 6456 /** 6457 * Check to see if a user's real email address should be used for the "From" field. 6458 * 6459 * @param object $from The user object for the user we are sending the email from. 6460 * @param object $user The user object that we are sending the email to. 6461 * @param array $unused No longer used. 6462 * @return bool Returns true if we can use the from user's email adress in the "From" field. 6463 */ 6464 function can_send_from_real_email_address($from, $user, $unused = null) { 6465 global $CFG; 6466 if (!isset($CFG->allowedemaildomains) || empty(trim($CFG->allowedemaildomains))) { 6467 return false; 6468 } 6469 $alloweddomains = array_map('trim', explode("\n", $CFG->allowedemaildomains)); 6470 // Email is in the list of allowed domains for sending email, 6471 // and the senders email setting is either displayed to everyone, or display to only other users that are enrolled 6472 // in a course with the sender. 6473 if (\core\ip_utils::is_domain_in_allowed_list(substr($from->email, strpos($from->email, '@') + 1), $alloweddomains) 6474 && ($from->maildisplay == core_user::MAILDISPLAY_EVERYONE 6475 || ($from->maildisplay == core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY 6476 && enrol_get_shared_courses($user, $from, false, true)))) { 6477 return true; 6478 } 6479 return false; 6480 } 6481 6482 /** 6483 * Generate a signoff for emails based on support settings 6484 * 6485 * @return string 6486 */ 6487 function generate_email_signoff() { 6488 global $CFG, $OUTPUT; 6489 6490 $signoff = "\n"; 6491 if (!empty($CFG->supportname)) { 6492 $signoff .= $CFG->supportname."\n"; 6493 } 6494 6495 $supportemail = $OUTPUT->supportemail(['class' => 'font-weight-bold']); 6496 6497 if ($supportemail) { 6498 $signoff .= "\n" . $supportemail . "\n"; 6499 } 6500 6501 return $signoff; 6502 } 6503 6504 /** 6505 * Sets specified user's password and send the new password to the user via email. 6506 * 6507 * @param stdClass $user A {@link $USER} object 6508 * @param bool $fasthash If true, use a low cost factor when generating the hash for speed. 6509 * @return bool|string Returns "true" if mail was sent OK and "false" if there was an error 6510 */ 6511 function setnew_password_and_mail($user, $fasthash = false) { 6512 global $CFG, $DB; 6513 6514 // We try to send the mail in language the user understands, 6515 // unfortunately the filter_string() does not support alternative langs yet 6516 // so multilang will not work properly for site->fullname. 6517 $lang = empty($user->lang) ? get_newuser_language() : $user->lang; 6518 6519 $site = get_site(); 6520 6521 $supportuser = core_user::get_support_user(); 6522 6523 $newpassword = generate_password(); 6524 6525 update_internal_user_password($user, $newpassword, $fasthash); 6526 6527 $a = new stdClass(); 6528 $a->firstname = fullname($user, true); 6529 $a->sitename = format_string($site->fullname); 6530 $a->username = $user->username; 6531 $a->newpassword = $newpassword; 6532 $a->link = $CFG->wwwroot .'/login/?lang='.$lang; 6533 $a->signoff = generate_email_signoff(); 6534 6535 $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang); 6536 6537 $subject = format_string($site->fullname) .': '. (string)new lang_string('newusernewpasswordsubj', '', $a, $lang); 6538 6539 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 6540 return email_to_user($user, $supportuser, $subject, $message); 6541 6542 } 6543 6544 /** 6545 * Resets specified user's password and send the new password to the user via email. 6546 * 6547 * @param stdClass $user A {@link $USER} object 6548 * @return bool Returns true if mail was sent OK and false if there was an error. 6549 */ 6550 function reset_password_and_mail($user) { 6551 global $CFG; 6552 6553 $site = get_site(); 6554 $supportuser = core_user::get_support_user(); 6555 6556 $userauth = get_auth_plugin($user->auth); 6557 if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) { 6558 trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth."); 6559 return false; 6560 } 6561 6562 $newpassword = generate_password(); 6563 6564 if (!$userauth->user_update_password($user, $newpassword)) { 6565 throw new \moodle_exception("cannotsetpassword"); 6566 } 6567 6568 $a = new stdClass(); 6569 $a->firstname = $user->firstname; 6570 $a->lastname = $user->lastname; 6571 $a->sitename = format_string($site->fullname); 6572 $a->username = $user->username; 6573 $a->newpassword = $newpassword; 6574 $a->link = $CFG->wwwroot .'/login/change_password.php'; 6575 $a->signoff = generate_email_signoff(); 6576 6577 $message = get_string('newpasswordtext', '', $a); 6578 6579 $subject = format_string($site->fullname) .': '. get_string('changedpassword'); 6580 6581 unset_user_preference('create_password', $user); // Prevent cron from generating the password. 6582 6583 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 6584 return email_to_user($user, $supportuser, $subject, $message); 6585 } 6586 6587 /** 6588 * Send email to specified user with confirmation text and activation link. 6589 * 6590 * @param stdClass $user A {@link $USER} object 6591 * @param string $confirmationurl user confirmation URL 6592 * @return bool Returns true if mail was sent OK and false if there was an error. 6593 */ 6594 function send_confirmation_email($user, $confirmationurl = null) { 6595 global $CFG; 6596 6597 $site = get_site(); 6598 $supportuser = core_user::get_support_user(); 6599 6600 $data = new stdClass(); 6601 $data->sitename = format_string($site->fullname); 6602 $data->admin = generate_email_signoff(); 6603 6604 $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname)); 6605 6606 if (empty($confirmationurl)) { 6607 $confirmationurl = '/login/confirm.php'; 6608 } 6609 6610 $confirmationurl = new moodle_url($confirmationurl); 6611 // Remove data parameter just in case it was included in the confirmation so we can add it manually later. 6612 $confirmationurl->remove_params('data'); 6613 $confirmationpath = $confirmationurl->out(false); 6614 6615 // We need to custom encode the username to include trailing dots in the link. 6616 // Because of this custom encoding we can't use moodle_url directly. 6617 // Determine if a query string is present in the confirmation url. 6618 $hasquerystring = strpos($confirmationpath, '?') !== false; 6619 // Perform normal url encoding of the username first. 6620 $username = urlencode($user->username); 6621 // Prevent problems with trailing dots not being included as part of link in some mail clients. 6622 $username = str_replace('.', '%2E', $username); 6623 6624 $data->link = $confirmationpath . ( $hasquerystring ? '&' : '?') . 'data='. $user->secret .'/'. $username; 6625 6626 $message = get_string('emailconfirmation', '', $data); 6627 $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true); 6628 6629 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 6630 return email_to_user($user, $supportuser, $subject, $message, $messagehtml); 6631 } 6632 6633 /** 6634 * Sends a password change confirmation email. 6635 * 6636 * @param stdClass $user A {@link $USER} object 6637 * @param stdClass $resetrecord An object tracking metadata regarding password reset request 6638 * @return bool Returns true if mail was sent OK and false if there was an error. 6639 */ 6640 function send_password_change_confirmation_email($user, $resetrecord) { 6641 global $CFG; 6642 6643 $site = get_site(); 6644 $supportuser = core_user::get_support_user(); 6645 $pwresetmins = isset($CFG->pwresettime) ? floor($CFG->pwresettime / MINSECS) : 30; 6646 6647 $data = new stdClass(); 6648 $data->firstname = $user->firstname; 6649 $data->lastname = $user->lastname; 6650 $data->username = $user->username; 6651 $data->sitename = format_string($site->fullname); 6652 $data->link = $CFG->wwwroot .'/login/forgot_password.php?token='. $resetrecord->token; 6653 $data->admin = generate_email_signoff(); 6654 $data->resetminutes = $pwresetmins; 6655 6656 $message = get_string('emailresetconfirmation', '', $data); 6657 $subject = get_string('emailresetconfirmationsubject', '', format_string($site->fullname)); 6658 6659 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 6660 return email_to_user($user, $supportuser, $subject, $message); 6661 6662 } 6663 6664 /** 6665 * Sends an email containing information on how to change your password. 6666 * 6667 * @param stdClass $user A {@link $USER} object 6668 * @return bool Returns true if mail was sent OK and false if there was an error. 6669 */ 6670 function send_password_change_info($user) { 6671 $site = get_site(); 6672 $supportuser = core_user::get_support_user(); 6673 6674 $data = new stdClass(); 6675 $data->firstname = $user->firstname; 6676 $data->lastname = $user->lastname; 6677 $data->username = $user->username; 6678 $data->sitename = format_string($site->fullname); 6679 $data->admin = generate_email_signoff(); 6680 6681 if (!is_enabled_auth($user->auth)) { 6682 $message = get_string('emailpasswordchangeinfodisabled', '', $data); 6683 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname)); 6684 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 6685 return email_to_user($user, $supportuser, $subject, $message); 6686 } 6687 6688 $userauth = get_auth_plugin($user->auth); 6689 ['subject' => $subject, 'message' => $message] = $userauth->get_password_change_info($user); 6690 6691 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 6692 return email_to_user($user, $supportuser, $subject, $message); 6693 } 6694 6695 /** 6696 * Check that an email is allowed. It returns an error message if there was a problem. 6697 * 6698 * @param string $email Content of email 6699 * @return string|false 6700 */ 6701 function email_is_not_allowed($email) { 6702 global $CFG; 6703 6704 // Comparing lowercase domains. 6705 $email = strtolower($email); 6706 if (!empty($CFG->allowemailaddresses)) { 6707 $allowed = explode(' ', strtolower($CFG->allowemailaddresses)); 6708 foreach ($allowed as $allowedpattern) { 6709 $allowedpattern = trim($allowedpattern); 6710 if (!$allowedpattern) { 6711 continue; 6712 } 6713 if (strpos($allowedpattern, '.') === 0) { 6714 if (strpos(strrev($email), strrev($allowedpattern)) === 0) { 6715 // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com". 6716 return false; 6717 } 6718 6719 } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) { 6720 return false; 6721 } 6722 } 6723 return get_string('emailonlyallowed', '', $CFG->allowemailaddresses); 6724 6725 } else if (!empty($CFG->denyemailaddresses)) { 6726 $denied = explode(' ', strtolower($CFG->denyemailaddresses)); 6727 foreach ($denied as $deniedpattern) { 6728 $deniedpattern = trim($deniedpattern); 6729 if (!$deniedpattern) { 6730 continue; 6731 } 6732 if (strpos($deniedpattern, '.') === 0) { 6733 if (strpos(strrev($email), strrev($deniedpattern)) === 0) { 6734 // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com". 6735 return get_string('emailnotallowed', '', $CFG->denyemailaddresses); 6736 } 6737 6738 } else if (strpos(strrev($email), strrev('@'.$deniedpattern)) === 0) { 6739 return get_string('emailnotallowed', '', $CFG->denyemailaddresses); 6740 } 6741 } 6742 } 6743 6744 return false; 6745 } 6746 6747 // FILE HANDLING. 6748 6749 /** 6750 * Returns local file storage instance 6751 * 6752 * @return file_storage 6753 */ 6754 function get_file_storage($reset = false) { 6755 global $CFG; 6756 6757 static $fs = null; 6758 6759 if ($reset) { 6760 $fs = null; 6761 return; 6762 } 6763 6764 if ($fs) { 6765 return $fs; 6766 } 6767 6768 require_once("$CFG->libdir/filelib.php"); 6769 6770 $fs = new file_storage(); 6771 6772 return $fs; 6773 } 6774 6775 /** 6776 * Returns local file storage instance 6777 * 6778 * @return file_browser 6779 */ 6780 function get_file_browser() { 6781 global $CFG; 6782 6783 static $fb = null; 6784 6785 if ($fb) { 6786 return $fb; 6787 } 6788 6789 require_once("$CFG->libdir/filelib.php"); 6790 6791 $fb = new file_browser(); 6792 6793 return $fb; 6794 } 6795 6796 /** 6797 * Returns file packer 6798 * 6799 * @param string $mimetype default application/zip 6800 * @return file_packer 6801 */ 6802 function get_file_packer($mimetype='application/zip') { 6803 global $CFG; 6804 6805 static $fp = array(); 6806 6807 if (isset($fp[$mimetype])) { 6808 return $fp[$mimetype]; 6809 } 6810 6811 switch ($mimetype) { 6812 case 'application/zip': 6813 case 'application/vnd.moodle.profiling': 6814 $classname = 'zip_packer'; 6815 break; 6816 6817 case 'application/x-gzip' : 6818 $classname = 'tgz_packer'; 6819 break; 6820 6821 case 'application/vnd.moodle.backup': 6822 $classname = 'mbz_packer'; 6823 break; 6824 6825 default: 6826 return false; 6827 } 6828 6829 require_once("$CFG->libdir/filestorage/$classname.php"); 6830 $fp[$mimetype] = new $classname(); 6831 6832 return $fp[$mimetype]; 6833 } 6834 6835 /** 6836 * Returns current name of file on disk if it exists. 6837 * 6838 * @param string $newfile File to be verified 6839 * @return string Current name of file on disk if true 6840 */ 6841 function valid_uploaded_file($newfile) { 6842 if (empty($newfile)) { 6843 return ''; 6844 } 6845 if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) { 6846 return $newfile['tmp_name']; 6847 } else { 6848 return ''; 6849 } 6850 } 6851 6852 /** 6853 * Returns the maximum size for uploading files. 6854 * 6855 * There are seven possible upload limits: 6856 * 1. in Apache using LimitRequestBody (no way of checking or changing this) 6857 * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP) 6858 * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP) 6859 * 4. in php.ini for 'post_max_size' (can not be changed inside PHP) 6860 * 5. by the Moodle admin in $CFG->maxbytes 6861 * 6. by the teacher in the current course $course->maxbytes 6862 * 7. by the teacher for the current module, eg $assignment->maxbytes 6863 * 6864 * These last two are passed to this function as arguments (in bytes). 6865 * Anything defined as 0 is ignored. 6866 * The smallest of all the non-zero numbers is returned. 6867 * 6868 * @todo Finish documenting this function 6869 * 6870 * @param int $sitebytes Set maximum size 6871 * @param int $coursebytes Current course $course->maxbytes (in bytes) 6872 * @param int $modulebytes Current module ->maxbytes (in bytes) 6873 * @param bool $unused This parameter has been deprecated and is not used any more. 6874 * @return int The maximum size for uploading files. 6875 */ 6876 function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0, $unused = false) { 6877 6878 if (! $filesize = ini_get('upload_max_filesize')) { 6879 $filesize = '5M'; 6880 } 6881 $minimumsize = get_real_size($filesize); 6882 6883 if ($postsize = ini_get('post_max_size')) { 6884 $postsize = get_real_size($postsize); 6885 if ($postsize < $minimumsize) { 6886 $minimumsize = $postsize; 6887 } 6888 } 6889 6890 if (($sitebytes > 0) and ($sitebytes < $minimumsize)) { 6891 $minimumsize = $sitebytes; 6892 } 6893 6894 if (($coursebytes > 0) and ($coursebytes < $minimumsize)) { 6895 $minimumsize = $coursebytes; 6896 } 6897 6898 if (($modulebytes > 0) and ($modulebytes < $minimumsize)) { 6899 $minimumsize = $modulebytes; 6900 } 6901 6902 return $minimumsize; 6903 } 6904 6905 /** 6906 * Returns the maximum size for uploading files for the current user 6907 * 6908 * This function takes in account {@link get_max_upload_file_size()} the user's capabilities 6909 * 6910 * @param context $context The context in which to check user capabilities 6911 * @param int $sitebytes Set maximum size 6912 * @param int $coursebytes Current course $course->maxbytes (in bytes) 6913 * @param int $modulebytes Current module ->maxbytes (in bytes) 6914 * @param stdClass $user The user 6915 * @param bool $unused This parameter has been deprecated and is not used any more. 6916 * @return int The maximum size for uploading files. 6917 */ 6918 function get_user_max_upload_file_size($context, $sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $user = null, 6919 $unused = false) { 6920 global $USER; 6921 6922 if (empty($user)) { 6923 $user = $USER; 6924 } 6925 6926 if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) { 6927 return USER_CAN_IGNORE_FILE_SIZE_LIMITS; 6928 } 6929 6930 return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes); 6931 } 6932 6933 /** 6934 * Returns an array of possible sizes in local language 6935 * 6936 * Related to {@link get_max_upload_file_size()} - this function returns an 6937 * array of possible sizes in an array, translated to the 6938 * local language. 6939 * 6940 * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes. 6941 * 6942 * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)" 6943 * with the value set to 0. This option will be the first in the list. 6944 * 6945 * @uses SORT_NUMERIC 6946 * @param int $sitebytes Set maximum size 6947 * @param int $coursebytes Current course $course->maxbytes (in bytes) 6948 * @param int $modulebytes Current module ->maxbytes (in bytes) 6949 * @param int|array $custombytes custom upload size/s which will be added to list, 6950 * Only value/s smaller then maxsize will be added to list. 6951 * @return array 6952 */ 6953 function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null) { 6954 global $CFG; 6955 6956 if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) { 6957 return array(); 6958 } 6959 6960 if ($sitebytes == 0) { 6961 // Will get the minimum of upload_max_filesize or post_max_size. 6962 $sitebytes = get_max_upload_file_size(); 6963 } 6964 6965 $filesize = array(); 6966 $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152, 6967 5242880, 10485760, 20971520, 52428800, 104857600, 6968 262144000, 524288000, 786432000, 1073741824, 6969 2147483648, 4294967296, 8589934592); 6970 6971 // If custombytes is given and is valid then add it to the list. 6972 if (is_number($custombytes) and $custombytes > 0) { 6973 $custombytes = (int)$custombytes; 6974 if (!in_array($custombytes, $sizelist)) { 6975 $sizelist[] = $custombytes; 6976 } 6977 } else if (is_array($custombytes)) { 6978 $sizelist = array_unique(array_merge($sizelist, $custombytes)); 6979 } 6980 6981 // Allow maxbytes to be selected if it falls outside the above boundaries. 6982 if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) { 6983 // Note: get_real_size() is used in order to prevent problems with invalid values. 6984 $sizelist[] = get_real_size($CFG->maxbytes); 6985 } 6986 6987 foreach ($sizelist as $sizebytes) { 6988 if ($sizebytes < $maxsize && $sizebytes > 0) { 6989 $filesize[(string)intval($sizebytes)] = display_size($sizebytes, 0); 6990 } 6991 } 6992 6993 $limitlevel = ''; 6994 $displaysize = ''; 6995 if ($modulebytes && 6996 (($modulebytes < $coursebytes || $coursebytes == 0) && 6997 ($modulebytes < $sitebytes || $sitebytes == 0))) { 6998 $limitlevel = get_string('activity', 'core'); 6999 $displaysize = display_size($modulebytes, 0); 7000 $filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list. 7001 7002 } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) { 7003 $limitlevel = get_string('course', 'core'); 7004 $displaysize = display_size($coursebytes, 0); 7005 $filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list. 7006 7007 } else if ($sitebytes) { 7008 $limitlevel = get_string('site', 'core'); 7009 $displaysize = display_size($sitebytes, 0); 7010 $filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list. 7011 } 7012 7013 krsort($filesize, SORT_NUMERIC); 7014 if ($limitlevel) { 7015 $params = (object) array('contextname' => $limitlevel, 'displaysize' => $displaysize); 7016 $filesize = array('0' => get_string('uploadlimitwithsize', 'core', $params)) + $filesize; 7017 } 7018 7019 return $filesize; 7020 } 7021 7022 /** 7023 * Returns an array with all the filenames in all subdirectories, relative to the given rootdir. 7024 * 7025 * If excludefiles is defined, then that file/directory is ignored 7026 * If getdirs is true, then (sub)directories are included in the output 7027 * If getfiles is true, then files are included in the output 7028 * (at least one of these must be true!) 7029 * 7030 * @todo Finish documenting this function. Add examples of $excludefile usage. 7031 * 7032 * @param string $rootdir A given root directory to start from 7033 * @param string|array $excludefiles If defined then the specified file/directory is ignored 7034 * @param bool $descend If true then subdirectories are recursed as well 7035 * @param bool $getdirs If true then (sub)directories are included in the output 7036 * @param bool $getfiles If true then files are included in the output 7037 * @return array An array with all the filenames in all subdirectories, relative to the given rootdir 7038 */ 7039 function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) { 7040 7041 $dirs = array(); 7042 7043 if (!$getdirs and !$getfiles) { // Nothing to show. 7044 return $dirs; 7045 } 7046 7047 if (!is_dir($rootdir)) { // Must be a directory. 7048 return $dirs; 7049 } 7050 7051 if (!$dir = opendir($rootdir)) { // Can't open it for some reason. 7052 return $dirs; 7053 } 7054 7055 if (!is_array($excludefiles)) { 7056 $excludefiles = array($excludefiles); 7057 } 7058 7059 while (false !== ($file = readdir($dir))) { 7060 $firstchar = substr($file, 0, 1); 7061 if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) { 7062 continue; 7063 } 7064 $fullfile = $rootdir .'/'. $file; 7065 if (filetype($fullfile) == 'dir') { 7066 if ($getdirs) { 7067 $dirs[] = $file; 7068 } 7069 if ($descend) { 7070 $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles); 7071 foreach ($subdirs as $subdir) { 7072 $dirs[] = $file .'/'. $subdir; 7073 } 7074 } 7075 } else if ($getfiles) { 7076 $dirs[] = $file; 7077 } 7078 } 7079 closedir($dir); 7080 7081 asort($dirs); 7082 7083 return $dirs; 7084 } 7085 7086 7087 /** 7088 * Adds up all the files in a directory and works out the size. 7089 * 7090 * @param string $rootdir The directory to start from 7091 * @param string $excludefile A file to exclude when summing directory size 7092 * @return int The summed size of all files and subfiles within the root directory 7093 */ 7094 function get_directory_size($rootdir, $excludefile='') { 7095 global $CFG; 7096 7097 // Do it this way if we can, it's much faster. 7098 if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) { 7099 $command = trim($CFG->pathtodu).' -sk '.escapeshellarg($rootdir); 7100 $output = null; 7101 $return = null; 7102 exec($command, $output, $return); 7103 if (is_array($output)) { 7104 // We told it to return k. 7105 return get_real_size(intval($output[0]).'k'); 7106 } 7107 } 7108 7109 if (!is_dir($rootdir)) { 7110 // Must be a directory. 7111 return 0; 7112 } 7113 7114 if (!$dir = @opendir($rootdir)) { 7115 // Can't open it for some reason. 7116 return 0; 7117 } 7118 7119 $size = 0; 7120 7121 while (false !== ($file = readdir($dir))) { 7122 $firstchar = substr($file, 0, 1); 7123 if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) { 7124 continue; 7125 } 7126 $fullfile = $rootdir .'/'. $file; 7127 if (filetype($fullfile) == 'dir') { 7128 $size += get_directory_size($fullfile, $excludefile); 7129 } else { 7130 $size += filesize($fullfile); 7131 } 7132 } 7133 closedir($dir); 7134 7135 return $size; 7136 } 7137 7138 /** 7139 * Converts bytes into display form 7140 * 7141 * @param int $size The size to convert to human readable form 7142 * @param int $decimalplaces If specified, uses fixed number of decimal places 7143 * @param string $fixedunits If specified, uses fixed units (e.g. 'KB') 7144 * @return string Display version of size 7145 */ 7146 function display_size($size, int $decimalplaces = 1, string $fixedunits = ''): string { 7147 7148 static $units; 7149 7150 if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) { 7151 return get_string('unlimited'); 7152 } 7153 7154 if (empty($units)) { 7155 $units[] = get_string('sizeb'); 7156 $units[] = get_string('sizekb'); 7157 $units[] = get_string('sizemb'); 7158 $units[] = get_string('sizegb'); 7159 $units[] = get_string('sizetb'); 7160 $units[] = get_string('sizepb'); 7161 } 7162 7163 switch ($fixedunits) { 7164 case 'PB' : 7165 $magnitude = 5; 7166 break; 7167 case 'TB' : 7168 $magnitude = 4; 7169 break; 7170 case 'GB' : 7171 $magnitude = 3; 7172 break; 7173 case 'MB' : 7174 $magnitude = 2; 7175 break; 7176 case 'KB' : 7177 $magnitude = 1; 7178 break; 7179 case 'B' : 7180 $magnitude = 0; 7181 break; 7182 case '': 7183 $magnitude = floor(log($size, 1024)); 7184 $magnitude = max(0, min(5, $magnitude)); 7185 break; 7186 default: 7187 throw new coding_exception('Unknown fixed units value: ' . $fixedunits); 7188 } 7189 7190 // Special case for magnitude 0 (bytes) - never use decimal places. 7191 $nbsp = "\xc2\xa0"; 7192 if ($magnitude === 0) { 7193 return round($size) . $nbsp . $units[$magnitude]; 7194 } 7195 7196 // Convert to specified units. 7197 $sizeinunit = $size / 1024 ** $magnitude; 7198 7199 // Fixed decimal places. 7200 return sprintf('%.' . $decimalplaces . 'f', $sizeinunit) . $nbsp . $units[$magnitude]; 7201 } 7202 7203 /** 7204 * Cleans a given filename by removing suspicious or troublesome characters 7205 * 7206 * @see clean_param() 7207 * @param string $string file name 7208 * @return string cleaned file name 7209 */ 7210 function clean_filename($string) { 7211 return clean_param($string, PARAM_FILE); 7212 } 7213 7214 // STRING TRANSLATION. 7215 7216 /** 7217 * Returns the code for the current language 7218 * 7219 * @category string 7220 * @return string 7221 */ 7222 function current_language() { 7223 global $CFG, $PAGE, $SESSION, $USER; 7224 7225 if (!empty($SESSION->forcelang)) { 7226 // Allows overriding course-forced language (useful for admins to check 7227 // issues in courses whose language they don't understand). 7228 // Also used by some code to temporarily get language-related information in a 7229 // specific language (see force_current_language()). 7230 $return = $SESSION->forcelang; 7231 7232 } else if (!empty($PAGE->cm->lang)) { 7233 // Activity language, if set. 7234 $return = $PAGE->cm->lang; 7235 7236 } else if (!empty($PAGE->course->id) && $PAGE->course->id != SITEID && !empty($PAGE->course->lang)) { 7237 // Course language can override all other settings for this page. 7238 $return = $PAGE->course->lang; 7239 7240 } else if (!empty($SESSION->lang)) { 7241 // Session language can override other settings. 7242 $return = $SESSION->lang; 7243 7244 } else if (!empty($USER->lang)) { 7245 $return = $USER->lang; 7246 7247 } else if (isset($CFG->lang)) { 7248 $return = $CFG->lang; 7249 7250 } else { 7251 $return = 'en'; 7252 } 7253 7254 // Just in case this slipped in from somewhere by accident. 7255 $return = str_replace('_utf8', '', $return); 7256 7257 return $return; 7258 } 7259 7260 /** 7261 * Fix the current language to the given language code. 7262 * 7263 * @param string $lang The language code to use. 7264 * @return void 7265 */ 7266 function fix_current_language(string $lang): void { 7267 global $CFG, $COURSE, $SESSION, $USER; 7268 7269 if (!get_string_manager()->translation_exists($lang)) { 7270 throw new coding_exception("The language pack for $lang is not available"); 7271 } 7272 7273 $fixglobal = ''; 7274 $fixlang = 'lang'; 7275 if (!empty($SESSION->forcelang)) { 7276 $fixglobal = $SESSION; 7277 $fixlang = 'forcelang'; 7278 } else if (!empty($COURSE->id) && $COURSE->id != SITEID && !empty($COURSE->lang)) { 7279 $fixglobal = $COURSE; 7280 } else if (!empty($SESSION->lang)) { 7281 $fixglobal = $SESSION; 7282 } else if (!empty($USER->lang)) { 7283 $fixglobal = $USER; 7284 } else if (isset($CFG->lang)) { 7285 set_config('lang', $lang); 7286 } 7287 7288 if ($fixglobal) { 7289 $fixglobal->$fixlang = $lang; 7290 } 7291 } 7292 7293 /** 7294 * Returns parent language of current active language if defined 7295 * 7296 * @category string 7297 * @param string $lang null means current language 7298 * @return string 7299 */ 7300 function get_parent_language($lang=null) { 7301 7302 $parentlang = get_string_manager()->get_string('parentlanguage', 'langconfig', null, $lang); 7303 7304 if ($parentlang === 'en') { 7305 $parentlang = ''; 7306 } 7307 7308 return $parentlang; 7309 } 7310 7311 /** 7312 * Force the current language to get strings and dates localised in the given language. 7313 * 7314 * After calling this function, all strings will be provided in the given language 7315 * until this function is called again, or equivalent code is run. 7316 * 7317 * @param string $language 7318 * @return string previous $SESSION->forcelang value 7319 */ 7320 function force_current_language($language) { 7321 global $SESSION; 7322 $sessionforcelang = isset($SESSION->forcelang) ? $SESSION->forcelang : ''; 7323 if ($language !== $sessionforcelang) { 7324 // Setting forcelang to null or an empty string disables its effect. 7325 if (empty($language) || get_string_manager()->translation_exists($language, false)) { 7326 $SESSION->forcelang = $language; 7327 moodle_setlocale(); 7328 } 7329 } 7330 return $sessionforcelang; 7331 } 7332 7333 /** 7334 * Returns current string_manager instance. 7335 * 7336 * The param $forcereload is needed for CLI installer only where the string_manager instance 7337 * must be replaced during the install.php script life time. 7338 * 7339 * @category string 7340 * @param bool $forcereload shall the singleton be released and new instance created instead? 7341 * @return core_string_manager 7342 */ 7343 function get_string_manager($forcereload=false) { 7344 global $CFG; 7345 7346 static $singleton = null; 7347 7348 if ($forcereload) { 7349 $singleton = null; 7350 } 7351 if ($singleton === null) { 7352 if (empty($CFG->early_install_lang)) { 7353 7354 $transaliases = array(); 7355 if (empty($CFG->langlist)) { 7356 $translist = array(); 7357 } else { 7358 $translist = explode(',', $CFG->langlist); 7359 $translist = array_map('trim', $translist); 7360 // Each language in the $CFG->langlist can has an "alias" that would substitute the default language name. 7361 foreach ($translist as $i => $value) { 7362 $parts = preg_split('/\s*\|\s*/', $value, 2); 7363 if (count($parts) == 2) { 7364 $transaliases[$parts[0]] = $parts[1]; 7365 $translist[$i] = $parts[0]; 7366 } 7367 } 7368 } 7369 7370 if (!empty($CFG->config_php_settings['customstringmanager'])) { 7371 $classname = $CFG->config_php_settings['customstringmanager']; 7372 7373 if (class_exists($classname)) { 7374 $implements = class_implements($classname); 7375 7376 if (isset($implements['core_string_manager'])) { 7377 $singleton = new $classname($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases); 7378 return $singleton; 7379 7380 } else { 7381 debugging('Unable to instantiate custom string manager: class '.$classname. 7382 ' does not implement the core_string_manager interface.'); 7383 } 7384 7385 } else { 7386 debugging('Unable to instantiate custom string manager: class '.$classname.' can not be found.'); 7387 } 7388 } 7389 7390 $singleton = new core_string_manager_standard($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases); 7391 7392 } else { 7393 $singleton = new core_string_manager_install(); 7394 } 7395 } 7396 7397 return $singleton; 7398 } 7399 7400 /** 7401 * Returns a localized string. 7402 * 7403 * Returns the translated string specified by $identifier as 7404 * for $module. Uses the same format files as STphp. 7405 * $a is an object, string or number that can be used 7406 * within translation strings 7407 * 7408 * eg 'hello {$a->firstname} {$a->lastname}' 7409 * or 'hello {$a}' 7410 * 7411 * If you would like to directly echo the localized string use 7412 * the function {@link print_string()} 7413 * 7414 * Example usage of this function involves finding the string you would 7415 * like a local equivalent of and using its identifier and module information 7416 * to retrieve it.<br/> 7417 * If you open moodle/lang/en/moodle.php and look near line 278 7418 * you will find a string to prompt a user for their word for 'course' 7419 * <code> 7420 * $string['course'] = 'Course'; 7421 * </code> 7422 * So if you want to display the string 'Course' 7423 * in any language that supports it on your site 7424 * you just need to use the identifier 'course' 7425 * <code> 7426 * $mystring = '<strong>'. get_string('course') .'</strong>'; 7427 * or 7428 * </code> 7429 * If the string you want is in another file you'd take a slightly 7430 * different approach. Looking in moodle/lang/en/calendar.php you find 7431 * around line 75: 7432 * <code> 7433 * $string['typecourse'] = 'Course event'; 7434 * </code> 7435 * If you want to display the string "Course event" in any language 7436 * supported you would use the identifier 'typecourse' and the module 'calendar' 7437 * (because it is in the file calendar.php): 7438 * <code> 7439 * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>'; 7440 * </code> 7441 * 7442 * As a last resort, should the identifier fail to map to a string 7443 * the returned string will be [[ $identifier ]] 7444 * 7445 * In Moodle 2.3 there is a new argument to this function $lazyload. 7446 * Setting $lazyload to true causes get_string to return a lang_string object 7447 * rather than the string itself. The fetching of the string is then put off until 7448 * the string object is first used. The object can be used by calling it's out 7449 * method or by casting the object to a string, either directly e.g. 7450 * (string)$stringobject 7451 * or indirectly by using the string within another string or echoing it out e.g. 7452 * echo $stringobject 7453 * return "<p>{$stringobject}</p>"; 7454 * It is worth noting that using $lazyload and attempting to use the string as an 7455 * array key will cause a fatal error as objects cannot be used as array keys. 7456 * But you should never do that anyway! 7457 * For more information {@link lang_string} 7458 * 7459 * @category string 7460 * @param string $identifier The key identifier for the localized string 7461 * @param string $component The module where the key identifier is stored, 7462 * usually expressed as the filename in the language pack without the 7463 * .php on the end but can also be written as mod/forum or grade/export/xls. 7464 * If none is specified then moodle.php is used. 7465 * @param string|object|array|int $a An object, string or number that can be used 7466 * within translation strings 7467 * @param bool $lazyload If set to true a string object is returned instead of 7468 * the string itself. The string then isn't calculated until it is first used. 7469 * @return string The localized string. 7470 * @throws coding_exception 7471 */ 7472 function get_string($identifier, $component = '', $a = null, $lazyload = false) { 7473 global $CFG; 7474 7475 // If the lazy load argument has been supplied return a lang_string object 7476 // instead. 7477 // We need to make sure it is true (and a bool) as you will see below there 7478 // used to be a forth argument at one point. 7479 if ($lazyload === true) { 7480 return new lang_string($identifier, $component, $a); 7481 } 7482 7483 if ($CFG->debugdeveloper && clean_param($identifier, PARAM_STRINGID) === '') { 7484 throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.', DEBUG_DEVELOPER); 7485 } 7486 7487 // There is now a forth argument again, this time it is a boolean however so 7488 // we can still check for the old extralocations parameter. 7489 if (!is_bool($lazyload) && !empty($lazyload)) { 7490 debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.'); 7491 } 7492 7493 if (strpos((string)$component, '/') !== false) { 7494 debugging('The module name you passed to get_string is the deprecated format ' . 7495 'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER); 7496 $componentpath = explode('/', $component); 7497 7498 switch ($componentpath[0]) { 7499 case 'mod': 7500 $component = $componentpath[1]; 7501 break; 7502 case 'blocks': 7503 case 'block': 7504 $component = 'block_'.$componentpath[1]; 7505 break; 7506 case 'enrol': 7507 $component = 'enrol_'.$componentpath[1]; 7508 break; 7509 case 'format': 7510 $component = 'format_'.$componentpath[1]; 7511 break; 7512 case 'grade': 7513 $component = 'grade'.$componentpath[1].'_'.$componentpath[2]; 7514 break; 7515 } 7516 } 7517 7518 $result = get_string_manager()->get_string($identifier, $component, $a); 7519 7520 // Debugging feature lets you display string identifier and component. 7521 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) { 7522 $result .= ' {' . $identifier . '/' . $component . '}'; 7523 } 7524 return $result; 7525 } 7526 7527 /** 7528 * Converts an array of strings to their localized value. 7529 * 7530 * @param array $array An array of strings 7531 * @param string $component The language module that these strings can be found in. 7532 * @return stdClass translated strings. 7533 */ 7534 function get_strings($array, $component = '') { 7535 $string = new stdClass; 7536 foreach ($array as $item) { 7537 $string->$item = get_string($item, $component); 7538 } 7539 return $string; 7540 } 7541 7542 /** 7543 * Prints out a translated string. 7544 * 7545 * Prints out a translated string using the return value from the {@link get_string()} function. 7546 * 7547 * Example usage of this function when the string is in the moodle.php file:<br/> 7548 * <code> 7549 * echo '<strong>'; 7550 * print_string('course'); 7551 * echo '</strong>'; 7552 * </code> 7553 * 7554 * Example usage of this function when the string is not in the moodle.php file:<br/> 7555 * <code> 7556 * echo '<h1>'; 7557 * print_string('typecourse', 'calendar'); 7558 * echo '</h1>'; 7559 * </code> 7560 * 7561 * @category string 7562 * @param string $identifier The key identifier for the localized string 7563 * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used. 7564 * @param string|object|array $a An object, string or number that can be used within translation strings 7565 */ 7566 function print_string($identifier, $component = '', $a = null) { 7567 echo get_string($identifier, $component, $a); 7568 } 7569 7570 /** 7571 * Returns a list of charset codes 7572 * 7573 * Returns a list of charset codes. It's hardcoded, so they should be added manually 7574 * (checking that such charset is supported by the texlib library!) 7575 * 7576 * @return array And associative array with contents in the form of charset => charset 7577 */ 7578 function get_list_of_charsets() { 7579 7580 $charsets = array( 7581 'EUC-JP' => 'EUC-JP', 7582 'ISO-2022-JP'=> 'ISO-2022-JP', 7583 'ISO-8859-1' => 'ISO-8859-1', 7584 'SHIFT-JIS' => 'SHIFT-JIS', 7585 'GB2312' => 'GB2312', 7586 'GB18030' => 'GB18030', // GB18030 not supported by typo and mbstring. 7587 'UTF-8' => 'UTF-8'); 7588 7589 asort($charsets); 7590 7591 return $charsets; 7592 } 7593 7594 /** 7595 * Returns a list of valid and compatible themes 7596 * 7597 * @return array 7598 */ 7599 function get_list_of_themes() { 7600 global $CFG; 7601 7602 $themes = array(); 7603 7604 if (!empty($CFG->themelist)) { // Use admin's list of themes. 7605 $themelist = explode(',', $CFG->themelist); 7606 } else { 7607 $themelist = array_keys(core_component::get_plugin_list("theme")); 7608 } 7609 7610 foreach ($themelist as $key => $themename) { 7611 $theme = theme_config::load($themename); 7612 $themes[$themename] = $theme; 7613 } 7614 7615 core_collator::asort_objects_by_method($themes, 'get_theme_name'); 7616 7617 return $themes; 7618 } 7619 7620 /** 7621 * Factory function for emoticon_manager 7622 * 7623 * @return emoticon_manager singleton 7624 */ 7625 function get_emoticon_manager() { 7626 static $singleton = null; 7627 7628 if (is_null($singleton)) { 7629 $singleton = new emoticon_manager(); 7630 } 7631 7632 return $singleton; 7633 } 7634 7635 /** 7636 * Provides core support for plugins that have to deal with emoticons (like HTML editor or emoticon filter). 7637 * 7638 * Whenever this manager mentiones 'emoticon object', the following data 7639 * structure is expected: stdClass with properties text, imagename, imagecomponent, 7640 * altidentifier and altcomponent 7641 * 7642 * @see admin_setting_emoticons 7643 * 7644 * @copyright 2010 David Mudrak 7645 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 7646 */ 7647 class emoticon_manager { 7648 7649 /** 7650 * Returns the currently enabled emoticons 7651 * 7652 * @param boolean $selectable - If true, only return emoticons that should be selectable from a list. 7653 * @return array of emoticon objects 7654 */ 7655 public function get_emoticons($selectable = false) { 7656 global $CFG; 7657 $notselectable = ['martin', 'egg']; 7658 7659 if (empty($CFG->emoticons)) { 7660 return array(); 7661 } 7662 7663 $emoticons = $this->decode_stored_config($CFG->emoticons); 7664 7665 if (!is_array($emoticons)) { 7666 // Something is wrong with the format of stored setting. 7667 debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL); 7668 return array(); 7669 } 7670 if ($selectable) { 7671 foreach ($emoticons as $index => $emote) { 7672 if (in_array($emote->altidentifier, $notselectable)) { 7673 // Skip this one. 7674 unset($emoticons[$index]); 7675 } 7676 } 7677 } 7678 7679 return $emoticons; 7680 } 7681 7682 /** 7683 * Converts emoticon object into renderable pix_emoticon object 7684 * 7685 * @param stdClass $emoticon emoticon object 7686 * @param array $attributes explicit HTML attributes to set 7687 * @return pix_emoticon 7688 */ 7689 public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array()) { 7690 $stringmanager = get_string_manager(); 7691 if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) { 7692 $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent); 7693 } else { 7694 $alt = s($emoticon->text); 7695 } 7696 return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes); 7697 } 7698 7699 /** 7700 * Encodes the array of emoticon objects into a string storable in config table 7701 * 7702 * @see self::decode_stored_config() 7703 * @param array $emoticons array of emtocion objects 7704 * @return string 7705 */ 7706 public function encode_stored_config(array $emoticons) { 7707 return json_encode($emoticons); 7708 } 7709 7710 /** 7711 * Decodes the string into an array of emoticon objects 7712 * 7713 * @see self::encode_stored_config() 7714 * @param string $encoded 7715 * @return array|null 7716 */ 7717 public function decode_stored_config($encoded) { 7718 $decoded = json_decode($encoded); 7719 if (!is_array($decoded)) { 7720 return null; 7721 } 7722 return $decoded; 7723 } 7724 7725 /** 7726 * Returns default set of emoticons supported by Moodle 7727 * 7728 * @return array of sdtClasses 7729 */ 7730 public function default_emoticons() { 7731 return array( 7732 $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'), 7733 $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'), 7734 $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'), 7735 $this->prepare_emoticon_object(";-)", 's/wink', 'wink'), 7736 $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'), 7737 $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'), 7738 $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'), 7739 $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'), 7740 $this->prepare_emoticon_object("B-)", 's/cool', 'cool'), 7741 $this->prepare_emoticon_object("^-)", 's/approve', 'approve'), 7742 $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'), 7743 $this->prepare_emoticon_object(":o)", 's/clown', 'clown'), 7744 $this->prepare_emoticon_object(":-(", 's/sad', 'sad'), 7745 $this->prepare_emoticon_object(":(", 's/sad', 'sad'), 7746 $this->prepare_emoticon_object("8-.", 's/shy', 'shy'), 7747 $this->prepare_emoticon_object(":-I", 's/blush', 'blush'), 7748 $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'), 7749 $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'), 7750 $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'), 7751 $this->prepare_emoticon_object("8-[", 's/angry', 'angry'), 7752 $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'), 7753 $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'), 7754 $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'), 7755 $this->prepare_emoticon_object("}-]", 's/evil', 'evil'), 7756 $this->prepare_emoticon_object("(h)", 's/heart', 'heart'), 7757 $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'), 7758 $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'), 7759 $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'), 7760 $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'), 7761 $this->prepare_emoticon_object("( )", 's/egg', 'egg'), 7762 ); 7763 } 7764 7765 /** 7766 * Helper method preparing the stdClass with the emoticon properties 7767 * 7768 * @param string|array $text or array of strings 7769 * @param string $imagename to be used by {@link pix_emoticon} 7770 * @param string $altidentifier alternative string identifier, null for no alt 7771 * @param string $altcomponent where the alternative string is defined 7772 * @param string $imagecomponent to be used by {@link pix_emoticon} 7773 * @return stdClass 7774 */ 7775 protected function prepare_emoticon_object($text, $imagename, $altidentifier = null, 7776 $altcomponent = 'core_pix', $imagecomponent = 'core') { 7777 return (object)array( 7778 'text' => $text, 7779 'imagename' => $imagename, 7780 'imagecomponent' => $imagecomponent, 7781 'altidentifier' => $altidentifier, 7782 'altcomponent' => $altcomponent, 7783 ); 7784 } 7785 } 7786 7787 // ENCRYPTION. 7788 7789 /** 7790 * rc4encrypt 7791 * 7792 * @param string $data Data to encrypt. 7793 * @return string The now encrypted data. 7794 */ 7795 function rc4encrypt($data) { 7796 return endecrypt(get_site_identifier(), $data, ''); 7797 } 7798 7799 /** 7800 * rc4decrypt 7801 * 7802 * @param string $data Data to decrypt. 7803 * @return string The now decrypted data. 7804 */ 7805 function rc4decrypt($data) { 7806 return endecrypt(get_site_identifier(), $data, 'de'); 7807 } 7808 7809 /** 7810 * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com] 7811 * 7812 * @todo Finish documenting this function 7813 * 7814 * @param string $pwd The password to use when encrypting or decrypting 7815 * @param string $data The data to be decrypted/encrypted 7816 * @param string $case Either 'de' for decrypt or '' for encrypt 7817 * @return string 7818 */ 7819 function endecrypt ($pwd, $data, $case) { 7820 7821 if ($case == 'de') { 7822 $data = urldecode($data); 7823 } 7824 7825 $key[] = ''; 7826 $box[] = ''; 7827 $pwdlength = strlen($pwd); 7828 7829 for ($i = 0; $i <= 255; $i++) { 7830 $key[$i] = ord(substr($pwd, ($i % $pwdlength), 1)); 7831 $box[$i] = $i; 7832 } 7833 7834 $x = 0; 7835 7836 for ($i = 0; $i <= 255; $i++) { 7837 $x = ($x + $box[$i] + $key[$i]) % 256; 7838 $tempswap = $box[$i]; 7839 $box[$i] = $box[$x]; 7840 $box[$x] = $tempswap; 7841 } 7842 7843 $cipher = ''; 7844 7845 $a = 0; 7846 $j = 0; 7847 7848 for ($i = 0; $i < strlen($data); $i++) { 7849 $a = ($a + 1) % 256; 7850 $j = ($j + $box[$a]) % 256; 7851 $temp = $box[$a]; 7852 $box[$a] = $box[$j]; 7853 $box[$j] = $temp; 7854 $k = $box[(($box[$a] + $box[$j]) % 256)]; 7855 $cipherby = ord(substr($data, $i, 1)) ^ $k; 7856 $cipher .= chr($cipherby); 7857 } 7858 7859 if ($case == 'de') { 7860 $cipher = urldecode(urlencode($cipher)); 7861 } else { 7862 $cipher = urlencode($cipher); 7863 } 7864 7865 return $cipher; 7866 } 7867 7868 // ENVIRONMENT CHECKING. 7869 7870 /** 7871 * This method validates a plug name. It is much faster than calling clean_param. 7872 * 7873 * @param string $name a string that might be a plugin name. 7874 * @return bool if this string is a valid plugin name. 7875 */ 7876 function is_valid_plugin_name($name) { 7877 // This does not work for 'mod', bad luck, use any other type. 7878 return core_component::is_valid_plugin_name('tool', $name); 7879 } 7880 7881 /** 7882 * Get a list of all the plugins of a given type that define a certain API function 7883 * in a certain file. The plugin component names and function names are returned. 7884 * 7885 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'. 7886 * @param string $function the part of the name of the function after the 7887 * frankenstyle prefix. e.g 'hook' if you are looking for functions with 7888 * names like report_courselist_hook. 7889 * @param string $file the name of file within the plugin that defines the 7890 * function. Defaults to lib.php. 7891 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum') 7892 * and the function names as values (e.g. 'report_courselist_hook', 'forum_hook'). 7893 */ 7894 function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php') { 7895 global $CFG; 7896 7897 // We don't include here as all plugin types files would be included. 7898 $plugins = get_plugins_with_function($function, $file, false); 7899 7900 if (empty($plugins[$plugintype])) { 7901 return array(); 7902 } 7903 7904 $allplugins = core_component::get_plugin_list($plugintype); 7905 7906 // Reformat the array and include the files. 7907 $pluginfunctions = array(); 7908 foreach ($plugins[$plugintype] as $pluginname => $functionname) { 7909 7910 // Check that it has not been removed and the file is still available. 7911 if (!empty($allplugins[$pluginname])) { 7912 7913 $filepath = $allplugins[$pluginname] . DIRECTORY_SEPARATOR . $file; 7914 if (file_exists($filepath)) { 7915 include_once($filepath); 7916 7917 // Now that the file is loaded, we must verify the function still exists. 7918 if (function_exists($functionname)) { 7919 $pluginfunctions[$plugintype . '_' . $pluginname] = $functionname; 7920 } else { 7921 // Invalidate the cache for next run. 7922 \cache_helper::invalidate_by_definition('core', 'plugin_functions'); 7923 } 7924 } 7925 } 7926 } 7927 7928 return $pluginfunctions; 7929 } 7930 7931 /** 7932 * Get a list of all the plugins that define a certain API function in a certain file. 7933 * 7934 * @param string $function the part of the name of the function after the 7935 * frankenstyle prefix. e.g 'hook' if you are looking for functions with 7936 * names like report_courselist_hook. 7937 * @param string $file the name of file within the plugin that defines the 7938 * function. Defaults to lib.php. 7939 * @param bool $include Whether to include the files that contain the functions or not. 7940 * @param bool $migratedtohook if true this is a deprecated lib.php callback, if hook callback is present then do nothing 7941 * @return array with [plugintype][plugin] = functionname 7942 */ 7943 function get_plugins_with_function($function, $file = 'lib.php', $include = true, bool $migratedtohook = false) { 7944 global $CFG; 7945 7946 if (during_initial_install() || isset($CFG->upgraderunning)) { 7947 // API functions _must not_ be called during an installation or upgrade. 7948 return []; 7949 } 7950 7951 $plugincallback = $function; 7952 $filtermigrated = function($plugincallback, $pluginfunctions): array { 7953 foreach ($pluginfunctions as $plugintype => $plugins) { 7954 foreach ($plugins as $plugin => $unusedfunction) { 7955 $component = $plugintype . '_' . $plugin; 7956 if (\core\hook\manager::get_instance()->is_deprecated_plugin_callback($plugincallback)) { 7957 if (\core\hook\manager::get_instance()->is_deprecating_hook_present($component, $plugincallback)) { 7958 // Ignore the old callback, it is there only for older Moodle versions. 7959 unset($pluginfunctions[$plugintype][$plugin]); 7960 } else { 7961 debugging("Callback $plugincallback in $component component should be migrated to new hook callback", 7962 DEBUG_DEVELOPER); 7963 } 7964 } 7965 } 7966 } 7967 return $pluginfunctions; 7968 }; 7969 7970 $cache = \cache::make('core', 'plugin_functions'); 7971 7972 // Including both although I doubt that we will find two functions definitions with the same name. 7973 // Clean the filename as cache_helper::hash_key only allows a-zA-Z0-9_. 7974 $pluginfunctions = false; 7975 if (!empty($CFG->allversionshash)) { 7976 $key = $CFG->allversionshash . '_' . $function . '_' . clean_param($file, PARAM_ALPHA); 7977 $pluginfunctions = $cache->get($key); 7978 } 7979 $dirty = false; 7980 7981 // Use the plugin manager to check that plugins are currently installed. 7982 $pluginmanager = \core_plugin_manager::instance(); 7983 7984 if ($pluginfunctions !== false) { 7985 7986 // Checking that the files are still available. 7987 foreach ($pluginfunctions as $plugintype => $plugins) { 7988 7989 $allplugins = \core_component::get_plugin_list($plugintype); 7990 $installedplugins = $pluginmanager->get_installed_plugins($plugintype); 7991 foreach ($plugins as $plugin => $function) { 7992 if (!isset($installedplugins[$plugin])) { 7993 // Plugin code is still present on disk but it is not installed. 7994 $dirty = true; 7995 break 2; 7996 } 7997 7998 // Cache might be out of sync with the codebase, skip the plugin if it is not available. 7999 if (empty($allplugins[$plugin])) { 8000 $dirty = true; 8001 break 2; 8002 } 8003 8004 $fileexists = file_exists($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file); 8005 if ($include && $fileexists) { 8006 // Include the files if it was requested. 8007 include_once($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file); 8008 } else if (!$fileexists) { 8009 // If the file is not available any more it should not be returned. 8010 $dirty = true; 8011 break 2; 8012 } 8013 8014 // Check if the function still exists in the file. 8015 if ($include && !function_exists($function)) { 8016 $dirty = true; 8017 break 2; 8018 } 8019 } 8020 } 8021 8022 // If the cache is dirty, we should fall through and let it rebuild. 8023 if (!$dirty) { 8024 if ($migratedtohook && $file === 'lib.php') { 8025 $pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions); 8026 } 8027 return $pluginfunctions; 8028 } 8029 } 8030 8031 $pluginfunctions = array(); 8032 8033 // To fill the cached. Also, everything should continue working with cache disabled. 8034 $plugintypes = \core_component::get_plugin_types(); 8035 foreach ($plugintypes as $plugintype => $unused) { 8036 8037 // We need to include files here. 8038 $pluginswithfile = \core_component::get_plugin_list_with_file($plugintype, $file, true); 8039 $installedplugins = $pluginmanager->get_installed_plugins($plugintype); 8040 foreach ($pluginswithfile as $plugin => $notused) { 8041 8042 if (!isset($installedplugins[$plugin])) { 8043 continue; 8044 } 8045 8046 $fullfunction = $plugintype . '_' . $plugin . '_' . $function; 8047 8048 $pluginfunction = false; 8049 if (function_exists($fullfunction)) { 8050 // Function exists with standard name. Store, indexed by frankenstyle name of plugin. 8051 $pluginfunction = $fullfunction; 8052 8053 } else if ($plugintype === 'mod') { 8054 // For modules, we also allow plugin without full frankenstyle but just starting with the module name. 8055 $shortfunction = $plugin . '_' . $function; 8056 if (function_exists($shortfunction)) { 8057 $pluginfunction = $shortfunction; 8058 } 8059 } 8060 8061 if ($pluginfunction) { 8062 if (empty($pluginfunctions[$plugintype])) { 8063 $pluginfunctions[$plugintype] = array(); 8064 } 8065 $pluginfunctions[$plugintype][$plugin] = $pluginfunction; 8066 } 8067 8068 } 8069 } 8070 if (!empty($CFG->allversionshash)) { 8071 $cache->set($key, $pluginfunctions); 8072 } 8073 8074 if ($migratedtohook && $file === 'lib.php') { 8075 $pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions); 8076 } 8077 8078 return $pluginfunctions; 8079 8080 } 8081 8082 /** 8083 * Lists plugin-like directories within specified directory 8084 * 8085 * This function was originally used for standard Moodle plugins, please use 8086 * new core_component::get_plugin_list() now. 8087 * 8088 * This function is used for general directory listing and backwards compatility. 8089 * 8090 * @param string $directory relative directory from root 8091 * @param string $exclude dir name to exclude from the list (defaults to none) 8092 * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot) 8093 * @return array Sorted array of directory names found under the requested parameters 8094 */ 8095 function get_list_of_plugins($directory='mod', $exclude='', $basedir='') { 8096 global $CFG; 8097 8098 $plugins = array(); 8099 8100 if (empty($basedir)) { 8101 $basedir = $CFG->dirroot .'/'. $directory; 8102 8103 } else { 8104 $basedir = $basedir .'/'. $directory; 8105 } 8106 8107 if ($CFG->debugdeveloper and empty($exclude)) { 8108 // Make sure devs do not use this to list normal plugins, 8109 // this is intended for general directories that are not plugins! 8110 8111 $subtypes = core_component::get_plugin_types(); 8112 if (in_array($basedir, $subtypes)) { 8113 debugging('get_list_of_plugins() should not be used to list real plugins, use core_component::get_plugin_list() instead!', DEBUG_DEVELOPER); 8114 } 8115 unset($subtypes); 8116 } 8117 8118 $ignorelist = array_flip(array_filter([ 8119 'CVS', 8120 '_vti_cnf', 8121 'amd', 8122 'classes', 8123 'simpletest', 8124 'tests', 8125 'templates', 8126 'yui', 8127 $exclude, 8128 ])); 8129 8130 if (file_exists($basedir) && filetype($basedir) == 'dir') { 8131 if (!$dirhandle = opendir($basedir)) { 8132 debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER); 8133 return array(); 8134 } 8135 while (false !== ($dir = readdir($dirhandle))) { 8136 if (strpos($dir, '.') === 0) { 8137 // Ignore directories starting with . 8138 // These are treated as hidden directories. 8139 continue; 8140 } 8141 if (array_key_exists($dir, $ignorelist)) { 8142 // This directory features on the ignore list. 8143 continue; 8144 } 8145 if (filetype($basedir .'/'. $dir) != 'dir') { 8146 continue; 8147 } 8148 $plugins[] = $dir; 8149 } 8150 closedir($dirhandle); 8151 } 8152 if ($plugins) { 8153 asort($plugins); 8154 } 8155 return $plugins; 8156 } 8157 8158 /** 8159 * Invoke plugin's callback functions 8160 * 8161 * @param string $type plugin type e.g. 'mod' 8162 * @param string $name plugin name 8163 * @param string $feature feature name 8164 * @param string $action feature's action 8165 * @param array $params parameters of callback function, should be an array 8166 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null. 8167 * @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing 8168 * @return mixed 8169 * 8170 * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743 8171 */ 8172 function plugin_callback($type, $name, $feature, $action, $params = null, $default = null, bool $migratedtohook = false) { 8173 return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default, $migratedtohook); 8174 } 8175 8176 /** 8177 * Invoke component's callback functions 8178 * 8179 * @param string $component frankenstyle component name, e.g. 'mod_quiz' 8180 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron' 8181 * @param array $params parameters of callback function 8182 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null. 8183 * @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing 8184 * @return mixed 8185 */ 8186 function component_callback($component, $function, array $params = array(), $default = null, bool $migratedtohook = false) { 8187 8188 $functionname = component_callback_exists($component, $function); 8189 8190 if ($params && (array_keys($params) !== range(0, count($params) - 1))) { 8191 // PHP 8 allows to have associative arrays in the call_user_func_array() parameters but 8192 // PHP 7 does not. Using associative arrays can result in different behavior in different PHP versions. 8193 // See https://php.watch/versions/8.0/named-parameters#named-params-call_user_func_array 8194 // This check can be removed when minimum PHP version for Moodle is raised to 8. 8195 debugging('Parameters array can not be an associative array while Moodle supports both PHP 7 and PHP 8.', 8196 DEBUG_DEVELOPER); 8197 $params = array_values($params); 8198 } 8199 8200 if ($functionname) { 8201 if ($migratedtohook) { 8202 if (\core\hook\manager::get_instance()->is_deprecated_plugin_callback($function)) { 8203 if (\core\hook\manager::get_instance()->is_deprecating_hook_present($component, $function)) { 8204 // Do not call the old lib.php callback, 8205 // it is there for compatibility with older Moodle versions only. 8206 return null; 8207 } else { 8208 debugging("Callback $function in $component component should be migrated to new hook callback", 8209 DEBUG_DEVELOPER); 8210 } 8211 } 8212 } 8213 8214 // Function exists, so just return function result. 8215 $ret = call_user_func_array($functionname, $params); 8216 if (is_null($ret)) { 8217 return $default; 8218 } else { 8219 return $ret; 8220 } 8221 } 8222 return $default; 8223 } 8224 8225 /** 8226 * Determine if a component callback exists and return the function name to call. Note that this 8227 * function will include the required library files so that the functioname returned can be 8228 * called directly. 8229 * 8230 * @param string $component frankenstyle component name, e.g. 'mod_quiz' 8231 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron' 8232 * @return mixed Complete function name to call if the callback exists or false if it doesn't. 8233 * @throws coding_exception if invalid component specfied 8234 */ 8235 function component_callback_exists($component, $function) { 8236 global $CFG; // This is needed for the inclusions. 8237 8238 $cleancomponent = clean_param($component, PARAM_COMPONENT); 8239 if (empty($cleancomponent)) { 8240 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component); 8241 } 8242 $component = $cleancomponent; 8243 8244 list($type, $name) = core_component::normalize_component($component); 8245 $component = $type . '_' . $name; 8246 8247 $oldfunction = $name.'_'.$function; 8248 $function = $component.'_'.$function; 8249 8250 $dir = core_component::get_component_directory($component); 8251 if (empty($dir)) { 8252 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component); 8253 } 8254 8255 // Load library and look for function. 8256 if (file_exists($dir.'/lib.php')) { 8257 require_once($dir.'/lib.php'); 8258 } 8259 8260 if (!function_exists($function) and function_exists($oldfunction)) { 8261 if ($type !== 'mod' and $type !== 'core') { 8262 debugging("Please use new function name $function instead of legacy $oldfunction", DEBUG_DEVELOPER); 8263 } 8264 $function = $oldfunction; 8265 } 8266 8267 if (function_exists($function)) { 8268 return $function; 8269 } 8270 return false; 8271 } 8272 8273 /** 8274 * Call the specified callback method on the provided class. 8275 * 8276 * If the callback returns null, then the default value is returned instead. 8277 * If the class does not exist, then the default value is returned. 8278 * 8279 * @param string $classname The name of the class to call upon. 8280 * @param string $methodname The name of the staticically defined method on the class. 8281 * @param array $params The arguments to pass into the method. 8282 * @param mixed $default The default value. 8283 * @return mixed The return value. 8284 */ 8285 function component_class_callback($classname, $methodname, array $params, $default = null) { 8286 if (!class_exists($classname)) { 8287 return $default; 8288 } 8289 8290 if (!method_exists($classname, $methodname)) { 8291 return $default; 8292 } 8293 8294 $fullfunction = $classname . '::' . $methodname; 8295 $result = call_user_func_array($fullfunction, $params); 8296 8297 if (null === $result) { 8298 return $default; 8299 } else { 8300 return $result; 8301 } 8302 } 8303 8304 /** 8305 * Checks whether a plugin supports a specified feature. 8306 * 8307 * @param string $type Plugin type e.g. 'mod' 8308 * @param string $name Plugin name e.g. 'forum' 8309 * @param string $feature Feature code (FEATURE_xx constant) 8310 * @param mixed $default default value if feature support unknown 8311 * @return mixed Feature result (false if not supported, null if feature is unknown, 8312 * otherwise usually true but may have other feature-specific value such as array) 8313 * @throws coding_exception 8314 */ 8315 function plugin_supports($type, $name, $feature, $default = null) { 8316 global $CFG; 8317 8318 if ($type === 'mod' and $name === 'NEWMODULE') { 8319 // Somebody forgot to rename the module template. 8320 return false; 8321 } 8322 8323 $component = clean_param($type . '_' . $name, PARAM_COMPONENT); 8324 if (empty($component)) { 8325 throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name); 8326 } 8327 8328 $function = null; 8329 8330 if ($type === 'mod') { 8331 // We need this special case because we support subplugins in modules, 8332 // otherwise it would end up in infinite loop. 8333 if (file_exists("$CFG->dirroot/mod/$name/lib.php")) { 8334 include_once("$CFG->dirroot/mod/$name/lib.php"); 8335 $function = $component.'_supports'; 8336 if (!function_exists($function)) { 8337 // Legacy non-frankenstyle function name. 8338 $function = $name.'_supports'; 8339 } 8340 } 8341 8342 } else { 8343 if (!$path = core_component::get_plugin_directory($type, $name)) { 8344 // Non existent plugin type. 8345 return false; 8346 } 8347 if (file_exists("$path/lib.php")) { 8348 include_once("$path/lib.php"); 8349 $function = $component.'_supports'; 8350 } 8351 } 8352 8353 if ($function and function_exists($function)) { 8354 $supports = $function($feature); 8355 if (is_null($supports)) { 8356 // Plugin does not know - use default. 8357 return $default; 8358 } else { 8359 return $supports; 8360 } 8361 } 8362 8363 // Plugin does not care, so use default. 8364 return $default; 8365 } 8366 8367 /** 8368 * Returns true if the current version of PHP is greater that the specified one. 8369 * 8370 * @todo Check PHP version being required here is it too low? 8371 * 8372 * @param string $version The version of php being tested. 8373 * @return bool 8374 */ 8375 function check_php_version($version='5.2.4') { 8376 return (version_compare(phpversion(), $version) >= 0); 8377 } 8378 8379 /** 8380 * Determine if moodle installation requires update. 8381 * 8382 * Checks version numbers of main code and all plugins to see 8383 * if there are any mismatches. 8384 * 8385 * @param bool $checkupgradeflag check the outagelessupgrade flag to see if an upgrade is running. 8386 * @return bool 8387 */ 8388 function moodle_needs_upgrading($checkupgradeflag = true) { 8389 global $CFG, $DB; 8390 8391 // Say no if there is already an upgrade running. 8392 if ($checkupgradeflag) { 8393 $lock = $DB->get_field('config', 'value', ['name' => 'outagelessupgrade']); 8394 $currentprocessrunningupgrade = (defined('CLI_UPGRADE_RUNNING') && CLI_UPGRADE_RUNNING); 8395 // If we ARE locked, but this PHP process is NOT the process running the upgrade, 8396 // We should always return false. 8397 // This means the upgrade is running from CLI somewhere, or about to. 8398 if (!empty($lock) && !$currentprocessrunningupgrade) { 8399 return false; 8400 } 8401 } 8402 8403 if (empty($CFG->version)) { 8404 return true; 8405 } 8406 8407 // There is no need to purge plugininfo caches here because 8408 // these caches are not used during upgrade and they are purged after 8409 // every upgrade. 8410 8411 if (empty($CFG->allversionshash)) { 8412 return true; 8413 } 8414 8415 $hash = core_component::get_all_versions_hash(); 8416 8417 return ($hash !== $CFG->allversionshash); 8418 } 8419 8420 /** 8421 * Returns the major version of this site 8422 * 8423 * Moodle version numbers consist of three numbers separated by a dot, for 8424 * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so 8425 * called major version. This function extracts the major version from either 8426 * $CFG->release (default) or eventually from the $release variable defined in 8427 * the main version.php. 8428 * 8429 * @param bool $fromdisk should the version if source code files be used 8430 * @return string|false the major version like '2.3', false if could not be determined 8431 */ 8432 function moodle_major_version($fromdisk = false) { 8433 global $CFG; 8434 8435 if ($fromdisk) { 8436 $release = null; 8437 require($CFG->dirroot.'/version.php'); 8438 if (empty($release)) { 8439 return false; 8440 } 8441 8442 } else { 8443 if (empty($CFG->release)) { 8444 return false; 8445 } 8446 $release = $CFG->release; 8447 } 8448 8449 if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) { 8450 return $matches[0]; 8451 } else { 8452 return false; 8453 } 8454 } 8455 8456 // MISCELLANEOUS. 8457 8458 /** 8459 * Gets the system locale 8460 * 8461 * @return string Retuns the current locale. 8462 */ 8463 function moodle_getlocale() { 8464 global $CFG; 8465 8466 // Fetch the correct locale based on ostype. 8467 if ($CFG->ostype == 'WINDOWS') { 8468 $stringtofetch = 'localewin'; 8469 } else { 8470 $stringtofetch = 'locale'; 8471 } 8472 8473 if (!empty($CFG->locale)) { // Override locale for all language packs. 8474 return $CFG->locale; 8475 } 8476 8477 return get_string($stringtofetch, 'langconfig'); 8478 } 8479 8480 /** 8481 * Sets the system locale 8482 * 8483 * @category string 8484 * @param string $locale Can be used to force a locale 8485 */ 8486 function moodle_setlocale($locale='') { 8487 global $CFG; 8488 8489 static $currentlocale = ''; // Last locale caching. 8490 8491 $oldlocale = $currentlocale; 8492 8493 // The priority is the same as in get_string() - parameter, config, course, session, user, global language. 8494 if (!empty($locale)) { 8495 $currentlocale = $locale; 8496 } else { 8497 $currentlocale = moodle_getlocale(); 8498 } 8499 8500 // Do nothing if locale already set up. 8501 if ($oldlocale == $currentlocale) { 8502 return; 8503 } 8504 8505 // Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values, 8506 // set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7 8507 // Some day, numeric, monetary and other categories should be set too, I think. :-/. 8508 8509 // Get current values. 8510 $monetary= setlocale (LC_MONETARY, 0); 8511 $numeric = setlocale (LC_NUMERIC, 0); 8512 $ctype = setlocale (LC_CTYPE, 0); 8513 if ($CFG->ostype != 'WINDOWS') { 8514 $messages= setlocale (LC_MESSAGES, 0); 8515 } 8516 // Set locale to all. 8517 $result = setlocale (LC_ALL, $currentlocale); 8518 // If setting of locale fails try the other utf8 or utf-8 variant, 8519 // some operating systems support both (Debian), others just one (OSX). 8520 if ($result === false) { 8521 if (stripos($currentlocale, '.UTF-8') !== false) { 8522 $newlocale = str_ireplace('.UTF-8', '.UTF8', $currentlocale); 8523 setlocale (LC_ALL, $newlocale); 8524 } else if (stripos($currentlocale, '.UTF8') !== false) { 8525 $newlocale = str_ireplace('.UTF8', '.UTF-8', $currentlocale); 8526 setlocale (LC_ALL, $newlocale); 8527 } 8528 } 8529 // Set old values. 8530 setlocale (LC_MONETARY, $monetary); 8531 setlocale (LC_NUMERIC, $numeric); 8532 if ($CFG->ostype != 'WINDOWS') { 8533 setlocale (LC_MESSAGES, $messages); 8534 } 8535 if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') { 8536 // To workaround a well-known PHP problem with Turkish letter Ii. 8537 setlocale (LC_CTYPE, $ctype); 8538 } 8539 } 8540 8541 /** 8542 * Count words in a string. 8543 * 8544 * Words are defined as things between whitespace. 8545 * 8546 * @category string 8547 * @param string $string The text to be searched for words. May be HTML. 8548 * @param int|null $format 8549 * @return int The count of words in the specified string 8550 */ 8551 function count_words($string, $format = null) { 8552 // Before stripping tags, add a space after the close tag of anything that is not obviously inline. 8553 // Also, br is a special case because it definitely delimits a word, but has no close tag. 8554 $string = preg_replace('~ 8555 ( # Capture the tag we match. 8556 </ # Start of close tag. 8557 (?! # Do not match any of these specific close tag names. 8558 a> | b> | del> | em> | i> | 8559 ins> | s> | small> | span> | 8560 strong> | sub> | sup> | u> 8561 ) 8562 \w+ # But, apart from those execptions, match any tag name. 8563 > # End of close tag. 8564 | 8565 <br> | <br\s*/> # Special cases that are not close tags. 8566 ) 8567 ~x', '$1 ', $string); // Add a space after the close tag. 8568 if ($format !== null && $format != FORMAT_PLAIN) { 8569 // Match the usual text cleaning before display. 8570 // Ideally we should apply multilang filter only here, other filters might add extra text. 8571 $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]); 8572 } 8573 // Now remove HTML tags. 8574 $string = strip_tags($string); 8575 // Decode HTML entities. 8576 $string = html_entity_decode($string, ENT_COMPAT); 8577 8578 // Now, the word count is the number of blocks of characters separated 8579 // by any sort of space. That seems to be the definition used by all other systems. 8580 // To be precise about what is considered to separate words: 8581 // * Anything that Unicode considers a 'Separator' 8582 // * Anything that Unicode considers a 'Control character' 8583 // * An em- or en- dash. 8584 return count(preg_split('~[\p{Z}\p{Cc}—–]+~u', $string, -1, PREG_SPLIT_NO_EMPTY)); 8585 } 8586 8587 /** 8588 * Count letters in a string. 8589 * 8590 * Letters are defined as chars not in tags and different from whitespace. 8591 * 8592 * @category string 8593 * @param string $string The text to be searched for letters. May be HTML. 8594 * @param int|null $format 8595 * @return int The count of letters in the specified text. 8596 */ 8597 function count_letters($string, $format = null) { 8598 if ($format !== null && $format != FORMAT_PLAIN) { 8599 // Match the usual text cleaning before display. 8600 // Ideally we should apply multilang filter only here, other filters might add extra text. 8601 $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]); 8602 } 8603 $string = strip_tags($string); // Tags are out now. 8604 $string = html_entity_decode($string, ENT_COMPAT); 8605 $string = preg_replace('/[[:space:]]*/', '', $string); // Whitespace are out now. 8606 8607 return core_text::strlen($string); 8608 } 8609 8610 /** 8611 * Generate and return a random string of the specified length. 8612 * 8613 * @param int $length The length of the string to be created. 8614 * @return string 8615 */ 8616 function random_string($length=15) { 8617 $randombytes = random_bytes($length); 8618 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 8619 $pool .= 'abcdefghijklmnopqrstuvwxyz'; 8620 $pool .= '0123456789'; 8621 $poollen = strlen($pool); 8622 $string = ''; 8623 for ($i = 0; $i < $length; $i++) { 8624 $rand = ord($randombytes[$i]); 8625 $string .= substr($pool, ($rand%($poollen)), 1); 8626 } 8627 return $string; 8628 } 8629 8630 /** 8631 * Generate a complex random string (useful for md5 salts) 8632 * 8633 * This function is based on the above {@link random_string()} however it uses a 8634 * larger pool of characters and generates a string between 24 and 32 characters 8635 * 8636 * @param int $length Optional if set generates a string to exactly this length 8637 * @return string 8638 */ 8639 function complex_random_string($length=null) { 8640 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 8641 $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} '; 8642 $poollen = strlen($pool); 8643 if ($length===null) { 8644 $length = floor(rand(24, 32)); 8645 } 8646 $randombytes = random_bytes($length); 8647 $string = ''; 8648 for ($i = 0; $i < $length; $i++) { 8649 $rand = ord($randombytes[$i]); 8650 $string .= $pool[($rand%$poollen)]; 8651 } 8652 return $string; 8653 } 8654 8655 /** 8656 * Given some text (which may contain HTML) and an ideal length, 8657 * this function truncates the text neatly on a word boundary if possible 8658 * 8659 * @category string 8660 * @param string $text text to be shortened 8661 * @param int $ideal ideal string length 8662 * @param boolean $exact if false, $text will not be cut mid-word 8663 * @param string $ending The string to append if the passed string is truncated 8664 * @return string $truncate shortened string 8665 */ 8666 function shorten_text($text, $ideal=30, $exact = false, $ending='...') { 8667 // If the plain text is shorter than the maximum length, return the whole text. 8668 if (core_text::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) { 8669 return $text; 8670 } 8671 8672 // Splits on HTML tags. Each open/close/empty tag will be the first thing 8673 // and only tag in its 'line'. 8674 preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER); 8675 8676 $totallength = core_text::strlen($ending); 8677 $truncate = ''; 8678 8679 // This array stores information about open and close tags and their position 8680 // in the truncated string. Each item in the array is an object with fields 8681 // ->open (true if open), ->tag (tag name in lower case), and ->pos 8682 // (byte position in truncated text). 8683 $tagdetails = array(); 8684 8685 foreach ($lines as $linematchings) { 8686 // If there is any html-tag in this line, handle it and add it (uncounted) to the output. 8687 if (!empty($linematchings[1])) { 8688 // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>). 8689 if (!preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $linematchings[1])) { 8690 if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $linematchings[1], $tagmatchings)) { 8691 // Record closing tag. 8692 $tagdetails[] = (object) array( 8693 'open' => false, 8694 'tag' => core_text::strtolower($tagmatchings[1]), 8695 'pos' => core_text::strlen($truncate), 8696 ); 8697 8698 } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $linematchings[1], $tagmatchings)) { 8699 // Record opening tag. 8700 $tagdetails[] = (object) array( 8701 'open' => true, 8702 'tag' => core_text::strtolower($tagmatchings[1]), 8703 'pos' => core_text::strlen($truncate), 8704 ); 8705 } else if (preg_match('/^<!--\[if\s.*?\]>$/s', $linematchings[1], $tagmatchings)) { 8706 $tagdetails[] = (object) array( 8707 'open' => true, 8708 'tag' => core_text::strtolower('if'), 8709 'pos' => core_text::strlen($truncate), 8710 ); 8711 } else if (preg_match('/^<!--<!\[endif\]-->$/s', $linematchings[1], $tagmatchings)) { 8712 $tagdetails[] = (object) array( 8713 'open' => false, 8714 'tag' => core_text::strtolower('if'), 8715 'pos' => core_text::strlen($truncate), 8716 ); 8717 } 8718 } 8719 // Add html-tag to $truncate'd text. 8720 $truncate .= $linematchings[1]; 8721 } 8722 8723 // Calculate the length of the plain text part of the line; handle entities as one character. 8724 $contentlength = core_text::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $linematchings[2])); 8725 if ($totallength + $contentlength > $ideal) { 8726 // The number of characters which are left. 8727 $left = $ideal - $totallength; 8728 $entitieslength = 0; 8729 // Search for html entities. 8730 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)) { 8731 // Calculate the real length of all entities in the legal range. 8732 foreach ($entities[0] as $entity) { 8733 if ($entity[1]+1-$entitieslength <= $left) { 8734 $left--; 8735 $entitieslength += core_text::strlen($entity[0]); 8736 } else { 8737 // No more characters left. 8738 break; 8739 } 8740 } 8741 } 8742 $breakpos = $left + $entitieslength; 8743 8744 // If the words shouldn't be cut in the middle... 8745 if (!$exact) { 8746 // Search the last occurence of a space. 8747 for (; $breakpos > 0; $breakpos--) { 8748 if ($char = core_text::substr($linematchings[2], $breakpos, 1)) { 8749 if ($char === '.' or $char === ' ') { 8750 $breakpos += 1; 8751 break; 8752 } else if (strlen($char) > 2) { 8753 // Chinese/Japanese/Korean text can be truncated at any UTF-8 character boundary. 8754 $breakpos += 1; 8755 break; 8756 } 8757 } 8758 } 8759 } 8760 if ($breakpos == 0) { 8761 // This deals with the test_shorten_text_no_spaces case. 8762 $breakpos = $left + $entitieslength; 8763 } else if ($breakpos > $left + $entitieslength) { 8764 // This deals with the previous for loop breaking on the first char. 8765 $breakpos = $left + $entitieslength; 8766 } 8767 8768 $truncate .= core_text::substr($linematchings[2], 0, $breakpos); 8769 // Maximum length is reached, so get off the loop. 8770 break; 8771 } else { 8772 $truncate .= $linematchings[2]; 8773 $totallength += $contentlength; 8774 } 8775 8776 // If the maximum length is reached, get off the loop. 8777 if ($totallength >= $ideal) { 8778 break; 8779 } 8780 } 8781 8782 // Add the defined ending to the text. 8783 $truncate .= $ending; 8784 8785 // Now calculate the list of open html tags based on the truncate position. 8786 $opentags = array(); 8787 foreach ($tagdetails as $taginfo) { 8788 if ($taginfo->open) { 8789 // Add tag to the beginning of $opentags list. 8790 array_unshift($opentags, $taginfo->tag); 8791 } else { 8792 // Can have multiple exact same open tags, close the last one. 8793 $pos = array_search($taginfo->tag, array_reverse($opentags, true)); 8794 if ($pos !== false) { 8795 unset($opentags[$pos]); 8796 } 8797 } 8798 } 8799 8800 // Close all unclosed html-tags. 8801 foreach ($opentags as $tag) { 8802 if ($tag === 'if') { 8803 $truncate .= '<!--<![endif]-->'; 8804 } else { 8805 $truncate .= '</' . $tag . '>'; 8806 } 8807 } 8808 8809 return $truncate; 8810 } 8811 8812 /** 8813 * Shortens a given filename by removing characters positioned after the ideal string length. 8814 * When the filename is too long, the file cannot be created on the filesystem due to exceeding max byte size. 8815 * Limiting the filename to a certain size (considering multibyte characters) will prevent this. 8816 * 8817 * @param string $filename file name 8818 * @param int $length ideal string length 8819 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness. 8820 * @return string $shortened shortened file name 8821 */ 8822 function shorten_filename($filename, $length = MAX_FILENAME_SIZE, $includehash = false) { 8823 $shortened = $filename; 8824 // Extract a part of the filename if it's char size exceeds the ideal string length. 8825 if (core_text::strlen($filename) > $length) { 8826 // Exclude extension if present in filename. 8827 $mimetypes = get_mimetypes_array(); 8828 $extension = pathinfo($filename, PATHINFO_EXTENSION); 8829 if ($extension && !empty($mimetypes[$extension])) { 8830 $basename = pathinfo($filename, PATHINFO_FILENAME); 8831 $hash = empty($includehash) ? '' : ' - ' . substr(sha1($basename), 0, 10); 8832 $shortened = core_text::substr($basename, 0, $length - strlen($hash)) . $hash; 8833 $shortened .= '.' . $extension; 8834 } else { 8835 $hash = empty($includehash) ? '' : ' - ' . substr(sha1($filename), 0, 10); 8836 $shortened = core_text::substr($filename, 0, $length - strlen($hash)) . $hash; 8837 } 8838 } 8839 return $shortened; 8840 } 8841 8842 /** 8843 * Shortens a given array of filenames by removing characters positioned after the ideal string length. 8844 * 8845 * @param array $path The paths to reduce the length. 8846 * @param int $length Ideal string length 8847 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness. 8848 * @return array $result Shortened paths in array. 8849 */ 8850 function shorten_filenames(array $path, $length = MAX_FILENAME_SIZE, $includehash = false) { 8851 $result = null; 8852 8853 $result = array_reduce($path, function($carry, $singlepath) use ($length, $includehash) { 8854 $carry[] = shorten_filename($singlepath, $length, $includehash); 8855 return $carry; 8856 }, []); 8857 8858 return $result; 8859 } 8860 8861 /** 8862 * Given dates in seconds, how many weeks is the date from startdate 8863 * The first week is 1, the second 2 etc ... 8864 * 8865 * @param int $startdate Timestamp for the start date 8866 * @param int $thedate Timestamp for the end date 8867 * @return string 8868 */ 8869 function getweek ($startdate, $thedate) { 8870 if ($thedate < $startdate) { 8871 return 0; 8872 } 8873 8874 return floor(($thedate - $startdate) / WEEKSECS) + 1; 8875 } 8876 8877 /** 8878 * Returns a randomly generated password of length $maxlen. inspired by 8879 * 8880 * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and 8881 * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254} 8882 * 8883 * @param int $maxlen The maximum size of the password being generated. 8884 * @return string 8885 */ 8886 function generate_password($maxlen=10) { 8887 global $CFG; 8888 8889 if (empty($CFG->passwordpolicy)) { 8890 $fillers = PASSWORD_DIGITS; 8891 $wordlist = file($CFG->wordlist); 8892 $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]); 8893 $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]); 8894 $filler1 = $fillers[rand(0, strlen($fillers) - 1)]; 8895 $password = $word1 . $filler1 . $word2; 8896 } else { 8897 $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0; 8898 $digits = $CFG->minpassworddigits; 8899 $lower = $CFG->minpasswordlower; 8900 $upper = $CFG->minpasswordupper; 8901 $nonalphanum = $CFG->minpasswordnonalphanum; 8902 $total = $lower + $upper + $digits + $nonalphanum; 8903 // Var minlength should be the greater one of the two ( $minlen and $total ). 8904 $minlen = $minlen < $total ? $total : $minlen; 8905 // Var maxlen can never be smaller than minlen. 8906 $maxlen = $minlen > $maxlen ? $minlen : $maxlen; 8907 $additional = $maxlen - $total; 8908 8909 // Make sure we have enough characters to fulfill 8910 // complexity requirements. 8911 $passworddigits = PASSWORD_DIGITS; 8912 while ($digits > strlen($passworddigits)) { 8913 $passworddigits .= PASSWORD_DIGITS; 8914 } 8915 $passwordlower = PASSWORD_LOWER; 8916 while ($lower > strlen($passwordlower)) { 8917 $passwordlower .= PASSWORD_LOWER; 8918 } 8919 $passwordupper = PASSWORD_UPPER; 8920 while ($upper > strlen($passwordupper)) { 8921 $passwordupper .= PASSWORD_UPPER; 8922 } 8923 $passwordnonalphanum = PASSWORD_NONALPHANUM; 8924 while ($nonalphanum > strlen($passwordnonalphanum)) { 8925 $passwordnonalphanum .= PASSWORD_NONALPHANUM; 8926 } 8927 8928 // Now mix and shuffle it all. 8929 $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) . 8930 substr(str_shuffle ($passwordupper), 0, $upper) . 8931 substr(str_shuffle ($passworddigits), 0, $digits) . 8932 substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) . 8933 substr(str_shuffle ($passwordlower . 8934 $passwordupper . 8935 $passworddigits . 8936 $passwordnonalphanum), 0 , $additional)); 8937 } 8938 8939 return substr ($password, 0, $maxlen); 8940 } 8941 8942 /** 8943 * Given a float, prints it nicely. 8944 * Localized floats must not be used in calculations! 8945 * 8946 * The stripzeros feature is intended for making numbers look nicer in small 8947 * areas where it is not necessary to indicate the degree of accuracy by showing 8948 * ending zeros. If you turn it on with $decimalpoints set to 3, for example, 8949 * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'. 8950 * 8951 * @param float $float The float to print 8952 * @param int $decimalpoints The number of decimal places to print. -1 is a special value for auto detect (full precision). 8953 * @param bool $localized use localized decimal separator 8954 * @param bool $stripzeros If true, removes final zeros after decimal point. It will be ignored and the trailing zeros after 8955 * the decimal point are always striped if $decimalpoints is -1. 8956 * @return string locale float 8957 */ 8958 function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=false) { 8959 if (is_null($float)) { 8960 return ''; 8961 } 8962 if ($localized) { 8963 $separator = get_string('decsep', 'langconfig'); 8964 } else { 8965 $separator = '.'; 8966 } 8967 if ($decimalpoints == -1) { 8968 // The following counts the number of decimals. 8969 // It is safe as both floatval() and round() functions have same behaviour when non-numeric values are provided. 8970 $floatval = floatval($float); 8971 for ($decimalpoints = 0; $floatval != round($float, $decimalpoints); $decimalpoints++); 8972 } 8973 8974 $result = number_format($float, $decimalpoints, $separator, ''); 8975 if ($stripzeros && $decimalpoints > 0) { 8976 // Remove zeros and final dot if not needed. 8977 // However, only do this if there is a decimal point! 8978 $result = preg_replace('~(' . preg_quote($separator, '~') . ')?0+$~', '', $result); 8979 } 8980 return $result; 8981 } 8982 8983 /** 8984 * Converts locale specific floating point/comma number back to standard PHP float value 8985 * Do NOT try to do any math operations before this conversion on any user submitted floats! 8986 * 8987 * @param string $localefloat locale aware float representation 8988 * @param bool $strict If true, then check the input and return false if it is not a valid number. 8989 * @return mixed float|bool - false or the parsed float. 8990 */ 8991 function unformat_float($localefloat, $strict = false) { 8992 $localefloat = trim((string)$localefloat); 8993 8994 if ($localefloat == '') { 8995 return null; 8996 } 8997 8998 $localefloat = str_replace(' ', '', $localefloat); // No spaces - those might be used as thousand separators. 8999 $localefloat = str_replace(get_string('decsep', 'langconfig'), '.', $localefloat); 9000 9001 if ($strict && !is_numeric($localefloat)) { 9002 return false; 9003 } 9004 9005 return (float)$localefloat; 9006 } 9007 9008 /** 9009 * Given a simple array, this shuffles it up just like shuffle() 9010 * Unlike PHP's shuffle() this function works on any machine. 9011 * 9012 * @param array $array The array to be rearranged 9013 * @return array 9014 */ 9015 function swapshuffle($array) { 9016 9017 $last = count($array) - 1; 9018 for ($i = 0; $i <= $last; $i++) { 9019 $from = rand(0, $last); 9020 $curr = $array[$i]; 9021 $array[$i] = $array[$from]; 9022 $array[$from] = $curr; 9023 } 9024 return $array; 9025 } 9026 9027 /** 9028 * Like {@link swapshuffle()}, but works on associative arrays 9029 * 9030 * @param array $array The associative array to be rearranged 9031 * @return array 9032 */ 9033 function swapshuffle_assoc($array) { 9034 9035 $newarray = array(); 9036 $newkeys = swapshuffle(array_keys($array)); 9037 9038 foreach ($newkeys as $newkey) { 9039 $newarray[$newkey] = $array[$newkey]; 9040 } 9041 return $newarray; 9042 } 9043 9044 /** 9045 * Given an arbitrary array, and a number of draws, 9046 * this function returns an array with that amount 9047 * of items. The indexes are retained. 9048 * 9049 * @todo Finish documenting this function 9050 * 9051 * @param array $array 9052 * @param int $draws 9053 * @return array 9054 */ 9055 function draw_rand_array($array, $draws) { 9056 9057 $return = array(); 9058 9059 $last = count($array); 9060 9061 if ($draws > $last) { 9062 $draws = $last; 9063 } 9064 9065 while ($draws > 0) { 9066 $last--; 9067 9068 $keys = array_keys($array); 9069 $rand = rand(0, $last); 9070 9071 $return[$keys[$rand]] = $array[$keys[$rand]]; 9072 unset($array[$keys[$rand]]); 9073 9074 $draws--; 9075 } 9076 9077 return $return; 9078 } 9079 9080 /** 9081 * Calculate the difference between two microtimes 9082 * 9083 * @param string $a The first Microtime 9084 * @param string $b The second Microtime 9085 * @return string 9086 */ 9087 function microtime_diff($a, $b) { 9088 list($adec, $asec) = explode(' ', $a); 9089 list($bdec, $bsec) = explode(' ', $b); 9090 return $bsec - $asec + $bdec - $adec; 9091 } 9092 9093 /** 9094 * Given a list (eg a,b,c,d,e) this function returns 9095 * an array of 1->a, 2->b, 3->c etc 9096 * 9097 * @param string $list The string to explode into array bits 9098 * @param string $separator The separator used within the list string 9099 * @return array The now assembled array 9100 */ 9101 function make_menu_from_list($list, $separator=',') { 9102 9103 $array = array_reverse(explode($separator, $list), true); 9104 foreach ($array as $key => $item) { 9105 $outarray[$key+1] = trim($item); 9106 } 9107 return $outarray; 9108 } 9109 9110 /** 9111 * Creates an array that represents all the current grades that 9112 * can be chosen using the given grading type. 9113 * 9114 * Negative numbers 9115 * are scales, zero is no grade, and positive numbers are maximum 9116 * grades. 9117 * 9118 * @todo Finish documenting this function or better deprecated this completely! 9119 * 9120 * @param int $gradingtype 9121 * @return array 9122 */ 9123 function make_grades_menu($gradingtype) { 9124 global $DB; 9125 9126 $grades = array(); 9127 if ($gradingtype < 0) { 9128 if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) { 9129 return make_menu_from_list($scale->scale); 9130 } 9131 } else if ($gradingtype > 0) { 9132 for ($i=$gradingtype; $i>=0; $i--) { 9133 $grades[$i] = $i .' / '. $gradingtype; 9134 } 9135 return $grades; 9136 } 9137 return $grades; 9138 } 9139 9140 /** 9141 * make_unique_id_code 9142 * 9143 * @todo Finish documenting this function 9144 * 9145 * @uses $_SERVER 9146 * @param string $extra Extra string to append to the end of the code 9147 * @return string 9148 */ 9149 function make_unique_id_code($extra = '') { 9150 9151 $hostname = 'unknownhost'; 9152 if (!empty($_SERVER['HTTP_HOST'])) { 9153 $hostname = $_SERVER['HTTP_HOST']; 9154 } else if (!empty($_ENV['HTTP_HOST'])) { 9155 $hostname = $_ENV['HTTP_HOST']; 9156 } else if (!empty($_SERVER['SERVER_NAME'])) { 9157 $hostname = $_SERVER['SERVER_NAME']; 9158 } else if (!empty($_ENV['SERVER_NAME'])) { 9159 $hostname = $_ENV['SERVER_NAME']; 9160 } 9161 9162 $date = gmdate("ymdHis"); 9163 9164 $random = random_string(6); 9165 9166 if ($extra) { 9167 return $hostname .'+'. $date .'+'. $random .'+'. $extra; 9168 } else { 9169 return $hostname .'+'. $date .'+'. $random; 9170 } 9171 } 9172 9173 9174 /** 9175 * Function to check the passed address is within the passed subnet 9176 * 9177 * The parameter is a comma separated string of subnet definitions. 9178 * Subnet strings can be in one of three formats: 9179 * 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn (number of bits in net mask) 9180 * 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) 9181 * 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. (incomplete address, a bit non-technical ;-) 9182 * Code for type 1 modified from user posted comments by mediator at 9183 * {@link http://au.php.net/manual/en/function.ip2long.php} 9184 * 9185 * @param string $addr The address you are checking 9186 * @param string $subnetstr The string of subnet addresses 9187 * @param bool $checkallzeros The state to whether check for 0.0.0.0 9188 * @return bool 9189 */ 9190 function address_in_subnet($addr, $subnetstr, $checkallzeros = false) { 9191 9192 if ($addr == '0.0.0.0' && !$checkallzeros) { 9193 return false; 9194 } 9195 $subnets = explode(',', $subnetstr); 9196 $found = false; 9197 $addr = trim($addr); 9198 $addr = cleanremoteaddr($addr, false); // Normalise. 9199 if ($addr === null) { 9200 return false; 9201 } 9202 $addrparts = explode(':', $addr); 9203 9204 $ipv6 = strpos($addr, ':'); 9205 9206 foreach ($subnets as $subnet) { 9207 $subnet = trim($subnet); 9208 if ($subnet === '') { 9209 continue; 9210 } 9211 9212 if (strpos($subnet, '/') !== false) { 9213 // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn. 9214 list($ip, $mask) = explode('/', $subnet); 9215 $mask = trim($mask); 9216 if (!is_number($mask)) { 9217 continue; // Incorect mask number, eh? 9218 } 9219 $ip = cleanremoteaddr($ip, false); // Normalise. 9220 if ($ip === null) { 9221 continue; 9222 } 9223 if (strpos($ip, ':') !== false) { 9224 // IPv6. 9225 if (!$ipv6) { 9226 continue; 9227 } 9228 if ($mask > 128 or $mask < 0) { 9229 continue; // Nonsense. 9230 } 9231 if ($mask == 0) { 9232 return true; // Any address. 9233 } 9234 if ($mask == 128) { 9235 if ($ip === $addr) { 9236 return true; 9237 } 9238 continue; 9239 } 9240 $ipparts = explode(':', $ip); 9241 $modulo = $mask % 16; 9242 $ipnet = array_slice($ipparts, 0, ($mask-$modulo)/16); 9243 $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16); 9244 if (implode(':', $ipnet) === implode(':', $addrnet)) { 9245 if ($modulo == 0) { 9246 return true; 9247 } 9248 $pos = ($mask-$modulo)/16; 9249 $ipnet = hexdec($ipparts[$pos]); 9250 $addrnet = hexdec($addrparts[$pos]); 9251 $mask = 0xffff << (16 - $modulo); 9252 if (($addrnet & $mask) == ($ipnet & $mask)) { 9253 return true; 9254 } 9255 } 9256 9257 } else { 9258 // IPv4. 9259 if ($ipv6) { 9260 continue; 9261 } 9262 if ($mask > 32 or $mask < 0) { 9263 continue; // Nonsense. 9264 } 9265 if ($mask == 0) { 9266 return true; 9267 } 9268 if ($mask == 32) { 9269 if ($ip === $addr) { 9270 return true; 9271 } 9272 continue; 9273 } 9274 $mask = 0xffffffff << (32 - $mask); 9275 if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) { 9276 return true; 9277 } 9278 } 9279 9280 } else if (strpos($subnet, '-') !== false) { 9281 // 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. 9282 $parts = explode('-', $subnet); 9283 if (count($parts) != 2) { 9284 continue; 9285 } 9286 9287 if (strpos($subnet, ':') !== false) { 9288 // IPv6. 9289 if (!$ipv6) { 9290 continue; 9291 } 9292 $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise. 9293 if ($ipstart === null) { 9294 continue; 9295 } 9296 $ipparts = explode(':', $ipstart); 9297 $start = hexdec(array_pop($ipparts)); 9298 $ipparts[] = trim($parts[1]); 9299 $ipend = cleanremoteaddr(implode(':', $ipparts), false); // Normalise. 9300 if ($ipend === null) { 9301 continue; 9302 } 9303 $ipparts[7] = ''; 9304 $ipnet = implode(':', $ipparts); 9305 if (strpos($addr, $ipnet) !== 0) { 9306 continue; 9307 } 9308 $ipparts = explode(':', $ipend); 9309 $end = hexdec($ipparts[7]); 9310 9311 $addrend = hexdec($addrparts[7]); 9312 9313 if (($addrend >= $start) and ($addrend <= $end)) { 9314 return true; 9315 } 9316 9317 } else { 9318 // IPv4. 9319 if ($ipv6) { 9320 continue; 9321 } 9322 $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise. 9323 if ($ipstart === null) { 9324 continue; 9325 } 9326 $ipparts = explode('.', $ipstart); 9327 $ipparts[3] = trim($parts[1]); 9328 $ipend = cleanremoteaddr(implode('.', $ipparts), false); // Normalise. 9329 if ($ipend === null) { 9330 continue; 9331 } 9332 9333 if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) { 9334 return true; 9335 } 9336 } 9337 9338 } else { 9339 // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. 9340 if (strpos($subnet, ':') !== false) { 9341 // IPv6. 9342 if (!$ipv6) { 9343 continue; 9344 } 9345 $parts = explode(':', $subnet); 9346 $count = count($parts); 9347 if ($parts[$count-1] === '') { 9348 unset($parts[$count-1]); // Trim trailing :'s. 9349 $count--; 9350 $subnet = implode('.', $parts); 9351 } 9352 $isip = cleanremoteaddr($subnet, false); // Normalise. 9353 if ($isip !== null) { 9354 if ($isip === $addr) { 9355 return true; 9356 } 9357 continue; 9358 } else if ($count > 8) { 9359 continue; 9360 } 9361 $zeros = array_fill(0, 8-$count, '0'); 9362 $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16); 9363 if (address_in_subnet($addr, $subnet)) { 9364 return true; 9365 } 9366 9367 } else { 9368 // IPv4. 9369 if ($ipv6) { 9370 continue; 9371 } 9372 $parts = explode('.', $subnet); 9373 $count = count($parts); 9374 if ($parts[$count-1] === '') { 9375 unset($parts[$count-1]); // Trim trailing . 9376 $count--; 9377 $subnet = implode('.', $parts); 9378 } 9379 if ($count == 4) { 9380 $subnet = cleanremoteaddr($subnet, false); // Normalise. 9381 if ($subnet === $addr) { 9382 return true; 9383 } 9384 continue; 9385 } else if ($count > 4) { 9386 continue; 9387 } 9388 $zeros = array_fill(0, 4-$count, '0'); 9389 $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8); 9390 if (address_in_subnet($addr, $subnet)) { 9391 return true; 9392 } 9393 } 9394 } 9395 } 9396 9397 return false; 9398 } 9399 9400 /** 9401 * For outputting debugging info 9402 * 9403 * @param string $string The string to write 9404 * @param string $eol The end of line char(s) to use 9405 * @param string $sleep Period to make the application sleep 9406 * This ensures any messages have time to display before redirect 9407 */ 9408 function mtrace($string, $eol="\n", $sleep=0) { 9409 global $CFG; 9410 9411 if (isset($CFG->mtrace_wrapper) && function_exists($CFG->mtrace_wrapper)) { 9412 $fn = $CFG->mtrace_wrapper; 9413 $fn($string, $eol); 9414 return; 9415 } else if (defined('STDOUT') && !PHPUNIT_TEST && !defined('BEHAT_TEST')) { 9416 // We must explicitly call the add_line function here. 9417 // Uses of fwrite to STDOUT are not picked up by ob_start. 9418 if ($output = \core\task\logmanager::add_line("{$string}{$eol}")) { 9419 fwrite(STDOUT, $output); 9420 } 9421 } else { 9422 echo $string . $eol; 9423 } 9424 9425 // Flush again. 9426 flush(); 9427 9428 // Delay to keep message on user's screen in case of subsequent redirect. 9429 if ($sleep) { 9430 sleep($sleep); 9431 } 9432 } 9433 9434 /** 9435 * Helper to {@see mtrace()} an exception or throwable, including all relevant information. 9436 * 9437 * @param Throwable $e the error to ouptput. 9438 */ 9439 function mtrace_exception(Throwable $e): void { 9440 $info = get_exception_info($e); 9441 9442 $message = $info->message; 9443 if ($info->debuginfo) { 9444 $message .= "\n\n" . $info->debuginfo; 9445 } 9446 if ($info->backtrace) { 9447 $message .= "\n\n" . format_backtrace($info->backtrace, true); 9448 } 9449 9450 mtrace($message); 9451 } 9452 9453 /** 9454 * Replace 1 or more slashes or backslashes to 1 slash 9455 * 9456 * @param string $path The path to strip 9457 * @return string the path with double slashes removed 9458 */ 9459 function cleardoubleslashes ($path) { 9460 return preg_replace('/(\/|\\\){1,}/', '/', $path); 9461 } 9462 9463 /** 9464 * Is the current ip in a given list? 9465 * 9466 * @param string $list 9467 * @return bool 9468 */ 9469 function remoteip_in_list($list) { 9470 $clientip = getremoteaddr(null); 9471 9472 if (!$clientip) { 9473 // Ensure access on cli. 9474 return true; 9475 } 9476 return \core\ip_utils::is_ip_in_subnet_list($clientip, $list); 9477 } 9478 9479 /** 9480 * Returns most reliable client address 9481 * 9482 * @param string $default If an address can't be determined, then return this 9483 * @return string The remote IP address 9484 */ 9485 function getremoteaddr($default='0.0.0.0') { 9486 global $CFG; 9487 9488 if (!isset($CFG->getremoteaddrconf)) { 9489 // This will happen, for example, before just after the upgrade, as the 9490 // user is redirected to the admin screen. 9491 $variablestoskip = GETREMOTEADDR_SKIP_DEFAULT; 9492 } else { 9493 $variablestoskip = $CFG->getremoteaddrconf; 9494 } 9495 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) { 9496 if (!empty($_SERVER['HTTP_CLIENT_IP'])) { 9497 $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']); 9498 return $address ? $address : $default; 9499 } 9500 } 9501 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) { 9502 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { 9503 $forwardedaddresses = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']); 9504 9505 $forwardedaddresses = array_filter($forwardedaddresses, function($ip) { 9506 global $CFG; 9507 return !\core\ip_utils::is_ip_in_subnet_list($ip, $CFG->reverseproxyignore ?? '', ','); 9508 }); 9509 9510 // Multiple proxies can append values to this header including an 9511 // untrusted original request header so we must only trust the last ip. 9512 $address = end($forwardedaddresses); 9513 9514 if (substr_count($address, ":") > 1) { 9515 // Remove port and brackets from IPv6. 9516 if (preg_match("/\[(.*)\]:/", $address, $matches)) { 9517 $address = $matches[1]; 9518 } 9519 } else { 9520 // Remove port from IPv4. 9521 if (substr_count($address, ":") == 1) { 9522 $parts = explode(":", $address); 9523 $address = $parts[0]; 9524 } 9525 } 9526 9527 $address = cleanremoteaddr($address); 9528 return $address ? $address : $default; 9529 } 9530 } 9531 if (!empty($_SERVER['REMOTE_ADDR'])) { 9532 $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']); 9533 return $address ? $address : $default; 9534 } else { 9535 return $default; 9536 } 9537 } 9538 9539 /** 9540 * Cleans an ip address. Internal addresses are now allowed. 9541 * (Originally local addresses were not allowed.) 9542 * 9543 * @param string $addr IPv4 or IPv6 address 9544 * @param bool $compress use IPv6 address compression 9545 * @return string normalised ip address string, null if error 9546 */ 9547 function cleanremoteaddr($addr, $compress=false) { 9548 $addr = trim($addr); 9549 9550 if (strpos($addr, ':') !== false) { 9551 // Can be only IPv6. 9552 $parts = explode(':', $addr); 9553 $count = count($parts); 9554 9555 if (strpos($parts[$count-1], '.') !== false) { 9556 // Legacy ipv4 notation. 9557 $last = array_pop($parts); 9558 $ipv4 = cleanremoteaddr($last, true); 9559 if ($ipv4 === null) { 9560 return null; 9561 } 9562 $bits = explode('.', $ipv4); 9563 $parts[] = dechex($bits[0]).dechex($bits[1]); 9564 $parts[] = dechex($bits[2]).dechex($bits[3]); 9565 $count = count($parts); 9566 $addr = implode(':', $parts); 9567 } 9568 9569 if ($count < 3 or $count > 8) { 9570 return null; // Severly malformed. 9571 } 9572 9573 if ($count != 8) { 9574 if (strpos($addr, '::') === false) { 9575 return null; // Malformed. 9576 } 9577 // Uncompress. 9578 $insertat = array_search('', $parts, true); 9579 $missing = array_fill(0, 1 + 8 - $count, '0'); 9580 array_splice($parts, $insertat, 1, $missing); 9581 foreach ($parts as $key => $part) { 9582 if ($part === '') { 9583 $parts[$key] = '0'; 9584 } 9585 } 9586 } 9587 9588 $adr = implode(':', $parts); 9589 if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) { 9590 return null; // Incorrect format - sorry. 9591 } 9592 9593 // Normalise 0s and case. 9594 $parts = array_map('hexdec', $parts); 9595 $parts = array_map('dechex', $parts); 9596 9597 $result = implode(':', $parts); 9598 9599 if (!$compress) { 9600 return $result; 9601 } 9602 9603 if ($result === '0:0:0:0:0:0:0:0') { 9604 return '::'; // All addresses. 9605 } 9606 9607 $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1); 9608 if ($compressed !== $result) { 9609 return $compressed; 9610 } 9611 9612 $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1); 9613 if ($compressed !== $result) { 9614 return $compressed; 9615 } 9616 9617 $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1); 9618 if ($compressed !== $result) { 9619 return $compressed; 9620 } 9621 9622 return $result; 9623 } 9624 9625 // First get all things that look like IPv4 addresses. 9626 $parts = array(); 9627 if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) { 9628 return null; 9629 } 9630 unset($parts[0]); 9631 9632 foreach ($parts as $key => $match) { 9633 if ($match > 255) { 9634 return null; 9635 } 9636 $parts[$key] = (int)$match; // Normalise 0s. 9637 } 9638 9639 return implode('.', $parts); 9640 } 9641 9642 9643 /** 9644 * Is IP address a public address? 9645 * 9646 * @param string $ip The ip to check 9647 * @return bool true if the ip is public 9648 */ 9649 function ip_is_public($ip) { 9650 return (bool) filter_var($ip, FILTER_VALIDATE_IP, (FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)); 9651 } 9652 9653 /** 9654 * This function will make a complete copy of anything it's given, 9655 * regardless of whether it's an object or not. 9656 * 9657 * @param mixed $thing Something you want cloned 9658 * @return mixed What ever it is you passed it 9659 */ 9660 function fullclone($thing) { 9661 return unserialize(serialize($thing)); 9662 } 9663 9664 /** 9665 * Used to make sure that $min <= $value <= $max 9666 * 9667 * Make sure that value is between min, and max 9668 * 9669 * @param int $min The minimum value 9670 * @param int $value The value to check 9671 * @param int $max The maximum value 9672 * @return int 9673 */ 9674 function bounded_number($min, $value, $max) { 9675 if ($value < $min) { 9676 return $min; 9677 } 9678 if ($value > $max) { 9679 return $max; 9680 } 9681 return $value; 9682 } 9683 9684 /** 9685 * Check if there is a nested array within the passed array 9686 * 9687 * @param array $array 9688 * @return bool true if there is a nested array false otherwise 9689 */ 9690 function array_is_nested($array) { 9691 foreach ($array as $value) { 9692 if (is_array($value)) { 9693 return true; 9694 } 9695 } 9696 return false; 9697 } 9698 9699 /** 9700 * get_performance_info() pairs up with init_performance_info() 9701 * loaded in setup.php. Returns an array with 'html' and 'txt' 9702 * values ready for use, and each of the individual stats provided 9703 * separately as well. 9704 * 9705 * @return array 9706 */ 9707 function get_performance_info() { 9708 global $CFG, $PERF, $DB, $PAGE; 9709 9710 $info = array(); 9711 $info['txt'] = me() . ' '; // Holds log-friendly representation. 9712 9713 $info['html'] = ''; 9714 if (!empty($CFG->themedesignermode)) { 9715 // Attempt to avoid devs debugging peformance issues, when its caused by css building and so on. 9716 $info['html'] .= '<p><strong>Warning: Theme designer mode is enabled.</strong></p>'; 9717 } 9718 $info['html'] .= '<ul class="list-unstyled row mx-md-0">'; // Holds userfriendly HTML representation. 9719 9720 $info['realtime'] = microtime_diff($PERF->starttime, microtime()); 9721 9722 $info['html'] .= '<li class="timeused col-sm-4">'.$info['realtime'].' secs</li> '; 9723 $info['txt'] .= 'time: '.$info['realtime'].'s '; 9724 9725 // GET/POST (or NULL if $_SERVER['REQUEST_METHOD'] is undefined) is useful for txt logged information. 9726 $info['txt'] .= 'method: ' . ($_SERVER['REQUEST_METHOD'] ?? "NULL") . ' '; 9727 9728 if (function_exists('memory_get_usage')) { 9729 $info['memory_total'] = memory_get_usage(); 9730 $info['memory_growth'] = memory_get_usage() - $PERF->startmemory; 9731 $info['html'] .= '<li class="memoryused col-sm-4">RAM: '.display_size($info['memory_total']).'</li> '; 9732 $info['txt'] .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '. 9733 $info['memory_growth'].'B ('.display_size($info['memory_growth']).') '; 9734 } 9735 9736 if (function_exists('memory_get_peak_usage')) { 9737 $info['memory_peak'] = memory_get_peak_usage(); 9738 $info['html'] .= '<li class="memoryused col-sm-4">RAM peak: '.display_size($info['memory_peak']).'</li> '; 9739 $info['txt'] .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') '; 9740 } 9741 9742 $info['html'] .= '</ul><ul class="list-unstyled row mx-md-0">'; 9743 $inc = get_included_files(); 9744 $info['includecount'] = count($inc); 9745 $info['html'] .= '<li class="included col-sm-4">Included '.$info['includecount'].' files</li> '; 9746 $info['txt'] .= 'includecount: '.$info['includecount'].' '; 9747 9748 if (!empty($CFG->early_install_lang) or empty($PAGE)) { 9749 // We can not track more performance before installation or before PAGE init, sorry. 9750 return $info; 9751 } 9752 9753 $filtermanager = filter_manager::instance(); 9754 if (method_exists($filtermanager, 'get_performance_summary')) { 9755 list($filterinfo, $nicenames) = $filtermanager->get_performance_summary(); 9756 $info = array_merge($filterinfo, $info); 9757 foreach ($filterinfo as $key => $value) { 9758 $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> "; 9759 $info['txt'] .= "$key: $value "; 9760 } 9761 } 9762 9763 $stringmanager = get_string_manager(); 9764 if (method_exists($stringmanager, 'get_performance_summary')) { 9765 list($filterinfo, $nicenames) = $stringmanager->get_performance_summary(); 9766 $info = array_merge($filterinfo, $info); 9767 foreach ($filterinfo as $key => $value) { 9768 $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> "; 9769 $info['txt'] .= "$key: $value "; 9770 } 9771 } 9772 9773 $info['dbqueries'] = $DB->perf_get_reads().'/'.$DB->perf_get_writes(); 9774 $info['html'] .= '<li class="dbqueries col-sm-4">DB reads/writes: '.$info['dbqueries'].'</li> '; 9775 $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' '; 9776 9777 if ($DB->want_read_slave()) { 9778 $info['dbreads_slave'] = $DB->perf_get_reads_slave(); 9779 $info['html'] .= '<li class="dbqueries col-sm-4">DB reads from slave: '.$info['dbreads_slave'].'</li> '; 9780 $info['txt'] .= 'db reads from slave: '.$info['dbreads_slave'].' '; 9781 } 9782 9783 $info['dbtime'] = round($DB->perf_get_queries_time(), 5); 9784 $info['html'] .= '<li class="dbtime col-sm-4">DB queries time: '.$info['dbtime'].' secs</li> '; 9785 $info['txt'] .= 'db queries time: ' . $info['dbtime'] . 's '; 9786 9787 if (function_exists('posix_times')) { 9788 $ptimes = posix_times(); 9789 if (is_array($ptimes)) { 9790 foreach ($ptimes as $key => $val) { 9791 $info[$key] = $ptimes[$key] - $PERF->startposixtimes[$key]; 9792 } 9793 $info['html'] .= "<li class=\"posixtimes col-sm-4\">ticks: $info[ticks] user: $info[utime]"; 9794 $info['html'] .= "sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</li> "; 9795 $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] "; 9796 } 9797 } 9798 9799 // Grab the load average for the last minute. 9800 // /proc will only work under some linux configurations 9801 // while uptime is there under MacOSX/Darwin and other unices. 9802 if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) { 9803 list($serverload) = explode(' ', $loadavg[0]); 9804 unset($loadavg); 9805 } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) { 9806 if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) { 9807 $serverload = $matches[1]; 9808 } else { 9809 trigger_error('Could not parse uptime output!'); 9810 } 9811 } 9812 if (!empty($serverload)) { 9813 $info['serverload'] = $serverload; 9814 $info['html'] .= '<li class="serverload col-sm-4">Load average: '.$info['serverload'].'</li> '; 9815 $info['txt'] .= "serverload: {$info['serverload']} "; 9816 } 9817 9818 // Display size of session if session started. 9819 if ($si = \core\session\manager::get_performance_info()) { 9820 $info['sessionsize'] = $si['size']; 9821 $info['html'] .= "<li class=\"serverload col-sm-4\">" . $si['html'] . "</li>"; 9822 $info['txt'] .= $si['txt']; 9823 } 9824 9825 // Display time waiting for session if applicable. 9826 if (!empty($PERF->sessionlock['wait'])) { 9827 $sessionwait = number_format($PERF->sessionlock['wait'], 3) . ' secs'; 9828 $info['html'] .= html_writer::tag('li', 'Session wait: ' . $sessionwait, [ 9829 'class' => 'sessionwait col-sm-4' 9830 ]); 9831 $info['txt'] .= 'sessionwait: ' . $sessionwait . ' '; 9832 } 9833 9834 $info['html'] .= '</ul>'; 9835 $html = ''; 9836 if ($stats = cache_helper::get_stats()) { 9837 9838 $table = new html_table(); 9839 $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered'; 9840 $table->head = ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S', 'I/O']; 9841 $table->data = []; 9842 $table->align = ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right', 'right']; 9843 9844 $text = 'Caches used (hits/misses/sets): '; 9845 $hits = 0; 9846 $misses = 0; 9847 $sets = 0; 9848 $maxstores = 0; 9849 9850 // We want to align static caches into their own column. 9851 $hasstatic = false; 9852 foreach ($stats as $definition => $details) { 9853 $numstores = count($details['stores']); 9854 $first = key($details['stores']); 9855 if ($first !== cache_store::STATIC_ACCEL) { 9856 $numstores++; // Add a blank space for the missing static store. 9857 } 9858 $maxstores = max($maxstores, $numstores); 9859 } 9860 9861 $storec = 0; 9862 9863 while ($storec++ < ($maxstores - 2)) { 9864 if ($storec == ($maxstores - 2)) { 9865 $table->head[] = get_string('mappingfinal', 'cache'); 9866 } else { 9867 $table->head[] = "Store $storec"; 9868 } 9869 $table->align[] = 'left'; 9870 $table->align[] = 'right'; 9871 $table->align[] = 'right'; 9872 $table->align[] = 'right'; 9873 $table->align[] = 'right'; 9874 $table->head[] = 'H'; 9875 $table->head[] = 'M'; 9876 $table->head[] = 'S'; 9877 $table->head[] = 'I/O'; 9878 } 9879 9880 ksort($stats); 9881 9882 foreach ($stats as $definition => $details) { 9883 switch ($details['mode']) { 9884 case cache_store::MODE_APPLICATION: 9885 $modeclass = 'application'; 9886 $mode = ' <span title="application cache">App</span>'; 9887 break; 9888 case cache_store::MODE_SESSION: 9889 $modeclass = 'session'; 9890 $mode = ' <span title="session cache">Ses</span>'; 9891 break; 9892 case cache_store::MODE_REQUEST: 9893 $modeclass = 'request'; 9894 $mode = ' <span title="request cache">Req</span>'; 9895 break; 9896 } 9897 $row = [$mode, $definition]; 9898 9899 $text .= "$definition {"; 9900 9901 $storec = 0; 9902 foreach ($details['stores'] as $store => $data) { 9903 9904 if ($storec == 0 && $store !== cache_store::STATIC_ACCEL) { 9905 $row[] = ''; 9906 $row[] = ''; 9907 $row[] = ''; 9908 $storec++; 9909 } 9910 9911 $hits += $data['hits']; 9912 $misses += $data['misses']; 9913 $sets += $data['sets']; 9914 if ($data['hits'] == 0 and $data['misses'] > 0) { 9915 $cachestoreclass = 'nohits bg-danger'; 9916 } else if ($data['hits'] < $data['misses']) { 9917 $cachestoreclass = 'lowhits bg-warning text-dark'; 9918 } else { 9919 $cachestoreclass = 'hihits'; 9920 } 9921 $text .= "$store($data[hits]/$data[misses]/$data[sets]) "; 9922 $cell = new html_table_cell($store); 9923 $cell->attributes = ['class' => $cachestoreclass]; 9924 $row[] = $cell; 9925 $cell = new html_table_cell($data['hits']); 9926 $cell->attributes = ['class' => $cachestoreclass]; 9927 $row[] = $cell; 9928 $cell = new html_table_cell($data['misses']); 9929 $cell->attributes = ['class' => $cachestoreclass]; 9930 $row[] = $cell; 9931 9932 if ($store !== cache_store::STATIC_ACCEL) { 9933 // The static cache is never set. 9934 $cell = new html_table_cell($data['sets']); 9935 $cell->attributes = ['class' => $cachestoreclass]; 9936 $row[] = $cell; 9937 9938 if ($data['hits'] || $data['sets']) { 9939 if ($data['iobytes'] === cache_store::IO_BYTES_NOT_SUPPORTED) { 9940 $size = '-'; 9941 } else { 9942 $size = display_size($data['iobytes'], 1, 'KB'); 9943 if ($data['iobytes'] >= 10 * 1024) { 9944 $cachestoreclass = ' bg-warning text-dark'; 9945 } 9946 } 9947 } else { 9948 $size = ''; 9949 } 9950 $cell = new html_table_cell($size); 9951 $cell->attributes = ['class' => $cachestoreclass]; 9952 $row[] = $cell; 9953 } 9954 $storec++; 9955 } 9956 while ($storec++ < $maxstores) { 9957 $row[] = ''; 9958 $row[] = ''; 9959 $row[] = ''; 9960 $row[] = ''; 9961 $row[] = ''; 9962 } 9963 $text .= '} '; 9964 9965 $table->data[] = $row; 9966 } 9967 9968 $html .= html_writer::table($table); 9969 9970 // Now lets also show sub totals for each cache store. 9971 $storetotals = []; 9972 $storetotal = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0]; 9973 foreach ($stats as $definition => $details) { 9974 foreach ($details['stores'] as $store => $data) { 9975 if (!array_key_exists($store, $storetotals)) { 9976 $storetotals[$store] = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0]; 9977 } 9978 $storetotals[$store]['class'] = $data['class']; 9979 $storetotals[$store]['hits'] += $data['hits']; 9980 $storetotals[$store]['misses'] += $data['misses']; 9981 $storetotals[$store]['sets'] += $data['sets']; 9982 $storetotal['hits'] += $data['hits']; 9983 $storetotal['misses'] += $data['misses']; 9984 $storetotal['sets'] += $data['sets']; 9985 if ($data['iobytes'] !== cache_store::IO_BYTES_NOT_SUPPORTED) { 9986 $storetotals[$store]['iobytes'] += $data['iobytes']; 9987 $storetotal['iobytes'] += $data['iobytes']; 9988 } 9989 } 9990 } 9991 9992 $table = new html_table(); 9993 $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered'; 9994 $table->head = [get_string('storename', 'cache'), get_string('type_cachestore', 'plugin'), 'H', 'M', 'S', 'I/O']; 9995 $table->data = []; 9996 $table->align = ['left', 'left', 'right', 'right', 'right', 'right']; 9997 9998 ksort($storetotals); 9999 10000 foreach ($storetotals as $store => $data) { 10001 $row = []; 10002 if ($data['hits'] == 0 and $data['misses'] > 0) { 10003 $cachestoreclass = 'nohits bg-danger'; 10004 } else if ($data['hits'] < $data['misses']) { 10005 $cachestoreclass = 'lowhits bg-warning text-dark'; 10006 } else { 10007 $cachestoreclass = 'hihits'; 10008 } 10009 $cell = new html_table_cell($store); 10010 $cell->attributes = ['class' => $cachestoreclass]; 10011 $row[] = $cell; 10012 $cell = new html_table_cell($data['class']); 10013 $cell->attributes = ['class' => $cachestoreclass]; 10014 $row[] = $cell; 10015 $cell = new html_table_cell($data['hits']); 10016 $cell->attributes = ['class' => $cachestoreclass]; 10017 $row[] = $cell; 10018 $cell = new html_table_cell($data['misses']); 10019 $cell->attributes = ['class' => $cachestoreclass]; 10020 $row[] = $cell; 10021 $cell = new html_table_cell($data['sets']); 10022 $cell->attributes = ['class' => $cachestoreclass]; 10023 $row[] = $cell; 10024 if ($data['hits'] || $data['sets']) { 10025 if ($data['iobytes']) { 10026 $size = display_size($data['iobytes'], 1, 'KB'); 10027 } else { 10028 $size = '-'; 10029 } 10030 } else { 10031 $size = ''; 10032 } 10033 $cell = new html_table_cell($size); 10034 $cell->attributes = ['class' => $cachestoreclass]; 10035 $row[] = $cell; 10036 $table->data[] = $row; 10037 } 10038 if (!empty($storetotal['iobytes'])) { 10039 $size = display_size($storetotal['iobytes'], 1, 'KB'); 10040 } else if (!empty($storetotal['hits']) || !empty($storetotal['sets'])) { 10041 $size = '-'; 10042 } else { 10043 $size = ''; 10044 } 10045 $row = [ 10046 get_string('total'), 10047 '', 10048 $storetotal['hits'], 10049 $storetotal['misses'], 10050 $storetotal['sets'], 10051 $size, 10052 ]; 10053 $table->data[] = $row; 10054 10055 $html .= html_writer::table($table); 10056 10057 $info['cachesused'] = "$hits / $misses / $sets"; 10058 $info['html'] .= $html; 10059 $info['txt'] .= $text.'. '; 10060 } else { 10061 $info['cachesused'] = '0 / 0 / 0'; 10062 $info['html'] .= '<div class="cachesused">Caches used (hits/misses/sets): 0/0/0</div>'; 10063 $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 '; 10064 } 10065 10066 // Display lock information if any. 10067 if (!empty($PERF->locks)) { 10068 $table = new html_table(); 10069 $table->attributes['class'] = 'locktimings table table-dark table-sm w-auto table-bordered'; 10070 $table->head = ['Lock', 'Waited (s)', 'Obtained', 'Held for (s)']; 10071 $table->align = ['left', 'right', 'center', 'right']; 10072 $table->data = []; 10073 $text = 'Locks (waited/obtained/held):'; 10074 foreach ($PERF->locks as $locktiming) { 10075 $row = []; 10076 $row[] = s($locktiming->type . '/' . $locktiming->resource); 10077 $text .= ' ' . $locktiming->type . '/' . $locktiming->resource . ' ('; 10078 10079 // The time we had to wait to get the lock. 10080 $roundedtime = number_format($locktiming->wait, 1); 10081 $cell = new html_table_cell($roundedtime); 10082 if ($locktiming->wait > 0.5) { 10083 $cell->attributes = ['class' => 'bg-warning text-dark']; 10084 } 10085 $row[] = $cell; 10086 $text .= $roundedtime . '/'; 10087 10088 // Show a tick or cross for success. 10089 $row[] = $locktiming->success ? '✓' : '❌'; 10090 $text .= ($locktiming->success ? 'y' : 'n') . '/'; 10091 10092 // If applicable, show how long we held the lock before releasing it. 10093 if (property_exists($locktiming, 'held')) { 10094 $roundedtime = number_format($locktiming->held, 1); 10095 $cell = new html_table_cell($roundedtime); 10096 if ($locktiming->held > 0.5) { 10097 $cell->attributes = ['class' => 'bg-warning text-dark']; 10098 } 10099 $row[] = $cell; 10100 $text .= $roundedtime; 10101 } else { 10102 $row[] = '-'; 10103 $text .= '-'; 10104 } 10105 $text .= ')'; 10106 10107 $table->data[] = $row; 10108 } 10109 $info['html'] .= html_writer::table($table); 10110 $info['txt'] .= $text . '. '; 10111 } 10112 10113 $info['html'] = '<div class="performanceinfo siteinfo container-fluid px-md-0 overflow-auto pt-3">'.$info['html'].'</div>'; 10114 return $info; 10115 } 10116 10117 /** 10118 * Renames a file or directory to a unique name within the same directory. 10119 * 10120 * This function is designed to avoid any potential race conditions, and select an unused name. 10121 * 10122 * @param string $filepath Original filepath 10123 * @param string $prefix Prefix to use for the temporary name 10124 * @return string|bool New file path or false if failed 10125 * @since Moodle 3.10 10126 */ 10127 function rename_to_unused_name(string $filepath, string $prefix = '_temp_') { 10128 $dir = dirname($filepath); 10129 $basename = $dir . '/' . $prefix; 10130 $limit = 0; 10131 while ($limit < 100) { 10132 // Select a new name based on a random number. 10133 $newfilepath = $basename . md5(mt_rand()); 10134 10135 // Attempt a rename to that new name. 10136 if (@rename($filepath, $newfilepath)) { 10137 return $newfilepath; 10138 } 10139 10140 // The first time, do some sanity checks, maybe it is failing for a good reason and there 10141 // is no point trying 100 times if so. 10142 if ($limit === 0 && (!file_exists($filepath) || !is_writable($dir))) { 10143 return false; 10144 } 10145 $limit++; 10146 } 10147 return false; 10148 } 10149 10150 /** 10151 * Delete directory or only its content 10152 * 10153 * @param string $dir directory path 10154 * @param bool $contentonly 10155 * @return bool success, true also if dir does not exist 10156 */ 10157 function remove_dir($dir, $contentonly=false) { 10158 if (!is_dir($dir)) { 10159 // Nothing to do. 10160 return true; 10161 } 10162 10163 if (!$contentonly) { 10164 // Start by renaming the directory; this will guarantee that other processes don't write to it 10165 // while it is in the process of being deleted. 10166 $tempdir = rename_to_unused_name($dir); 10167 if ($tempdir) { 10168 // If the rename was successful then delete the $tempdir instead. 10169 $dir = $tempdir; 10170 } 10171 // If the rename fails, we will continue through and attempt to delete the directory 10172 // without renaming it since that is likely to at least delete most of the files. 10173 } 10174 10175 if (!$handle = opendir($dir)) { 10176 return false; 10177 } 10178 $result = true; 10179 while (false!==($item = readdir($handle))) { 10180 if ($item != '.' && $item != '..') { 10181 if (is_dir($dir.'/'.$item)) { 10182 $result = remove_dir($dir.'/'.$item) && $result; 10183 } else { 10184 $result = unlink($dir.'/'.$item) && $result; 10185 } 10186 } 10187 } 10188 closedir($handle); 10189 if ($contentonly) { 10190 clearstatcache(); // Make sure file stat cache is properly invalidated. 10191 return $result; 10192 } 10193 $result = rmdir($dir); // If anything left the result will be false, no need for && $result. 10194 clearstatcache(); // Make sure file stat cache is properly invalidated. 10195 return $result; 10196 } 10197 10198 /** 10199 * Detect if an object or a class contains a given property 10200 * will take an actual object or the name of a class 10201 * 10202 * @param mixed $obj Name of class or real object to test 10203 * @param string $property name of property to find 10204 * @return bool true if property exists 10205 */ 10206 function object_property_exists( $obj, $property ) { 10207 if (is_string( $obj )) { 10208 $properties = get_class_vars( $obj ); 10209 } else { 10210 $properties = get_object_vars( $obj ); 10211 } 10212 return array_key_exists( $property, $properties ); 10213 } 10214 10215 /** 10216 * Converts an object into an associative array 10217 * 10218 * This function converts an object into an associative array by iterating 10219 * over its public properties. Because this function uses the foreach 10220 * construct, Iterators are respected. It works recursively on arrays of objects. 10221 * Arrays and simple values are returned as is. 10222 * 10223 * If class has magic properties, it can implement IteratorAggregate 10224 * and return all available properties in getIterator() 10225 * 10226 * @param mixed $var 10227 * @return array 10228 */ 10229 function convert_to_array($var) { 10230 $result = array(); 10231 10232 // Loop over elements/properties. 10233 foreach ($var as $key => $value) { 10234 // Recursively convert objects. 10235 if (is_object($value) || is_array($value)) { 10236 $result[$key] = convert_to_array($value); 10237 } else { 10238 // Simple values are untouched. 10239 $result[$key] = $value; 10240 } 10241 } 10242 return $result; 10243 } 10244 10245 /** 10246 * Detect a custom script replacement in the data directory that will 10247 * replace an existing moodle script 10248 * 10249 * @return string|bool full path name if a custom script exists, false if no custom script exists 10250 */ 10251 function custom_script_path() { 10252 global $CFG, $SCRIPT; 10253 10254 if ($SCRIPT === null) { 10255 // Probably some weird external script. 10256 return false; 10257 } 10258 10259 $scriptpath = $CFG->customscripts . $SCRIPT; 10260 10261 // Check the custom script exists. 10262 if (file_exists($scriptpath) and is_file($scriptpath)) { 10263 return $scriptpath; 10264 } else { 10265 return false; 10266 } 10267 } 10268 10269 /** 10270 * Returns whether or not the user object is a remote MNET user. This function 10271 * is in moodlelib because it does not rely on loading any of the MNET code. 10272 * 10273 * @param object $user A valid user object 10274 * @return bool True if the user is from a remote Moodle. 10275 */ 10276 function is_mnet_remote_user($user) { 10277 global $CFG; 10278 10279 if (!isset($CFG->mnet_localhost_id)) { 10280 include_once($CFG->dirroot . '/mnet/lib.php'); 10281 $env = new mnet_environment(); 10282 $env->init(); 10283 unset($env); 10284 } 10285 10286 return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id); 10287 } 10288 10289 /** 10290 * This function will search for browser prefereed languages, setting Moodle 10291 * to use the best one available if $SESSION->lang is undefined 10292 */ 10293 function setup_lang_from_browser() { 10294 global $CFG, $SESSION, $USER; 10295 10296 if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) { 10297 // Lang is defined in session or user profile, nothing to do. 10298 return; 10299 } 10300 10301 if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do. 10302 return; 10303 } 10304 10305 // Extract and clean langs from headers. 10306 $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE']; 10307 $rawlangs = str_replace('-', '_', $rawlangs); // We are using underscores. 10308 $rawlangs = explode(',', $rawlangs); // Convert to array. 10309 $langs = array(); 10310 10311 $order = 1.0; 10312 foreach ($rawlangs as $lang) { 10313 if (strpos($lang, ';') === false) { 10314 $langs[(string)$order] = $lang; 10315 $order = $order-0.01; 10316 } else { 10317 $parts = explode(';', $lang); 10318 $pos = strpos($parts[1], '='); 10319 $langs[substr($parts[1], $pos+1)] = $parts[0]; 10320 } 10321 } 10322 krsort($langs, SORT_NUMERIC); 10323 10324 // Look for such langs under standard locations. 10325 foreach ($langs as $lang) { 10326 // Clean it properly for include. 10327 $lang = strtolower(clean_param($lang, PARAM_SAFEDIR)); 10328 if (get_string_manager()->translation_exists($lang, false)) { 10329 // If the translation for this language exists then try to set it 10330 // for the rest of the session, if this is a read only session then 10331 // we can only set it temporarily in $CFG. 10332 if (defined('READ_ONLY_SESSION') && !empty($CFG->enable_read_only_sessions)) { 10333 $CFG->lang = $lang; 10334 } else { 10335 $SESSION->lang = $lang; 10336 } 10337 // We have finished. Go out. 10338 break; 10339 } 10340 } 10341 return; 10342 } 10343 10344 /** 10345 * Check if $url matches anything in proxybypass list 10346 * 10347 * Any errors just result in the proxy being used (least bad) 10348 * 10349 * @param string $url url to check 10350 * @return boolean true if we should bypass the proxy 10351 */ 10352 function is_proxybypass( $url ) { 10353 global $CFG; 10354 10355 // Sanity check. 10356 if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) { 10357 return false; 10358 } 10359 10360 // Get the host part out of the url. 10361 if (!$host = parse_url( $url, PHP_URL_HOST )) { 10362 return false; 10363 } 10364 10365 // Get the possible bypass hosts into an array. 10366 $matches = explode( ',', $CFG->proxybypass ); 10367 10368 // Check for a exact match on the IP or in the domains. 10369 $isdomaininallowedlist = \core\ip_utils::is_domain_in_allowed_list($host, $matches); 10370 $isipinsubnetlist = \core\ip_utils::is_ip_in_subnet_list($host, $CFG->proxybypass, ','); 10371 10372 if ($isdomaininallowedlist || $isipinsubnetlist) { 10373 return true; 10374 } 10375 10376 // Nothing matched. 10377 return false; 10378 } 10379 10380 /** 10381 * Check if the passed navigation is of the new style 10382 * 10383 * @param mixed $navigation 10384 * @return bool true for yes false for no 10385 */ 10386 function is_newnav($navigation) { 10387 if (is_array($navigation) && !empty($navigation['newnav'])) { 10388 return true; 10389 } else { 10390 return false; 10391 } 10392 } 10393 10394 /** 10395 * Checks whether the given variable name is defined as a variable within the given object. 10396 * 10397 * This will NOT work with stdClass objects, which have no class variables. 10398 * 10399 * @param string $var The variable name 10400 * @param object $object The object to check 10401 * @return boolean 10402 */ 10403 function in_object_vars($var, $object) { 10404 $classvars = get_class_vars(get_class($object)); 10405 $classvars = array_keys($classvars); 10406 return in_array($var, $classvars); 10407 } 10408 10409 /** 10410 * Returns an array without repeated objects. 10411 * This function is similar to array_unique, but for arrays that have objects as values 10412 * 10413 * @param array $array 10414 * @param bool $keepkeyassoc 10415 * @return array 10416 */ 10417 function object_array_unique($array, $keepkeyassoc = true) { 10418 $duplicatekeys = array(); 10419 $tmp = array(); 10420 10421 foreach ($array as $key => $val) { 10422 // Convert objects to arrays, in_array() does not support objects. 10423 if (is_object($val)) { 10424 $val = (array)$val; 10425 } 10426 10427 if (!in_array($val, $tmp)) { 10428 $tmp[] = $val; 10429 } else { 10430 $duplicatekeys[] = $key; 10431 } 10432 } 10433 10434 foreach ($duplicatekeys as $key) { 10435 unset($array[$key]); 10436 } 10437 10438 return $keepkeyassoc ? $array : array_values($array); 10439 } 10440 10441 /** 10442 * Is a userid the primary administrator? 10443 * 10444 * @param int $userid int id of user to check 10445 * @return boolean 10446 */ 10447 function is_primary_admin($userid) { 10448 $primaryadmin = get_admin(); 10449 10450 if ($userid == $primaryadmin->id) { 10451 return true; 10452 } else { 10453 return false; 10454 } 10455 } 10456 10457 /** 10458 * Returns the site identifier 10459 * 10460 * @return string $CFG->siteidentifier, first making sure it is properly initialised. 10461 */ 10462 function get_site_identifier() { 10463 global $CFG; 10464 // Check to see if it is missing. If so, initialise it. 10465 if (empty($CFG->siteidentifier)) { 10466 set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']); 10467 } 10468 // Return it. 10469 return $CFG->siteidentifier; 10470 } 10471 10472 /** 10473 * Check whether the given password has no more than the specified 10474 * number of consecutive identical characters. 10475 * 10476 * @param string $password password to be checked against the password policy 10477 * @param integer $maxchars maximum number of consecutive identical characters 10478 * @return bool 10479 */ 10480 function check_consecutive_identical_characters($password, $maxchars) { 10481 10482 if ($maxchars < 1) { 10483 return true; // Zero 0 is to disable this check. 10484 } 10485 if (strlen($password) <= $maxchars) { 10486 return true; // Too short to fail this test. 10487 } 10488 10489 $previouschar = ''; 10490 $consecutivecount = 1; 10491 foreach (str_split($password) as $char) { 10492 if ($char != $previouschar) { 10493 $consecutivecount = 1; 10494 } else { 10495 $consecutivecount++; 10496 if ($consecutivecount > $maxchars) { 10497 return false; // Check failed already. 10498 } 10499 } 10500 10501 $previouschar = $char; 10502 } 10503 10504 return true; 10505 } 10506 10507 /** 10508 * Helper function to do partial function binding. 10509 * so we can use it for preg_replace_callback, for example 10510 * this works with php functions, user functions, static methods and class methods 10511 * it returns you a callback that you can pass on like so: 10512 * 10513 * $callback = partial('somefunction', $arg1, $arg2); 10514 * or 10515 * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2); 10516 * or even 10517 * $obj = new someclass(); 10518 * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2); 10519 * 10520 * and then the arguments that are passed through at calltime are appended to the argument list. 10521 * 10522 * @param mixed $function a php callback 10523 * @param mixed $arg1,... $argv arguments to partially bind with 10524 * @return array Array callback 10525 */ 10526 function partial() { 10527 if (!class_exists('partial')) { 10528 /** 10529 * Used to manage function binding. 10530 * @copyright 2009 Penny Leach 10531 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 10532 */ 10533 class partial{ 10534 /** @var array */ 10535 public $values = array(); 10536 /** @var string The function to call as a callback. */ 10537 public $func; 10538 /** 10539 * Constructor 10540 * @param string $func 10541 * @param array $args 10542 */ 10543 public function __construct($func, $args) { 10544 $this->values = $args; 10545 $this->func = $func; 10546 } 10547 /** 10548 * Calls the callback function. 10549 * @return mixed 10550 */ 10551 public function method() { 10552 $args = func_get_args(); 10553 return call_user_func_array($this->func, array_merge($this->values, $args)); 10554 } 10555 } 10556 } 10557 $args = func_get_args(); 10558 $func = array_shift($args); 10559 $p = new partial($func, $args); 10560 return array($p, 'method'); 10561 } 10562 10563 /** 10564 * helper function to load up and initialise the mnet environment 10565 * this must be called before you use mnet functions. 10566 * 10567 * @return mnet_environment the equivalent of old $MNET global 10568 */ 10569 function get_mnet_environment() { 10570 global $CFG; 10571 require_once($CFG->dirroot . '/mnet/lib.php'); 10572 static $instance = null; 10573 if (empty($instance)) { 10574 $instance = new mnet_environment(); 10575 $instance->init(); 10576 } 10577 return $instance; 10578 } 10579 10580 /** 10581 * during xmlrpc server code execution, any code wishing to access 10582 * information about the remote peer must use this to get it. 10583 * 10584 * @return mnet_remote_client the equivalent of old $MNETREMOTE_CLIENT global 10585 */ 10586 function get_mnet_remote_client() { 10587 if (!defined('MNET_SERVER')) { 10588 debugging(get_string('notinxmlrpcserver', 'mnet')); 10589 return false; 10590 } 10591 global $MNET_REMOTE_CLIENT; 10592 if (isset($MNET_REMOTE_CLIENT)) { 10593 return $MNET_REMOTE_CLIENT; 10594 } 10595 return false; 10596 } 10597 10598 /** 10599 * during the xmlrpc server code execution, this will be called 10600 * to setup the object returned by {@link get_mnet_remote_client} 10601 * 10602 * @param mnet_remote_client $client the client to set up 10603 * @throws moodle_exception 10604 */ 10605 function set_mnet_remote_client($client) { 10606 if (!defined('MNET_SERVER')) { 10607 throw new moodle_exception('notinxmlrpcserver', 'mnet'); 10608 } 10609 global $MNET_REMOTE_CLIENT; 10610 $MNET_REMOTE_CLIENT = $client; 10611 } 10612 10613 /** 10614 * return the jump url for a given remote user 10615 * this is used for rewriting forum post links in emails, etc 10616 * 10617 * @param stdclass $user the user to get the idp url for 10618 */ 10619 function mnet_get_idp_jump_url($user) { 10620 global $CFG; 10621 10622 static $mnetjumps = array(); 10623 if (!array_key_exists($user->mnethostid, $mnetjumps)) { 10624 $idp = mnet_get_peer_host($user->mnethostid); 10625 $idpjumppath = mnet_get_app_jumppath($idp->applicationid); 10626 $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl='; 10627 } 10628 return $mnetjumps[$user->mnethostid]; 10629 } 10630 10631 /** 10632 * Gets the homepage to use for the current user 10633 * 10634 * @return int One of HOMEPAGE_* 10635 */ 10636 function get_home_page() { 10637 global $CFG; 10638 10639 if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) { 10640 // If dashboard is disabled, home will be set to default page. 10641 $defaultpage = get_default_home_page(); 10642 if ($CFG->defaulthomepage == HOMEPAGE_MY) { 10643 if (!empty($CFG->enabledashboard)) { 10644 return HOMEPAGE_MY; 10645 } else { 10646 return $defaultpage; 10647 } 10648 } else if ($CFG->defaulthomepage == HOMEPAGE_MYCOURSES) { 10649 return HOMEPAGE_MYCOURSES; 10650 } else { 10651 $userhomepage = (int) get_user_preferences('user_home_page_preference', $defaultpage); 10652 if (empty($CFG->enabledashboard) && $userhomepage == HOMEPAGE_MY) { 10653 // If the user was using the dashboard but it's disabled, return the default home page. 10654 $userhomepage = $defaultpage; 10655 } 10656 return $userhomepage; 10657 } 10658 } 10659 return HOMEPAGE_SITE; 10660 } 10661 10662 /** 10663 * Returns the default home page to display if current one is not defined or can't be applied. 10664 * The default behaviour is to return Dashboard if it's enabled or My courses page if it isn't. 10665 * 10666 * @return int The default home page. 10667 */ 10668 function get_default_home_page(): int { 10669 global $CFG; 10670 10671 return (!isset($CFG->enabledashboard) || $CFG->enabledashboard) ? HOMEPAGE_MY : HOMEPAGE_MYCOURSES; 10672 } 10673 10674 /** 10675 * Gets the name of a course to be displayed when showing a list of courses. 10676 * By default this is just $course->fullname but user can configure it. The 10677 * result of this function should be passed through print_string. 10678 * @param stdClass|core_course_list_element $course Moodle course object 10679 * @return string Display name of course (either fullname or short + fullname) 10680 */ 10681 function get_course_display_name_for_list($course) { 10682 global $CFG; 10683 if (!empty($CFG->courselistshortnames)) { 10684 if (!($course instanceof stdClass)) { 10685 $course = (object)convert_to_array($course); 10686 } 10687 return get_string('courseextendednamedisplay', '', $course); 10688 } else { 10689 return $course->fullname; 10690 } 10691 } 10692 10693 /** 10694 * Safe analogue of unserialize() that can only parse arrays 10695 * 10696 * Arrays may contain only integers or strings as both keys and values. Nested arrays are allowed. 10697 * 10698 * @param string $expression 10699 * @return array|bool either parsed array or false if parsing was impossible. 10700 */ 10701 function unserialize_array($expression) { 10702 10703 // Check the expression is an array. 10704 if (!preg_match('/^a:(\d+):/', $expression)) { 10705 return false; 10706 } 10707 10708 $values = (array) unserialize_object($expression); 10709 10710 // Callback that returns true if the given value is an unserialized object, executes recursively. 10711 $invalidvaluecallback = static function($value) use (&$invalidvaluecallback): bool { 10712 if (is_array($value)) { 10713 return (bool) array_filter($value, $invalidvaluecallback); 10714 } 10715 return ($value instanceof stdClass) || ($value instanceof __PHP_Incomplete_Class); 10716 }; 10717 10718 // Iterate over the result to ensure there are no stray objects. 10719 if (array_filter($values, $invalidvaluecallback)) { 10720 return false; 10721 } 10722 10723 return $values; 10724 } 10725 10726 /** 10727 * Safe method for unserializing given input that is expected to contain only a serialized instance of an stdClass object 10728 * 10729 * If any class type other than stdClass is included in the input string, it will not be instantiated and will be cast to an 10730 * stdClass object. The initial cast to array, then back to object is to ensure we are always returning the correct type, 10731 * otherwise we would return an instances of {@see __PHP_Incomplete_class} for malformed strings 10732 * 10733 * @param string $input 10734 * @return stdClass 10735 */ 10736 function unserialize_object(string $input): stdClass { 10737 $instance = (array) unserialize($input, ['allowed_classes' => [stdClass::class]]); 10738 return (object) $instance; 10739 } 10740 10741 /** 10742 * The lang_string class 10743 * 10744 * This special class is used to create an object representation of a string request. 10745 * It is special because processing doesn't occur until the object is first used. 10746 * The class was created especially to aid performance in areas where strings were 10747 * required to be generated but were not necessarily used. 10748 * As an example the admin tree when generated uses over 1500 strings, of which 10749 * normally only 1/3 are ever actually printed at any time. 10750 * The performance advantage is achieved by not actually processing strings that 10751 * arn't being used, as such reducing the processing required for the page. 10752 * 10753 * How to use the lang_string class? 10754 * There are two methods of using the lang_string class, first through the 10755 * forth argument of the get_string function, and secondly directly. 10756 * The following are examples of both. 10757 * 1. Through get_string calls e.g. 10758 * $string = get_string($identifier, $component, $a, true); 10759 * $string = get_string('yes', 'moodle', null, true); 10760 * 2. Direct instantiation 10761 * $string = new lang_string($identifier, $component, $a, $lang); 10762 * $string = new lang_string('yes'); 10763 * 10764 * How do I use a lang_string object? 10765 * The lang_string object makes use of a magic __toString method so that you 10766 * are able to use the object exactly as you would use a string in most cases. 10767 * This means you are able to collect it into a variable and then directly 10768 * echo it, or concatenate it into another string, or similar. 10769 * The other thing you can do is manually get the string by calling the 10770 * lang_strings out method e.g. 10771 * $string = new lang_string('yes'); 10772 * $string->out(); 10773 * Also worth noting is that the out method can take one argument, $lang which 10774 * allows the developer to change the language on the fly. 10775 * 10776 * When should I use a lang_string object? 10777 * The lang_string object is designed to be used in any situation where a 10778 * string may not be needed, but needs to be generated. 10779 * The admin tree is a good example of where lang_string objects should be 10780 * used. 10781 * A more practical example would be any class that requries strings that may 10782 * not be printed (after all classes get renderer by renderers and who knows 10783 * what they will do ;)) 10784 * 10785 * When should I not use a lang_string object? 10786 * Don't use lang_strings when you are going to use a string immediately. 10787 * There is no need as it will be processed immediately and there will be no 10788 * advantage, and in fact perhaps a negative hit as a class has to be 10789 * instantiated for a lang_string object, however get_string won't require 10790 * that. 10791 * 10792 * Limitations: 10793 * 1. You cannot use a lang_string object as an array offset. Doing so will 10794 * result in PHP throwing an error. (You can use it as an object property!) 10795 * 10796 * @package core 10797 * @category string 10798 * @copyright 2011 Sam Hemelryk 10799 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 10800 */ 10801 class lang_string { 10802 10803 /** @var string The strings identifier */ 10804 protected $identifier; 10805 /** @var string The strings component. Default '' */ 10806 protected $component = ''; 10807 /** @var array|stdClass Any arguments required for the string. Default null */ 10808 protected $a = null; 10809 /** @var string The language to use when processing the string. Default null */ 10810 protected $lang = null; 10811 10812 /** @var string The processed string (once processed) */ 10813 protected $string = null; 10814 10815 /** 10816 * A special boolean. If set to true then the object has been woken up and 10817 * cannot be regenerated. If this is set then $this->string MUST be used. 10818 * @var bool 10819 */ 10820 protected $forcedstring = false; 10821 10822 /** 10823 * Constructs a lang_string object 10824 * 10825 * This function should do as little processing as possible to ensure the best 10826 * performance for strings that won't be used. 10827 * 10828 * @param string $identifier The strings identifier 10829 * @param string $component The strings component 10830 * @param stdClass|array|mixed $a Any arguments the string requires 10831 * @param string $lang The language to use when processing the string. 10832 * @throws coding_exception 10833 */ 10834 public function __construct($identifier, $component = '', $a = null, $lang = null) { 10835 if (empty($component)) { 10836 $component = 'moodle'; 10837 } 10838 10839 $this->identifier = $identifier; 10840 $this->component = $component; 10841 $this->lang = $lang; 10842 10843 // We MUST duplicate $a to ensure that it if it changes by reference those 10844 // changes are not carried across. 10845 // To do this we always ensure $a or its properties/values are strings 10846 // and that any properties/values that arn't convertable are forgotten. 10847 if ($a !== null) { 10848 if (is_scalar($a)) { 10849 $this->a = $a; 10850 } else if ($a instanceof lang_string) { 10851 $this->a = $a->out(); 10852 } else if (is_object($a) or is_array($a)) { 10853 $a = (array)$a; 10854 $this->a = array(); 10855 foreach ($a as $key => $value) { 10856 // Make sure conversion errors don't get displayed (results in ''). 10857 if (is_array($value)) { 10858 $this->a[$key] = ''; 10859 } else if (is_object($value)) { 10860 if (method_exists($value, '__toString')) { 10861 $this->a[$key] = $value->__toString(); 10862 } else { 10863 $this->a[$key] = ''; 10864 } 10865 } else { 10866 $this->a[$key] = (string)$value; 10867 } 10868 } 10869 } 10870 } 10871 10872 if (debugging(false, DEBUG_DEVELOPER)) { 10873 if (clean_param($this->identifier, PARAM_STRINGID) == '') { 10874 throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition'); 10875 } 10876 if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') { 10877 throw new coding_exception('Invalid string compontent. Please check your string definition'); 10878 } 10879 if (!get_string_manager()->string_exists($this->identifier, $this->component)) { 10880 debugging('String does not exist. Please check your string definition for '.$this->identifier.'/'.$this->component, DEBUG_DEVELOPER); 10881 } 10882 } 10883 } 10884 10885 /** 10886 * Processes the string. 10887 * 10888 * This function actually processes the string, stores it in the string property 10889 * and then returns it. 10890 * You will notice that this function is VERY similar to the get_string method. 10891 * That is because it is pretty much doing the same thing. 10892 * However as this function is an upgrade it isn't as tolerant to backwards 10893 * compatibility. 10894 * 10895 * @return string 10896 * @throws coding_exception 10897 */ 10898 protected function get_string() { 10899 global $CFG; 10900 10901 // Check if we need to process the string. 10902 if ($this->string === null) { 10903 // Check the quality of the identifier. 10904 if ($CFG->debugdeveloper && clean_param($this->identifier, PARAM_STRINGID) === '') { 10905 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); 10906 } 10907 10908 // Process the string. 10909 $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang); 10910 // Debugging feature lets you display string identifier and component. 10911 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) { 10912 $this->string .= ' {' . $this->identifier . '/' . $this->component . '}'; 10913 } 10914 } 10915 // Return the string. 10916 return $this->string; 10917 } 10918 10919 /** 10920 * Returns the string 10921 * 10922 * @param string $lang The langauge to use when processing the string 10923 * @return string 10924 */ 10925 public function out($lang = null) { 10926 if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) { 10927 if ($this->forcedstring) { 10928 debugging('lang_string objects that have been used cannot be printed in another language. ('.$this->lang.' used)', DEBUG_DEVELOPER); 10929 return $this->get_string(); 10930 } 10931 $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang); 10932 return $translatedstring->out(); 10933 } 10934 return $this->get_string(); 10935 } 10936 10937 /** 10938 * Magic __toString method for printing a string 10939 * 10940 * @return string 10941 */ 10942 public function __toString() { 10943 return $this->get_string(); 10944 } 10945 10946 /** 10947 * Magic __set_state method used for var_export 10948 * 10949 * @param array $array 10950 * @return self 10951 */ 10952 public static function __set_state(array $array): self { 10953 $tmp = new lang_string($array['identifier'], $array['component'], $array['a'], $array['lang']); 10954 $tmp->string = $array['string']; 10955 $tmp->forcedstring = $array['forcedstring']; 10956 return $tmp; 10957 } 10958 10959 /** 10960 * Prepares the lang_string for sleep and stores only the forcedstring and 10961 * string properties... the string cannot be regenerated so we need to ensure 10962 * it is generated for this. 10963 * 10964 * @return string 10965 */ 10966 public function __sleep() { 10967 $this->get_string(); 10968 $this->forcedstring = true; 10969 return array('forcedstring', 'string', 'lang'); 10970 } 10971 10972 /** 10973 * Returns the identifier. 10974 * 10975 * @return string 10976 */ 10977 public function get_identifier() { 10978 return $this->identifier; 10979 } 10980 10981 /** 10982 * Returns the component. 10983 * 10984 * @return string 10985 */ 10986 public function get_component() { 10987 return $this->component; 10988 } 10989 } 10990 10991 /** 10992 * Get human readable name describing the given callable. 10993 * 10994 * This performs syntax check only to see if the given param looks like a valid function, method or closure. 10995 * It does not check if the callable actually exists. 10996 * 10997 * @param callable|string|array $callable 10998 * @return string|bool Human readable name of callable, or false if not a valid callable. 10999 */ 11000 function get_callable_name($callable) { 11001 11002 if (!is_callable($callable, true, $name)) { 11003 return false; 11004 11005 } else { 11006 return $name; 11007 } 11008 } 11009 11010 /** 11011 * Tries to guess if $CFG->wwwroot is publicly accessible or not. 11012 * Never put your faith on this function and rely on its accuracy as there might be false positives. 11013 * It just performs some simple checks, and mainly is used for places where we want to hide some options 11014 * such as site registration when $CFG->wwwroot is not publicly accessible. 11015 * Good thing is there is no false negative. 11016 * Note that it's possible to force the result of this check by specifying $CFG->site_is_public in config.php 11017 * 11018 * @return bool 11019 */ 11020 function site_is_public() { 11021 global $CFG; 11022 11023 // Return early if site admin has forced this setting. 11024 if (isset($CFG->site_is_public)) { 11025 return (bool)$CFG->site_is_public; 11026 } 11027 11028 $host = parse_url($CFG->wwwroot, PHP_URL_HOST); 11029 11030 if ($host === 'localhost' || preg_match('|^127\.\d+\.\d+\.\d+$|', $host)) { 11031 $ispublic = false; 11032 } else if (\core\ip_utils::is_ip_address($host) && !ip_is_public($host)) { 11033 $ispublic = false; 11034 } else if (($address = \core\ip_utils::get_ip_address($host)) && !ip_is_public($address)) { 11035 $ispublic = false; 11036 } else { 11037 $ispublic = true; 11038 } 11039 11040 return $ispublic; 11041 } 11042 11043 /** 11044 * Validates user's password length. 11045 * 11046 * @param string $password 11047 * @param int $pepperlength The length of the used peppers 11048 * @return bool 11049 */ 11050 function exceeds_password_length(string $password, int $pepperlength = 0): bool { 11051 return (strlen($password) > (MAX_PASSWORD_CHARACTERS + $pepperlength)); 11052 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body