See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * moodlelib.php - Moodle main library 19 * 20 * Main library file of miscellaneous general-purpose Moodle functions. 21 * Other main libraries: 22 * - weblib.php - functions that produce web output 23 * - datalib.php - functions that access the database 24 * 25 * @package core 26 * @subpackage lib 27 * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com 28 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 29 */ 30 31 defined('MOODLE_INTERNAL') || die(); 32 33 // CONSTANTS (Encased in phpdoc proper comments). 34 35 // Date and time constants. 36 /** 37 * Time constant - the number of seconds in a year 38 */ 39 define('YEARSECS', 31536000); 40 41 /** 42 * Time constant - the number of seconds in a week 43 */ 44 define('WEEKSECS', 604800); 45 46 /** 47 * Time constant - the number of seconds in a day 48 */ 49 define('DAYSECS', 86400); 50 51 /** 52 * Time constant - the number of seconds in an hour 53 */ 54 define('HOURSECS', 3600); 55 56 /** 57 * Time constant - the number of seconds in a minute 58 */ 59 define('MINSECS', 60); 60 61 /** 62 * Time constant - the number of minutes in a day 63 */ 64 define('DAYMINS', 1440); 65 66 /** 67 * Time constant - the number of minutes in an hour 68 */ 69 define('HOURMINS', 60); 70 71 // Parameter constants - every call to optional_param(), required_param() 72 // or clean_param() should have a specified type of parameter. 73 74 /** 75 * PARAM_ALPHA - contains only English ascii letters [a-zA-Z]. 76 */ 77 define('PARAM_ALPHA', 'alpha'); 78 79 /** 80 * PARAM_ALPHAEXT the same contents as PARAM_ALPHA (English ascii letters [a-zA-Z]) plus the chars in quotes: "_-" allowed 81 * NOTE: originally this allowed "/" too, please use PARAM_SAFEPATH if "/" needed 82 */ 83 define('PARAM_ALPHAEXT', 'alphaext'); 84 85 /** 86 * PARAM_ALPHANUM - expected numbers 0-9 and English ascii letters [a-zA-Z] only. 87 */ 88 define('PARAM_ALPHANUM', 'alphanum'); 89 90 /** 91 * PARAM_ALPHANUMEXT - expected numbers 0-9, letters (English ascii letters [a-zA-Z]) and _- only. 92 */ 93 define('PARAM_ALPHANUMEXT', 'alphanumext'); 94 95 /** 96 * PARAM_AUTH - actually checks to make sure the string is a valid auth plugin 97 */ 98 define('PARAM_AUTH', 'auth'); 99 100 /** 101 * PARAM_BASE64 - Base 64 encoded format 102 */ 103 define('PARAM_BASE64', 'base64'); 104 105 /** 106 * PARAM_BOOL - converts input into 0 or 1, use for switches in forms and urls. 107 */ 108 define('PARAM_BOOL', 'bool'); 109 110 /** 111 * PARAM_CAPABILITY - A capability name, like 'moodle/role:manage'. Actually 112 * checked against the list of capabilities in the database. 113 */ 114 define('PARAM_CAPABILITY', 'capability'); 115 116 /** 117 * PARAM_CLEANHTML - cleans submitted HTML code. Note that you almost never want 118 * to use this. The normal mode of operation is to use PARAM_RAW when receiving 119 * the input (required/optional_param or formslib) and then sanitise the HTML 120 * using format_text on output. This is for the rare cases when you want to 121 * sanitise the HTML on input. This cleaning may also fix xhtml strictness. 122 */ 123 define('PARAM_CLEANHTML', 'cleanhtml'); 124 125 /** 126 * PARAM_EMAIL - an email address following the RFC 127 */ 128 define('PARAM_EMAIL', 'email'); 129 130 /** 131 * PARAM_FILE - safe file name, all dangerous chars are stripped, protects against XSS, SQL injections and directory traversals 132 */ 133 define('PARAM_FILE', 'file'); 134 135 /** 136 * PARAM_FLOAT - a real/floating point number. 137 * 138 * Note that you should not use PARAM_FLOAT for numbers typed in by the user. 139 * It does not work for languages that use , as a decimal separator. 140 * Use PARAM_LOCALISEDFLOAT instead. 141 */ 142 define('PARAM_FLOAT', 'float'); 143 144 /** 145 * PARAM_LOCALISEDFLOAT - a localised real/floating point number. 146 * This is preferred over PARAM_FLOAT for numbers typed in by the user. 147 * Cleans localised numbers to computer readable numbers; false for invalid numbers. 148 */ 149 define('PARAM_LOCALISEDFLOAT', 'localisedfloat'); 150 151 /** 152 * PARAM_HOST - expected fully qualified domain name (FQDN) or an IPv4 dotted quad (IP address) 153 */ 154 define('PARAM_HOST', 'host'); 155 156 /** 157 * PARAM_INT - integers only, use when expecting only numbers. 158 */ 159 define('PARAM_INT', 'int'); 160 161 /** 162 * PARAM_LANG - checks to see if the string is a valid installed language in the current site. 163 */ 164 define('PARAM_LANG', 'lang'); 165 166 /** 167 * PARAM_LOCALURL - expected properly formatted URL as well as one that refers to the local server itself. (NOT orthogonal to the 168 * others! Implies PARAM_URL!) 169 */ 170 define('PARAM_LOCALURL', 'localurl'); 171 172 /** 173 * PARAM_NOTAGS - all html tags are stripped from the text. Do not abuse this type. 174 */ 175 define('PARAM_NOTAGS', 'notags'); 176 177 /** 178 * PARAM_PATH - safe relative path name, all dangerous chars are stripped, protects against XSS, SQL injections and directory 179 * traversals note: the leading slash is not removed, window drive letter is not allowed 180 */ 181 define('PARAM_PATH', 'path'); 182 183 /** 184 * PARAM_PEM - Privacy Enhanced Mail format 185 */ 186 define('PARAM_PEM', 'pem'); 187 188 /** 189 * PARAM_PERMISSION - A permission, one of CAP_INHERIT, CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT. 190 */ 191 define('PARAM_PERMISSION', 'permission'); 192 193 /** 194 * PARAM_RAW specifies a parameter that is not cleaned/processed in any way except the discarding of the invalid utf-8 characters 195 */ 196 define('PARAM_RAW', 'raw'); 197 198 /** 199 * PARAM_RAW_TRIMMED like PARAM_RAW but leading and trailing whitespace is stripped. 200 */ 201 define('PARAM_RAW_TRIMMED', 'raw_trimmed'); 202 203 /** 204 * PARAM_SAFEDIR - safe directory name, suitable for include() and require() 205 */ 206 define('PARAM_SAFEDIR', 'safedir'); 207 208 /** 209 * PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths 210 * and other references to Moodle code files. 211 * 212 * This is NOT intended to be used for absolute paths or any user uploaded files. 213 */ 214 define('PARAM_SAFEPATH', 'safepath'); 215 216 /** 217 * PARAM_SEQUENCE - expects a sequence of numbers like 8 to 1,5,6,4,6,8,9. Numbers and comma only. 218 */ 219 define('PARAM_SEQUENCE', 'sequence'); 220 221 /** 222 * PARAM_TAG - one tag (interests, blogs, etc.) - mostly international characters and space, <> not supported 223 */ 224 define('PARAM_TAG', 'tag'); 225 226 /** 227 * PARAM_TAGLIST - list of tags separated by commas (interests, blogs, etc.) 228 */ 229 define('PARAM_TAGLIST', 'taglist'); 230 231 /** 232 * PARAM_TEXT - general plain text compatible with multilang filter, no other html tags. Please note '<', or '>' are allowed here. 233 */ 234 define('PARAM_TEXT', 'text'); 235 236 /** 237 * PARAM_THEME - Checks to see if the string is a valid theme name in the current site 238 */ 239 define('PARAM_THEME', 'theme'); 240 241 /** 242 * PARAM_URL - expected properly formatted URL. Please note that domain part is required, http://localhost/ is not accepted but 243 * http://localhost.localdomain/ is ok. 244 */ 245 define('PARAM_URL', 'url'); 246 247 /** 248 * PARAM_USERNAME - Clean username to only contains allowed characters. This is to be used ONLY when manually creating user 249 * accounts, do NOT use when syncing with external systems!! 250 */ 251 define('PARAM_USERNAME', 'username'); 252 253 /** 254 * PARAM_STRINGID - used to check if the given string is valid string identifier for get_string() 255 */ 256 define('PARAM_STRINGID', 'stringid'); 257 258 // DEPRECATED PARAM TYPES OR ALIASES - DO NOT USE FOR NEW CODE. 259 /** 260 * PARAM_CLEAN - obsoleted, please use a more specific type of parameter. 261 * It was one of the first types, that is why it is abused so much ;-) 262 * @deprecated since 2.0 263 */ 264 define('PARAM_CLEAN', 'clean'); 265 266 /** 267 * PARAM_INTEGER - deprecated alias for PARAM_INT 268 * @deprecated since 2.0 269 */ 270 define('PARAM_INTEGER', 'int'); 271 272 /** 273 * PARAM_NUMBER - deprecated alias of PARAM_FLOAT 274 * @deprecated since 2.0 275 */ 276 define('PARAM_NUMBER', 'float'); 277 278 /** 279 * PARAM_ACTION - deprecated alias for PARAM_ALPHANUMEXT, use for various actions in forms and urls 280 * NOTE: originally alias for PARAM_APLHA 281 * @deprecated since 2.0 282 */ 283 define('PARAM_ACTION', 'alphanumext'); 284 285 /** 286 * PARAM_FORMAT - deprecated alias for PARAM_ALPHANUMEXT, use for names of plugins, formats, etc. 287 * NOTE: originally alias for PARAM_APLHA 288 * @deprecated since 2.0 289 */ 290 define('PARAM_FORMAT', 'alphanumext'); 291 292 /** 293 * PARAM_MULTILANG - deprecated alias of PARAM_TEXT. 294 * @deprecated since 2.0 295 */ 296 define('PARAM_MULTILANG', 'text'); 297 298 /** 299 * PARAM_TIMEZONE - expected timezone. Timezone can be int +-(0-13) or float +-(0.5-12.5) or 300 * string separated by '/' and can have '-' &/ '_' (eg. America/North_Dakota/New_Salem 301 * America/Port-au-Prince) 302 */ 303 define('PARAM_TIMEZONE', 'timezone'); 304 305 /** 306 * PARAM_CLEANFILE - deprecated alias of PARAM_FILE; originally was removing regional chars too 307 */ 308 define('PARAM_CLEANFILE', 'file'); 309 310 /** 311 * PARAM_COMPONENT is used for full component names (aka frankenstyle) such as 'mod_forum', 'core_rating', 'auth_ldap'. 312 * Short legacy subsystem names and module names are accepted too ex: 'forum', 'rating', 'user'. 313 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter. 314 * NOTE: numbers and underscores are strongly discouraged in plugin names! 315 */ 316 define('PARAM_COMPONENT', 'component'); 317 318 /** 319 * PARAM_AREA is a name of area used when addressing files, comments, ratings, etc. 320 * It is usually used together with context id and component. 321 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter. 322 */ 323 define('PARAM_AREA', 'area'); 324 325 /** 326 * PARAM_PLUGIN is used for plugin names such as 'forum', 'glossary', 'ldap', 'paypal', 'completionstatus'. 327 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter. 328 * NOTE: numbers and underscores are strongly discouraged in plugin names! Underscores are forbidden in module names. 329 */ 330 define('PARAM_PLUGIN', 'plugin'); 331 332 333 // Web Services. 334 335 /** 336 * VALUE_REQUIRED - if the parameter is not supplied, there is an error 337 */ 338 define('VALUE_REQUIRED', 1); 339 340 /** 341 * VALUE_OPTIONAL - if the parameter is not supplied, then the param has no value 342 */ 343 define('VALUE_OPTIONAL', 2); 344 345 /** 346 * VALUE_DEFAULT - if the parameter is not supplied, then the default value is used 347 */ 348 define('VALUE_DEFAULT', 0); 349 350 /** 351 * NULL_NOT_ALLOWED - the parameter can not be set to null in the database 352 */ 353 define('NULL_NOT_ALLOWED', false); 354 355 /** 356 * NULL_ALLOWED - the parameter can be set to null in the database 357 */ 358 define('NULL_ALLOWED', true); 359 360 // Page types. 361 362 /** 363 * PAGE_COURSE_VIEW is a definition of a page type. For more information on the page class see moodle/lib/pagelib.php. 364 */ 365 define('PAGE_COURSE_VIEW', 'course-view'); 366 367 /** Get remote addr constant */ 368 define('GETREMOTEADDR_SKIP_HTTP_CLIENT_IP', '1'); 369 /** Get remote addr constant */ 370 define('GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR', '2'); 371 /** 372 * GETREMOTEADDR_SKIP_DEFAULT defines the default behavior remote IP address validation. 373 */ 374 define('GETREMOTEADDR_SKIP_DEFAULT', GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR|GETREMOTEADDR_SKIP_HTTP_CLIENT_IP); 375 376 // Blog access level constant declaration. 377 define ('BLOG_USER_LEVEL', 1); 378 define ('BLOG_GROUP_LEVEL', 2); 379 define ('BLOG_COURSE_LEVEL', 3); 380 define ('BLOG_SITE_LEVEL', 4); 381 define ('BLOG_GLOBAL_LEVEL', 5); 382 383 384 // Tag constants. 385 /** 386 * To prevent problems with multibytes strings,Flag updating in nav not working on the review page. this should not exceed the 387 * length of "varchar(255) / 3 (bytes / utf-8 character) = 85". 388 * TODO: this is not correct, varchar(255) are 255 unicode chars ;-) 389 * 390 * @todo define(TAG_MAX_LENGTH) this is not correct, varchar(255) are 255 unicode chars ;-) 391 */ 392 define('TAG_MAX_LENGTH', 50); 393 394 // Password policy constants. 395 define ('PASSWORD_LOWER', 'abcdefghijklmnopqrstuvwxyz'); 396 define ('PASSWORD_UPPER', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'); 397 define ('PASSWORD_DIGITS', '0123456789'); 398 define ('PASSWORD_NONALPHANUM', '.,;:!?_-+/*@#&$'); 399 400 // Feature constants. 401 // Used for plugin_supports() to report features that are, or are not, supported by a module. 402 403 /** True if module can provide a grade */ 404 define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade'); 405 /** True if module supports outcomes */ 406 define('FEATURE_GRADE_OUTCOMES', 'outcomes'); 407 /** True if module supports advanced grading methods */ 408 define('FEATURE_ADVANCED_GRADING', 'grade_advanced_grading'); 409 /** True if module controls the grade visibility over the gradebook */ 410 define('FEATURE_CONTROLS_GRADE_VISIBILITY', 'controlsgradevisbility'); 411 /** True if module supports plagiarism plugins */ 412 define('FEATURE_PLAGIARISM', 'plagiarism'); 413 414 /** True if module has code to track whether somebody viewed it */ 415 define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views'); 416 /** True if module has custom completion rules */ 417 define('FEATURE_COMPLETION_HAS_RULES', 'completion_has_rules'); 418 419 /** True if module has no 'view' page (like label) */ 420 define('FEATURE_NO_VIEW_LINK', 'viewlink'); 421 /** True (which is default) if the module wants support for setting the ID number for grade calculation purposes. */ 422 define('FEATURE_IDNUMBER', 'idnumber'); 423 /** True if module supports groups */ 424 define('FEATURE_GROUPS', 'groups'); 425 /** True if module supports groupings */ 426 define('FEATURE_GROUPINGS', 'groupings'); 427 /** 428 * True if module supports groupmembersonly (which no longer exists) 429 * @deprecated Since Moodle 2.8 430 */ 431 define('FEATURE_GROUPMEMBERSONLY', 'groupmembersonly'); 432 433 /** Type of module */ 434 define('FEATURE_MOD_ARCHETYPE', 'mod_archetype'); 435 /** True if module supports intro editor */ 436 define('FEATURE_MOD_INTRO', 'mod_intro'); 437 /** True if module has default completion */ 438 define('FEATURE_MODEDIT_DEFAULT_COMPLETION', 'modedit_default_completion'); 439 440 define('FEATURE_COMMENT', 'comment'); 441 442 define('FEATURE_RATE', 'rate'); 443 /** True if module supports backup/restore of moodle2 format */ 444 define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2'); 445 446 /** True if module can show description on course main page */ 447 define('FEATURE_SHOW_DESCRIPTION', 'showdescription'); 448 449 /** True if module uses the question bank */ 450 define('FEATURE_USES_QUESTIONS', 'usesquestions'); 451 452 /** 453 * Maximum filename char size 454 */ 455 define('MAX_FILENAME_SIZE', 100); 456 457 /** Unspecified module archetype */ 458 define('MOD_ARCHETYPE_OTHER', 0); 459 /** Resource-like type module */ 460 define('MOD_ARCHETYPE_RESOURCE', 1); 461 /** Assignment module archetype */ 462 define('MOD_ARCHETYPE_ASSIGNMENT', 2); 463 /** System (not user-addable) module archetype */ 464 define('MOD_ARCHETYPE_SYSTEM', 3); 465 466 /** Type of module */ 467 define('FEATURE_MOD_PURPOSE', 'mod_purpose'); 468 /** Module purpose administration */ 469 define('MOD_PURPOSE_ADMINISTRATION', 'administration'); 470 /** Module purpose assessment */ 471 define('MOD_PURPOSE_ASSESSMENT', 'assessment'); 472 /** Module purpose communication */ 473 define('MOD_PURPOSE_COLLABORATION', 'collaboration'); 474 /** Module purpose communication */ 475 define('MOD_PURPOSE_COMMUNICATION', 'communication'); 476 /** Module purpose content */ 477 define('MOD_PURPOSE_CONTENT', 'content'); 478 /** Module purpose interface */ 479 define('MOD_PURPOSE_INTERFACE', 'interface'); 480 /** Module purpose other */ 481 define('MOD_PURPOSE_OTHER', 'other'); 482 483 /** 484 * Security token used for allowing access 485 * from external application such as web services. 486 * Scripts do not use any session, performance is relatively 487 * low because we need to load access info in each request. 488 * Scripts are executed in parallel. 489 */ 490 define('EXTERNAL_TOKEN_PERMANENT', 0); 491 492 /** 493 * Security token used for allowing access 494 * of embedded applications, the code is executed in the 495 * active user session. Token is invalidated after user logs out. 496 * Scripts are executed serially - normal session locking is used. 497 */ 498 define('EXTERNAL_TOKEN_EMBEDDED', 1); 499 500 /** 501 * The home page should be the site home 502 */ 503 define('HOMEPAGE_SITE', 0); 504 /** 505 * The home page should be the users my page 506 */ 507 define('HOMEPAGE_MY', 1); 508 /** 509 * The home page can be chosen by the user 510 */ 511 define('HOMEPAGE_USER', 2); 512 /** 513 * The home page should be the users my courses page 514 */ 515 define('HOMEPAGE_MYCOURSES', 3); 516 517 /** 518 * URL of the Moodle sites registration portal. 519 */ 520 defined('HUB_MOODLEORGHUBURL') || define('HUB_MOODLEORGHUBURL', 'https://stats.moodle.org'); 521 522 /** 523 * URL of the statistic server public key. 524 */ 525 defined('HUB_STATSPUBLICKEY') || define('HUB_STATSPUBLICKEY', 'https://moodle.org/static/statspubkey.pem'); 526 527 /** 528 * Moodle mobile app service name 529 */ 530 define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app'); 531 532 /** 533 * Indicates the user has the capabilities required to ignore activity and course file size restrictions 534 */ 535 define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1); 536 537 /** 538 * Course display settings: display all sections on one page. 539 */ 540 define('COURSE_DISPLAY_SINGLEPAGE', 0); 541 /** 542 * Course display settings: split pages into a page per section. 543 */ 544 define('COURSE_DISPLAY_MULTIPAGE', 1); 545 546 /** 547 * Authentication constant: String used in password field when password is not stored. 548 */ 549 define('AUTH_PASSWORD_NOT_CACHED', 'not cached'); 550 551 /** 552 * Email from header to never include via information. 553 */ 554 define('EMAIL_VIA_NEVER', 0); 555 556 /** 557 * Email from header to always include via information. 558 */ 559 define('EMAIL_VIA_ALWAYS', 1); 560 561 /** 562 * Email from header to only include via information if the address is no-reply. 563 */ 564 define('EMAIL_VIA_NO_REPLY_ONLY', 2); 565 566 /** 567 * Contact site support form/link disabled. 568 */ 569 define('CONTACT_SUPPORT_DISABLED', 0); 570 571 /** 572 * Contact site support form/link only available to authenticated users. 573 */ 574 define('CONTACT_SUPPORT_AUTHENTICATED', 1); 575 576 /** 577 * Contact site support form/link available to anyone visiting the site. 578 */ 579 define('CONTACT_SUPPORT_ANYONE', 2); 580 581 // PARAMETER HANDLING. 582 583 /** 584 * Returns a particular value for the named variable, taken from 585 * POST or GET. If the parameter doesn't exist then an error is 586 * thrown because we require this variable. 587 * 588 * This function should be used to initialise all required values 589 * in a script that are based on parameters. Usually it will be 590 * used like this: 591 * $id = required_param('id', PARAM_INT); 592 * 593 * Please note the $type parameter is now required and the value can not be array. 594 * 595 * @param string $parname the name of the page parameter we want 596 * @param string $type expected type of parameter 597 * @return mixed 598 * @throws coding_exception 599 */ 600 function required_param($parname, $type) { 601 if (func_num_args() != 2 or empty($parname) or empty($type)) { 602 throw new coding_exception('required_param() requires $parname and $type to be specified (parameter: '.$parname.')'); 603 } 604 // POST has precedence. 605 if (isset($_POST[$parname])) { 606 $param = $_POST[$parname]; 607 } else if (isset($_GET[$parname])) { 608 $param = $_GET[$parname]; 609 } else { 610 throw new \moodle_exception('missingparam', '', '', $parname); 611 } 612 613 if (is_array($param)) { 614 debugging('Invalid array parameter detected in required_param(): '.$parname); 615 // TODO: switch to fatal error in Moodle 2.3. 616 return required_param_array($parname, $type); 617 } 618 619 return clean_param($param, $type); 620 } 621 622 /** 623 * Returns a particular array value for the named variable, taken from 624 * POST or GET. If the parameter doesn't exist then an error is 625 * thrown because we require this variable. 626 * 627 * This function should be used to initialise all required values 628 * in a script that are based on parameters. Usually it will be 629 * used like this: 630 * $ids = required_param_array('ids', PARAM_INT); 631 * 632 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported 633 * 634 * @param string $parname the name of the page parameter we want 635 * @param string $type expected type of parameter 636 * @return array 637 * @throws coding_exception 638 */ 639 function required_param_array($parname, $type) { 640 if (func_num_args() != 2 or empty($parname) or empty($type)) { 641 throw new coding_exception('required_param_array() requires $parname and $type to be specified (parameter: '.$parname.')'); 642 } 643 // POST has precedence. 644 if (isset($_POST[$parname])) { 645 $param = $_POST[$parname]; 646 } else if (isset($_GET[$parname])) { 647 $param = $_GET[$parname]; 648 } else { 649 throw new \moodle_exception('missingparam', '', '', $parname); 650 } 651 if (!is_array($param)) { 652 throw new \moodle_exception('missingparam', '', '', $parname); 653 } 654 655 $result = array(); 656 foreach ($param as $key => $value) { 657 if (!preg_match('/^[a-z0-9_-]+$/i', $key)) { 658 debugging('Invalid key name in required_param_array() detected: '.$key.', parameter: '.$parname); 659 continue; 660 } 661 $result[$key] = clean_param($value, $type); 662 } 663 664 return $result; 665 } 666 667 /** 668 * Returns a particular value for the named variable, taken from 669 * POST or GET, otherwise returning a given default. 670 * 671 * This function should be used to initialise all optional values 672 * in a script that are based on parameters. Usually it will be 673 * used like this: 674 * $name = optional_param('name', 'Fred', PARAM_TEXT); 675 * 676 * Please note the $type parameter is now required and the value can not be array. 677 * 678 * @param string $parname the name of the page parameter we want 679 * @param mixed $default the default value to return if nothing is found 680 * @param string $type expected type of parameter 681 * @return mixed 682 * @throws coding_exception 683 */ 684 function optional_param($parname, $default, $type) { 685 if (func_num_args() != 3 or empty($parname) or empty($type)) { 686 throw new coding_exception('optional_param requires $parname, $default + $type to be specified (parameter: '.$parname.')'); 687 } 688 689 // POST has precedence. 690 if (isset($_POST[$parname])) { 691 $param = $_POST[$parname]; 692 } else if (isset($_GET[$parname])) { 693 $param = $_GET[$parname]; 694 } else { 695 return $default; 696 } 697 698 if (is_array($param)) { 699 debugging('Invalid array parameter detected in required_param(): '.$parname); 700 // TODO: switch to $default in Moodle 2.3. 701 return optional_param_array($parname, $default, $type); 702 } 703 704 return clean_param($param, $type); 705 } 706 707 /** 708 * Returns a particular array value for the named variable, taken from 709 * POST or GET, otherwise returning a given default. 710 * 711 * This function should be used to initialise all optional values 712 * in a script that are based on parameters. Usually it will be 713 * used like this: 714 * $ids = optional_param('id', array(), PARAM_INT); 715 * 716 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported 717 * 718 * @param string $parname the name of the page parameter we want 719 * @param mixed $default the default value to return if nothing is found 720 * @param string $type expected type of parameter 721 * @return array 722 * @throws coding_exception 723 */ 724 function optional_param_array($parname, $default, $type) { 725 if (func_num_args() != 3 or empty($parname) or empty($type)) { 726 throw new coding_exception('optional_param_array requires $parname, $default + $type to be specified (parameter: '.$parname.')'); 727 } 728 729 // POST has precedence. 730 if (isset($_POST[$parname])) { 731 $param = $_POST[$parname]; 732 } else if (isset($_GET[$parname])) { 733 $param = $_GET[$parname]; 734 } else { 735 return $default; 736 } 737 if (!is_array($param)) { 738 debugging('optional_param_array() expects array parameters only: '.$parname); 739 return $default; 740 } 741 742 $result = array(); 743 foreach ($param as $key => $value) { 744 if (!preg_match('/^[a-z0-9_-]+$/i', $key)) { 745 debugging('Invalid key name in optional_param_array() detected: '.$key.', parameter: '.$parname); 746 continue; 747 } 748 $result[$key] = clean_param($value, $type); 749 } 750 751 return $result; 752 } 753 754 /** 755 * Strict validation of parameter values, the values are only converted 756 * to requested PHP type. Internally it is using clean_param, the values 757 * before and after cleaning must be equal - otherwise 758 * an invalid_parameter_exception is thrown. 759 * Objects and classes are not accepted. 760 * 761 * @param mixed $param 762 * @param string $type PARAM_ constant 763 * @param bool $allownull are nulls valid value? 764 * @param string $debuginfo optional debug information 765 * @return mixed the $param value converted to PHP type 766 * @throws invalid_parameter_exception if $param is not of given type 767 */ 768 function validate_param($param, $type, $allownull=NULL_NOT_ALLOWED, $debuginfo='') { 769 if (is_null($param)) { 770 if ($allownull == NULL_ALLOWED) { 771 return null; 772 } else { 773 throw new invalid_parameter_exception($debuginfo); 774 } 775 } 776 if (is_array($param) or is_object($param)) { 777 throw new invalid_parameter_exception($debuginfo); 778 } 779 780 $cleaned = clean_param($param, $type); 781 782 if ($type == PARAM_FLOAT) { 783 // Do not detect precision loss here. 784 if (is_float($param) or is_int($param)) { 785 // These always fit. 786 } else if (!is_numeric($param) or !preg_match('/^[\+-]?[0-9]*\.?[0-9]*(e[-+]?[0-9]+)?$/i', (string)$param)) { 787 throw new invalid_parameter_exception($debuginfo); 788 } 789 } else if ((string)$param !== (string)$cleaned) { 790 // Conversion to string is usually lossless. 791 throw new invalid_parameter_exception($debuginfo); 792 } 793 794 return $cleaned; 795 } 796 797 /** 798 * Makes sure array contains only the allowed types, this function does not validate array key names! 799 * 800 * <code> 801 * $options = clean_param($options, PARAM_INT); 802 * </code> 803 * 804 * @param array|null $param the variable array we are cleaning 805 * @param string $type expected format of param after cleaning. 806 * @param bool $recursive clean recursive arrays 807 * @return array 808 * @throws coding_exception 809 */ 810 function clean_param_array(?array $param, $type, $recursive = false) { 811 // Convert null to empty array. 812 $param = (array)$param; 813 foreach ($param as $key => $value) { 814 if (is_array($value)) { 815 if ($recursive) { 816 $param[$key] = clean_param_array($value, $type, true); 817 } else { 818 throw new coding_exception('clean_param_array can not process multidimensional arrays when $recursive is false.'); 819 } 820 } else { 821 $param[$key] = clean_param($value, $type); 822 } 823 } 824 return $param; 825 } 826 827 /** 828 * Used by {@link optional_param()} and {@link required_param()} to 829 * clean the variables and/or cast to specific types, based on 830 * an options field. 831 * <code> 832 * $course->format = clean_param($course->format, PARAM_ALPHA); 833 * $selectedgradeitem = clean_param($selectedgradeitem, PARAM_INT); 834 * </code> 835 * 836 * @param mixed $param the variable we are cleaning 837 * @param string $type expected format of param after cleaning. 838 * @return mixed 839 * @throws coding_exception 840 */ 841 function clean_param($param, $type) { 842 global $CFG; 843 844 if (is_array($param)) { 845 throw new coding_exception('clean_param() can not process arrays, please use clean_param_array() instead.'); 846 } else if (is_object($param)) { 847 if (method_exists($param, '__toString')) { 848 $param = $param->__toString(); 849 } else { 850 throw new coding_exception('clean_param() can not process objects, please use clean_param_array() instead.'); 851 } 852 } 853 854 switch ($type) { 855 case PARAM_RAW: 856 // No cleaning at all. 857 $param = fix_utf8($param); 858 return $param; 859 860 case PARAM_RAW_TRIMMED: 861 // No cleaning, but strip leading and trailing whitespace. 862 $param = (string)fix_utf8($param); 863 return trim($param); 864 865 case PARAM_CLEAN: 866 // General HTML cleaning, try to use more specific type if possible this is deprecated! 867 // Please use more specific type instead. 868 if (is_numeric($param)) { 869 return $param; 870 } 871 $param = fix_utf8($param); 872 // Sweep for scripts, etc. 873 return clean_text($param); 874 875 case PARAM_CLEANHTML: 876 // Clean html fragment. 877 $param = (string)fix_utf8($param); 878 // Sweep for scripts, etc. 879 $param = clean_text($param, FORMAT_HTML); 880 return trim($param); 881 882 case PARAM_INT: 883 // Convert to integer. 884 return (int)$param; 885 886 case PARAM_FLOAT: 887 // Convert to float. 888 return (float)$param; 889 890 case PARAM_LOCALISEDFLOAT: 891 // Convert to float. 892 return unformat_float($param, true); 893 894 case PARAM_ALPHA: 895 // Remove everything not `a-z`. 896 return preg_replace('/[^a-zA-Z]/i', '', (string)$param); 897 898 case PARAM_ALPHAEXT: 899 // Remove everything not `a-zA-Z_-` (originally allowed "/" too). 900 return preg_replace('/[^a-zA-Z_-]/i', '', (string)$param); 901 902 case PARAM_ALPHANUM: 903 // Remove everything not `a-zA-Z0-9`. 904 return preg_replace('/[^A-Za-z0-9]/i', '', (string)$param); 905 906 case PARAM_ALPHANUMEXT: 907 // Remove everything not `a-zA-Z0-9_-`. 908 return preg_replace('/[^A-Za-z0-9_-]/i', '', (string)$param); 909 910 case PARAM_SEQUENCE: 911 // Remove everything not `0-9,`. 912 return preg_replace('/[^0-9,]/i', '', (string)$param); 913 914 case PARAM_BOOL: 915 // Convert to 1 or 0. 916 $tempstr = strtolower((string)$param); 917 if ($tempstr === 'on' or $tempstr === 'yes' or $tempstr === 'true') { 918 $param = 1; 919 } else if ($tempstr === 'off' or $tempstr === 'no' or $tempstr === 'false') { 920 $param = 0; 921 } else { 922 $param = empty($param) ? 0 : 1; 923 } 924 return $param; 925 926 case PARAM_NOTAGS: 927 // Strip all tags. 928 $param = fix_utf8($param); 929 return strip_tags((string)$param); 930 931 case PARAM_TEXT: 932 // Leave only tags needed for multilang. 933 $param = fix_utf8($param); 934 // If the multilang syntax is not correct we strip all tags because it would break xhtml strict which is required 935 // for accessibility standards please note this cleaning does not strip unbalanced '>' for BC compatibility reasons. 936 do { 937 if (strpos((string)$param, '</lang>') !== false) { 938 // Old and future mutilang syntax. 939 $param = strip_tags($param, '<lang>'); 940 if (!preg_match_all('/<.*>/suU', $param, $matches)) { 941 break; 942 } 943 $open = false; 944 foreach ($matches[0] as $match) { 945 if ($match === '</lang>') { 946 if ($open) { 947 $open = false; 948 continue; 949 } else { 950 break 2; 951 } 952 } 953 if (!preg_match('/^<lang lang="[a-zA-Z0-9_-]+"\s*>$/u', $match)) { 954 break 2; 955 } else { 956 $open = true; 957 } 958 } 959 if ($open) { 960 break; 961 } 962 return $param; 963 964 } else if (strpos((string)$param, '</span>') !== false) { 965 // Current problematic multilang syntax. 966 $param = strip_tags($param, '<span>'); 967 if (!preg_match_all('/<.*>/suU', $param, $matches)) { 968 break; 969 } 970 $open = false; 971 foreach ($matches[0] as $match) { 972 if ($match === '</span>') { 973 if ($open) { 974 $open = false; 975 continue; 976 } else { 977 break 2; 978 } 979 } 980 if (!preg_match('/^<span(\s+lang="[a-zA-Z0-9_-]+"|\s+class="multilang"){2}\s*>$/u', $match)) { 981 break 2; 982 } else { 983 $open = true; 984 } 985 } 986 if ($open) { 987 break; 988 } 989 return $param; 990 } 991 } while (false); 992 // Easy, just strip all tags, if we ever want to fix orphaned '&' we have to do that in format_string(). 993 return strip_tags((string)$param); 994 995 case PARAM_COMPONENT: 996 // We do not want any guessing here, either the name is correct or not 997 // please note only normalised component names are accepted. 998 $param = (string)$param; 999 if (!preg_match('/^[a-z][a-z0-9]*(_[a-z][a-z0-9_]*)?[a-z0-9]+$/', $param)) { 1000 return ''; 1001 } 1002 if (strpos($param, '__') !== false) { 1003 return ''; 1004 } 1005 if (strpos($param, 'mod_') === 0) { 1006 // Module names must not contain underscores because we need to differentiate them from invalid plugin types. 1007 if (substr_count($param, '_') != 1) { 1008 return ''; 1009 } 1010 } 1011 return $param; 1012 1013 case PARAM_PLUGIN: 1014 case PARAM_AREA: 1015 // We do not want any guessing here, either the name is correct or not. 1016 if (!is_valid_plugin_name($param)) { 1017 return ''; 1018 } 1019 return $param; 1020 1021 case PARAM_SAFEDIR: 1022 // Remove everything not a-zA-Z0-9_- . 1023 return preg_replace('/[^a-zA-Z0-9_-]/i', '', (string)$param); 1024 1025 case PARAM_SAFEPATH: 1026 // Remove everything not a-zA-Z0-9/_- . 1027 return preg_replace('/[^a-zA-Z0-9\/_-]/i', '', (string)$param); 1028 1029 case PARAM_FILE: 1030 // Strip all suspicious characters from filename. 1031 $param = (string)fix_utf8($param); 1032 $param = preg_replace('~[[:cntrl:]]|[&<>"`\|\':\\\\/]~u', '', $param); 1033 if ($param === '.' || $param === '..') { 1034 $param = ''; 1035 } 1036 return $param; 1037 1038 case PARAM_PATH: 1039 // Strip all suspicious characters from file path. 1040 $param = (string)fix_utf8($param); 1041 $param = str_replace('\\', '/', $param); 1042 1043 // Explode the path and clean each element using the PARAM_FILE rules. 1044 $breadcrumb = explode('/', $param); 1045 foreach ($breadcrumb as $key => $crumb) { 1046 if ($crumb === '.' && $key === 0) { 1047 // Special condition to allow for relative current path such as ./currentdirfile.txt. 1048 } else { 1049 $crumb = clean_param($crumb, PARAM_FILE); 1050 } 1051 $breadcrumb[$key] = $crumb; 1052 } 1053 $param = implode('/', $breadcrumb); 1054 1055 // Remove multiple current path (./././) and multiple slashes (///). 1056 $param = preg_replace('~//+~', '/', $param); 1057 $param = preg_replace('~/(\./)+~', '/', $param); 1058 return $param; 1059 1060 case PARAM_HOST: 1061 // Allow FQDN or IPv4 dotted quad. 1062 $param = preg_replace('/[^\.\d\w-]/', '', (string)$param ); 1063 // Match ipv4 dotted quad. 1064 if (preg_match('/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/', $param, $match)) { 1065 // Confirm values are ok. 1066 if ( $match[0] > 255 1067 || $match[1] > 255 1068 || $match[3] > 255 1069 || $match[4] > 255 ) { 1070 // Hmmm, what kind of dotted quad is this? 1071 $param = ''; 1072 } 1073 } else if ( preg_match('/^[\w\d\.-]+$/', $param) // Dots, hyphens, numbers. 1074 && !preg_match('/^[\.-]/', $param) // No leading dots/hyphens. 1075 && !preg_match('/[\.-]$/', $param) // No trailing dots/hyphens. 1076 ) { 1077 // All is ok - $param is respected. 1078 } else { 1079 // All is not ok... 1080 $param=''; 1081 } 1082 return $param; 1083 1084 case PARAM_URL: 1085 // Allow safe urls. 1086 $param = (string)fix_utf8($param); 1087 include_once($CFG->dirroot . '/lib/validateurlsyntax.php'); 1088 if (!empty($param) && validateUrlSyntax($param, 's?H?S?F?E-u-P-a?I?p?f?q?r?')) { 1089 // All is ok, param is respected. 1090 } else { 1091 // Not really ok. 1092 $param =''; 1093 } 1094 return $param; 1095 1096 case PARAM_LOCALURL: 1097 // Allow http absolute, root relative and relative URLs within wwwroot. 1098 $param = clean_param($param, PARAM_URL); 1099 if (!empty($param)) { 1100 1101 if ($param === $CFG->wwwroot) { 1102 // Exact match; 1103 } else if (preg_match(':^/:', $param)) { 1104 // Root-relative, ok! 1105 } else if (preg_match('/^' . preg_quote($CFG->wwwroot . '/', '/') . '/i', $param)) { 1106 // Absolute, and matches our wwwroot. 1107 } else { 1108 1109 // Relative - let's make sure there are no tricks. 1110 if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?') && !preg_match('/javascript:/i', $param)) { 1111 // Looks ok. 1112 } else { 1113 $param = ''; 1114 } 1115 } 1116 } 1117 return $param; 1118 1119 case PARAM_PEM: 1120 $param = trim((string)$param); 1121 // PEM formatted strings may contain letters/numbers and the symbols: 1122 // forward slash: / 1123 // plus sign: + 1124 // equal sign: = 1125 // , surrounded by BEGIN and END CERTIFICATE prefix and suffixes. 1126 if (preg_match('/^-----BEGIN CERTIFICATE-----([\s\w\/\+=]+)-----END CERTIFICATE-----$/', trim($param), $matches)) { 1127 list($wholething, $body) = $matches; 1128 unset($wholething, $matches); 1129 $b64 = clean_param($body, PARAM_BASE64); 1130 if (!empty($b64)) { 1131 return "-----BEGIN CERTIFICATE-----\n$b64\n-----END CERTIFICATE-----\n"; 1132 } else { 1133 return ''; 1134 } 1135 } 1136 return ''; 1137 1138 case PARAM_BASE64: 1139 if (!empty($param)) { 1140 // PEM formatted strings may contain letters/numbers and the symbols 1141 // forward slash: / 1142 // plus sign: + 1143 // equal sign: =. 1144 if (0 >= preg_match('/^([\s\w\/\+=]+)$/', trim($param))) { 1145 return ''; 1146 } 1147 $lines = preg_split('/[\s]+/', $param, -1, PREG_SPLIT_NO_EMPTY); 1148 // Each line of base64 encoded data must be 64 characters in length, except for the last line which may be less 1149 // than (or equal to) 64 characters long. 1150 for ($i=0, $j=count($lines); $i < $j; $i++) { 1151 if ($i + 1 == $j) { 1152 if (64 < strlen($lines[$i])) { 1153 return ''; 1154 } 1155 continue; 1156 } 1157 1158 if (64 != strlen($lines[$i])) { 1159 return ''; 1160 } 1161 } 1162 return implode("\n", $lines); 1163 } else { 1164 return ''; 1165 } 1166 1167 case PARAM_TAG: 1168 $param = (string)fix_utf8($param); 1169 // Please note it is not safe to use the tag name directly anywhere, 1170 // it must be processed with s(), urlencode() before embedding anywhere. 1171 // Remove some nasties. 1172 $param = preg_replace('~[[:cntrl:]]|[<>`]~u', '', $param); 1173 // Convert many whitespace chars into one. 1174 $param = preg_replace('/\s+/u', ' ', $param); 1175 $param = core_text::substr(trim($param), 0, TAG_MAX_LENGTH); 1176 return $param; 1177 1178 case PARAM_TAGLIST: 1179 $param = (string)fix_utf8($param); 1180 $tags = explode(',', $param); 1181 $result = array(); 1182 foreach ($tags as $tag) { 1183 $res = clean_param($tag, PARAM_TAG); 1184 if ($res !== '') { 1185 $result[] = $res; 1186 } 1187 } 1188 if ($result) { 1189 return implode(',', $result); 1190 } else { 1191 return ''; 1192 } 1193 1194 case PARAM_CAPABILITY: 1195 if (get_capability_info($param)) { 1196 return $param; 1197 } else { 1198 return ''; 1199 } 1200 1201 case PARAM_PERMISSION: 1202 $param = (int)$param; 1203 if (in_array($param, array(CAP_INHERIT, CAP_ALLOW, CAP_PREVENT, CAP_PROHIBIT))) { 1204 return $param; 1205 } else { 1206 return CAP_INHERIT; 1207 } 1208 1209 case PARAM_AUTH: 1210 $param = clean_param($param, PARAM_PLUGIN); 1211 if (empty($param)) { 1212 return ''; 1213 } else if (exists_auth_plugin($param)) { 1214 return $param; 1215 } else { 1216 return ''; 1217 } 1218 1219 case PARAM_LANG: 1220 $param = clean_param($param, PARAM_SAFEDIR); 1221 if (get_string_manager()->translation_exists($param)) { 1222 return $param; 1223 } else { 1224 // Specified language is not installed or param malformed. 1225 return ''; 1226 } 1227 1228 case PARAM_THEME: 1229 $param = clean_param($param, PARAM_PLUGIN); 1230 if (empty($param)) { 1231 return ''; 1232 } else if (file_exists("$CFG->dirroot/theme/$param/config.php")) { 1233 return $param; 1234 } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$param/config.php")) { 1235 return $param; 1236 } else { 1237 // Specified theme is not installed. 1238 return ''; 1239 } 1240 1241 case PARAM_USERNAME: 1242 $param = (string)fix_utf8($param); 1243 $param = trim($param); 1244 // Convert uppercase to lowercase MDL-16919. 1245 $param = core_text::strtolower($param); 1246 if (empty($CFG->extendedusernamechars)) { 1247 $param = str_replace(" " , "", $param); 1248 // Regular expression, eliminate all chars EXCEPT: 1249 // alphanum, dash (-), underscore (_), at sign (@) and period (.) characters. 1250 $param = preg_replace('/[^-\.@_a-z0-9]/', '', $param); 1251 } 1252 return $param; 1253 1254 case PARAM_EMAIL: 1255 $param = fix_utf8($param); 1256 if (validate_email($param ?? '')) { 1257 return $param; 1258 } else { 1259 return ''; 1260 } 1261 1262 case PARAM_STRINGID: 1263 if (preg_match('|^[a-zA-Z][a-zA-Z0-9\.:/_-]*$|', (string)$param)) { 1264 return $param; 1265 } else { 1266 return ''; 1267 } 1268 1269 case PARAM_TIMEZONE: 1270 // Can be int, float(with .5 or .0) or string seperated by '/' and can have '-_'. 1271 $param = (string)fix_utf8($param); 1272 $timezonepattern = '/^(([+-]?(0?[0-9](\.[5|0])?|1[0-3](\.0)?|1[0-2]\.5))|(99)|[[:alnum:]]+(\/?[[:alpha:]_-])+)$/'; 1273 if (preg_match($timezonepattern, $param)) { 1274 return $param; 1275 } else { 1276 return ''; 1277 } 1278 1279 default: 1280 // Doh! throw error, switched parameters in optional_param or another serious problem. 1281 throw new \moodle_exception("unknownparamtype", '', '', $type); 1282 } 1283 } 1284 1285 /** 1286 * Whether the PARAM_* type is compatible in RTL. 1287 * 1288 * Being compatible with RTL means that the data they contain can flow 1289 * from right-to-left or left-to-right without compromising the user experience. 1290 * 1291 * Take URLs for example, they are not RTL compatible as they should always 1292 * flow from the left to the right. This also applies to numbers, email addresses, 1293 * configuration snippets, base64 strings, etc... 1294 * 1295 * This function tries to best guess which parameters can contain localised strings. 1296 * 1297 * @param string $paramtype Constant PARAM_*. 1298 * @return bool 1299 */ 1300 function is_rtl_compatible($paramtype) { 1301 return $paramtype == PARAM_TEXT || $paramtype == PARAM_NOTAGS; 1302 } 1303 1304 /** 1305 * Makes sure the data is using valid utf8, invalid characters are discarded. 1306 * 1307 * Note: this function is not intended for full objects with methods and private properties. 1308 * 1309 * @param mixed $value 1310 * @return mixed with proper utf-8 encoding 1311 */ 1312 function fix_utf8($value) { 1313 if (is_null($value) or $value === '') { 1314 return $value; 1315 1316 } else if (is_string($value)) { 1317 if ((string)(int)$value === $value) { 1318 // Shortcut. 1319 return $value; 1320 } 1321 1322 // Remove null bytes or invalid Unicode sequences from value. 1323 $value = str_replace(["\0", "\xef\xbf\xbe", "\xef\xbf\xbf"], '', $value); 1324 1325 // Note: this duplicates min_fix_utf8() intentionally. 1326 static $buggyiconv = null; 1327 if ($buggyiconv === null) { 1328 $buggyiconv = (!function_exists('iconv') or @iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€'); 1329 } 1330 1331 if ($buggyiconv) { 1332 if (function_exists('mb_convert_encoding')) { 1333 $subst = mb_substitute_character(); 1334 mb_substitute_character('none'); 1335 $result = mb_convert_encoding($value, 'utf-8', 'utf-8'); 1336 mb_substitute_character($subst); 1337 1338 } else { 1339 // Warn admins on admin/index.php page. 1340 $result = $value; 1341 } 1342 1343 } else { 1344 $result = @iconv('UTF-8', 'UTF-8//IGNORE', $value); 1345 } 1346 1347 return $result; 1348 1349 } else if (is_array($value)) { 1350 foreach ($value as $k => $v) { 1351 $value[$k] = fix_utf8($v); 1352 } 1353 return $value; 1354 1355 } else if (is_object($value)) { 1356 // Do not modify original. 1357 $value = clone($value); 1358 foreach ($value as $k => $v) { 1359 $value->$k = fix_utf8($v); 1360 } 1361 return $value; 1362 1363 } else { 1364 // This is some other type, no utf-8 here. 1365 return $value; 1366 } 1367 } 1368 1369 /** 1370 * Return true if given value is integer or string with integer value 1371 * 1372 * @param mixed $value String or Int 1373 * @return bool true if number, false if not 1374 */ 1375 function is_number($value) { 1376 if (is_int($value)) { 1377 return true; 1378 } else if (is_string($value)) { 1379 return ((string)(int)$value) === $value; 1380 } else { 1381 return false; 1382 } 1383 } 1384 1385 /** 1386 * Returns host part from url. 1387 * 1388 * @param string $url full url 1389 * @return string host, null if not found 1390 */ 1391 function get_host_from_url($url) { 1392 preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches); 1393 if ($matches) { 1394 return $matches[1]; 1395 } 1396 return null; 1397 } 1398 1399 /** 1400 * Tests whether anything was returned by text editor 1401 * 1402 * This function is useful for testing whether something you got back from 1403 * the HTML editor actually contains anything. Sometimes the HTML editor 1404 * appear to be empty, but actually you get back a <br> tag or something. 1405 * 1406 * @param string $string a string containing HTML. 1407 * @return boolean does the string contain any actual content - that is text, 1408 * images, objects, etc. 1409 */ 1410 function html_is_blank($string) { 1411 return trim(strip_tags((string)$string, '<img><object><applet><input><select><textarea><hr>')) == ''; 1412 } 1413 1414 /** 1415 * Set a key in global configuration 1416 * 1417 * Set a key/value pair in both this session's {@link $CFG} global variable 1418 * and in the 'config' database table for future sessions. 1419 * 1420 * Can also be used to update keys for plugin-scoped configs in config_plugin table. 1421 * In that case it doesn't affect $CFG. 1422 * 1423 * A NULL value will delete the entry. 1424 * 1425 * NOTE: this function is called from lib/db/upgrade.php 1426 * 1427 * @param string $name the key to set 1428 * @param string $value the value to set (without magic quotes) 1429 * @param string $plugin (optional) the plugin scope, default null 1430 * @return bool true or exception 1431 */ 1432 function set_config($name, $value, $plugin = null) { 1433 global $CFG, $DB; 1434 1435 // Redirect to appropriate handler when value is null. 1436 if ($value === null) { 1437 return unset_config($name, $plugin); 1438 } 1439 1440 // Set variables determining conditions and where to store the new config. 1441 // Plugin config goes to {config_plugins}, core config goes to {config}. 1442 $iscore = empty($plugin); 1443 if ($iscore) { 1444 // If it's for core config. 1445 $table = 'config'; 1446 $conditions = ['name' => $name]; 1447 $invalidatecachekey = 'core'; 1448 } else { 1449 // If it's a plugin. 1450 $table = 'config_plugins'; 1451 $conditions = ['name' => $name, 'plugin' => $plugin]; 1452 $invalidatecachekey = $plugin; 1453 } 1454 1455 // DB handling - checks for existing config, updating or inserting only if necessary. 1456 $invalidatecache = true; 1457 $inserted = false; 1458 $record = $DB->get_record($table, $conditions, 'id, value'); 1459 if ($record === false) { 1460 // Inserts a new config record. 1461 $config = new stdClass(); 1462 $config->name = $name; 1463 $config->value = $value; 1464 if (!$iscore) { 1465 $config->plugin = $plugin; 1466 } 1467 $inserted = $DB->insert_record($table, $config, false); 1468 } else if ($invalidatecache = ($record->value !== $value)) { 1469 // Record exists - Check and only set new value if it has changed. 1470 $DB->set_field($table, 'value', $value, ['id' => $record->id]); 1471 } 1472 1473 if ($iscore && !isset($CFG->config_php_settings[$name])) { 1474 // So it's defined for this invocation at least. 1475 // Settings from db are always strings. 1476 $CFG->$name = (string) $value; 1477 } 1478 1479 // When setting config during a Behat test (in the CLI script, not in the web browser 1480 // requests), remember which ones are set so that we can clear them later. 1481 if ($iscore && $inserted && defined('BEHAT_TEST')) { 1482 $CFG->behat_cli_added_config[$name] = true; 1483 } 1484 1485 // Update siteidentifier cache, if required. 1486 if ($iscore && $name === 'siteidentifier') { 1487 cache_helper::update_site_identifier($value); 1488 } 1489 1490 // Invalidate cache, if required. 1491 if ($invalidatecache) { 1492 cache_helper::invalidate_by_definition('core', 'config', [], $invalidatecachekey); 1493 } 1494 1495 return true; 1496 } 1497 1498 /** 1499 * Get configuration values from the global config table 1500 * or the config_plugins table. 1501 * 1502 * If called with one parameter, it will load all the config 1503 * variables for one plugin, and return them as an object. 1504 * 1505 * If called with 2 parameters it will return a string single 1506 * value or false if the value is not found. 1507 * 1508 * NOTE: this function is called from lib/db/upgrade.php 1509 * 1510 * @param string $plugin full component name 1511 * @param string $name default null 1512 * @return mixed hash-like object or single value, return false no config found 1513 * @throws dml_exception 1514 */ 1515 function get_config($plugin, $name = null) { 1516 global $CFG, $DB; 1517 1518 if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) { 1519 $forced =& $CFG->config_php_settings; 1520 $iscore = true; 1521 $plugin = 'core'; 1522 } else { 1523 if (array_key_exists($plugin, $CFG->forced_plugin_settings)) { 1524 $forced =& $CFG->forced_plugin_settings[$plugin]; 1525 } else { 1526 $forced = array(); 1527 } 1528 $iscore = false; 1529 } 1530 1531 if (!isset($CFG->siteidentifier)) { 1532 try { 1533 // This may throw an exception during installation, which is how we detect the 1534 // need to install the database. For more details see {@see initialise_cfg()}. 1535 $CFG->siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier')); 1536 } catch (dml_exception $ex) { 1537 // Set siteidentifier to false. We don't want to trip this continually. 1538 $siteidentifier = false; 1539 throw $ex; 1540 } 1541 } 1542 1543 if (!empty($name)) { 1544 if (array_key_exists($name, $forced)) { 1545 return (string)$forced[$name]; 1546 } else if ($name === 'siteidentifier' && $plugin == 'core') { 1547 return $CFG->siteidentifier; 1548 } 1549 } 1550 1551 $cache = cache::make('core', 'config'); 1552 $result = $cache->get($plugin); 1553 if ($result === false) { 1554 // The user is after a recordset. 1555 if (!$iscore) { 1556 $result = $DB->get_records_menu('config_plugins', array('plugin' => $plugin), '', 'name,value'); 1557 } else { 1558 // This part is not really used any more, but anyway... 1559 $result = $DB->get_records_menu('config', array(), '', 'name,value');; 1560 } 1561 $cache->set($plugin, $result); 1562 } 1563 1564 if (!empty($name)) { 1565 if (array_key_exists($name, $result)) { 1566 return $result[$name]; 1567 } 1568 return false; 1569 } 1570 1571 if ($plugin === 'core') { 1572 $result['siteidentifier'] = $CFG->siteidentifier; 1573 } 1574 1575 foreach ($forced as $key => $value) { 1576 if (is_null($value) or is_array($value) or is_object($value)) { 1577 // We do not want any extra mess here, just real settings that could be saved in db. 1578 unset($result[$key]); 1579 } else { 1580 // Convert to string as if it went through the DB. 1581 $result[$key] = (string)$value; 1582 } 1583 } 1584 1585 return (object)$result; 1586 } 1587 1588 /** 1589 * Removes a key from global configuration. 1590 * 1591 * NOTE: this function is called from lib/db/upgrade.php 1592 * 1593 * @param string $name the key to set 1594 * @param string $plugin (optional) the plugin scope 1595 * @return boolean whether the operation succeeded. 1596 */ 1597 function unset_config($name, $plugin=null) { 1598 global $CFG, $DB; 1599 1600 if (empty($plugin)) { 1601 unset($CFG->$name); 1602 $DB->delete_records('config', array('name' => $name)); 1603 cache_helper::invalidate_by_definition('core', 'config', array(), 'core'); 1604 } else { 1605 $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin)); 1606 cache_helper::invalidate_by_definition('core', 'config', array(), $plugin); 1607 } 1608 1609 return true; 1610 } 1611 1612 /** 1613 * Remove all the config variables for a given plugin. 1614 * 1615 * NOTE: this function is called from lib/db/upgrade.php 1616 * 1617 * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice'; 1618 * @return boolean whether the operation succeeded. 1619 */ 1620 function unset_all_config_for_plugin($plugin) { 1621 global $DB; 1622 // Delete from the obvious config_plugins first. 1623 $DB->delete_records('config_plugins', array('plugin' => $plugin)); 1624 // Next delete any suspect settings from config. 1625 $like = $DB->sql_like('name', '?', true, true, false, '|'); 1626 $params = array($DB->sql_like_escape($plugin.'_', '|') . '%'); 1627 $DB->delete_records_select('config', $like, $params); 1628 // Finally clear both the plugin cache and the core cache (suspect settings now removed from core). 1629 cache_helper::invalidate_by_definition('core', 'config', array(), array('core', $plugin)); 1630 1631 return true; 1632 } 1633 1634 /** 1635 * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability. 1636 * 1637 * All users are verified if they still have the necessary capability. 1638 * 1639 * @param string $value the value of the config setting. 1640 * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor. 1641 * @param bool $includeadmins include administrators. 1642 * @return array of user objects. 1643 */ 1644 function get_users_from_config($value, $capability, $includeadmins = true) { 1645 if (empty($value) or $value === '$@NONE@$') { 1646 return array(); 1647 } 1648 1649 // We have to make sure that users still have the necessary capability, 1650 // it should be faster to fetch them all first and then test if they are present 1651 // instead of validating them one-by-one. 1652 $users = get_users_by_capability(context_system::instance(), $capability); 1653 if ($includeadmins) { 1654 $admins = get_admins(); 1655 foreach ($admins as $admin) { 1656 $users[$admin->id] = $admin; 1657 } 1658 } 1659 1660 if ($value === '$@ALL@$') { 1661 return $users; 1662 } 1663 1664 $result = array(); // Result in correct order. 1665 $allowed = explode(',', $value); 1666 foreach ($allowed as $uid) { 1667 if (isset($users[$uid])) { 1668 $user = $users[$uid]; 1669 $result[$user->id] = $user; 1670 } 1671 } 1672 1673 return $result; 1674 } 1675 1676 1677 /** 1678 * Invalidates browser caches and cached data in temp. 1679 * 1680 * @return void 1681 */ 1682 function purge_all_caches() { 1683 purge_caches(); 1684 } 1685 1686 /** 1687 * Selectively invalidate different types of cache. 1688 * 1689 * Purges the cache areas specified. By default, this will purge all caches but can selectively purge specific 1690 * areas alone or in combination. 1691 * 1692 * @param bool[] $options Specific parts of the cache to purge. Valid options are: 1693 * 'muc' Purge MUC caches? 1694 * 'theme' Purge theme cache? 1695 * 'lang' Purge language string cache? 1696 * 'js' Purge javascript cache? 1697 * 'filter' Purge text filter cache? 1698 * 'other' Purge all other caches? 1699 */ 1700 function purge_caches($options = []) { 1701 $defaults = array_fill_keys(['muc', 'theme', 'lang', 'js', 'template', 'filter', 'other'], false); 1702 if (empty(array_filter($options))) { 1703 $options = array_fill_keys(array_keys($defaults), true); // Set all options to true. 1704 } else { 1705 $options = array_merge($defaults, array_intersect_key($options, $defaults)); // Override defaults with specified options. 1706 } 1707 if ($options['muc']) { 1708 cache_helper::purge_all(); 1709 } 1710 if ($options['theme']) { 1711 theme_reset_all_caches(); 1712 } 1713 if ($options['lang']) { 1714 get_string_manager()->reset_caches(); 1715 } 1716 if ($options['js']) { 1717 js_reset_all_caches(); 1718 } 1719 if ($options['template']) { 1720 template_reset_all_caches(); 1721 } 1722 if ($options['filter']) { 1723 reset_text_filters_cache(); 1724 } 1725 if ($options['other']) { 1726 purge_other_caches(); 1727 } 1728 } 1729 1730 /** 1731 * Purge all non-MUC caches not otherwise purged in purge_caches. 1732 * 1733 * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at 1734 * {@link phpunit_util::reset_dataroot()} 1735 */ 1736 function purge_other_caches() { 1737 global $DB, $CFG; 1738 if (class_exists('core_plugin_manager')) { 1739 core_plugin_manager::reset_caches(); 1740 } 1741 1742 // Bump up cacherev field for all courses. 1743 try { 1744 increment_revision_number('course', 'cacherev', ''); 1745 } catch (moodle_exception $e) { 1746 // Ignore exception since this function is also called before upgrade script when field course.cacherev does not exist yet. 1747 } 1748 1749 $DB->reset_caches(); 1750 1751 // Purge all other caches: rss, simplepie, etc. 1752 clearstatcache(); 1753 remove_dir($CFG->cachedir.'', true); 1754 1755 // Make sure cache dir is writable, throws exception if not. 1756 make_cache_directory(''); 1757 1758 // This is the only place where we purge local caches, we are only adding files there. 1759 // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes. 1760 remove_dir($CFG->localcachedir, true); 1761 set_config('localcachedirpurged', time()); 1762 make_localcache_directory('', true); 1763 \core\task\manager::clear_static_caches(); 1764 } 1765 1766 /** 1767 * Get volatile flags 1768 * 1769 * @param string $type 1770 * @param int $changedsince default null 1771 * @return array records array 1772 */ 1773 function get_cache_flags($type, $changedsince = null) { 1774 global $DB; 1775 1776 $params = array('type' => $type, 'expiry' => time()); 1777 $sqlwhere = "flagtype = :type AND expiry >= :expiry"; 1778 if ($changedsince !== null) { 1779 $params['changedsince'] = $changedsince; 1780 $sqlwhere .= " AND timemodified > :changedsince"; 1781 } 1782 $cf = array(); 1783 if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) { 1784 foreach ($flags as $flag) { 1785 $cf[$flag->name] = $flag->value; 1786 } 1787 } 1788 return $cf; 1789 } 1790 1791 /** 1792 * Get volatile flags 1793 * 1794 * @param string $type 1795 * @param string $name 1796 * @param int $changedsince default null 1797 * @return string|false The cache flag value or false 1798 */ 1799 function get_cache_flag($type, $name, $changedsince=null) { 1800 global $DB; 1801 1802 $params = array('type' => $type, 'name' => $name, 'expiry' => time()); 1803 1804 $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry"; 1805 if ($changedsince !== null) { 1806 $params['changedsince'] = $changedsince; 1807 $sqlwhere .= " AND timemodified > :changedsince"; 1808 } 1809 1810 return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params); 1811 } 1812 1813 /** 1814 * Set a volatile flag 1815 * 1816 * @param string $type the "type" namespace for the key 1817 * @param string $name the key to set 1818 * @param string $value the value to set (without magic quotes) - null will remove the flag 1819 * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs 1820 * @return bool Always returns true 1821 */ 1822 function set_cache_flag($type, $name, $value, $expiry = null) { 1823 global $DB; 1824 1825 $timemodified = time(); 1826 if ($expiry === null || $expiry < $timemodified) { 1827 $expiry = $timemodified + 24 * 60 * 60; 1828 } else { 1829 $expiry = (int)$expiry; 1830 } 1831 1832 if ($value === null) { 1833 unset_cache_flag($type, $name); 1834 return true; 1835 } 1836 1837 if ($f = $DB->get_record('cache_flags', array('name' => $name, 'flagtype' => $type), '*', IGNORE_MULTIPLE)) { 1838 // This is a potential problem in DEBUG_DEVELOPER. 1839 if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) { 1840 return true; // No need to update. 1841 } 1842 $f->value = $value; 1843 $f->expiry = $expiry; 1844 $f->timemodified = $timemodified; 1845 $DB->update_record('cache_flags', $f); 1846 } else { 1847 $f = new stdClass(); 1848 $f->flagtype = $type; 1849 $f->name = $name; 1850 $f->value = $value; 1851 $f->expiry = $expiry; 1852 $f->timemodified = $timemodified; 1853 $DB->insert_record('cache_flags', $f); 1854 } 1855 return true; 1856 } 1857 1858 /** 1859 * Removes a single volatile flag 1860 * 1861 * @param string $type the "type" namespace for the key 1862 * @param string $name the key to set 1863 * @return bool 1864 */ 1865 function unset_cache_flag($type, $name) { 1866 global $DB; 1867 $DB->delete_records('cache_flags', array('name' => $name, 'flagtype' => $type)); 1868 return true; 1869 } 1870 1871 /** 1872 * Garbage-collect volatile flags 1873 * 1874 * @return bool Always returns true 1875 */ 1876 function gc_cache_flags() { 1877 global $DB; 1878 $DB->delete_records_select('cache_flags', 'expiry < ?', array(time())); 1879 return true; 1880 } 1881 1882 // USER PREFERENCE API. 1883 1884 /** 1885 * Refresh user preference cache. This is used most often for $USER 1886 * object that is stored in session, but it also helps with performance in cron script. 1887 * 1888 * Preferences for each user are loaded on first use on every page, then again after the timeout expires. 1889 * 1890 * @package core 1891 * @category preference 1892 * @access public 1893 * @param stdClass $user User object. Preferences are preloaded into 'preference' property 1894 * @param int $cachelifetime Cache life time on the current page (in seconds) 1895 * @throws coding_exception 1896 * @return null 1897 */ 1898 function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120) { 1899 global $DB; 1900 // Static cache, we need to check on each page load, not only every 2 minutes. 1901 static $loadedusers = array(); 1902 1903 if (!isset($user->id)) { 1904 throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field'); 1905 } 1906 1907 if (empty($user->id) or isguestuser($user->id)) { 1908 // No permanent storage for not-logged-in users and guest. 1909 if (!isset($user->preference)) { 1910 $user->preference = array(); 1911 } 1912 return; 1913 } 1914 1915 $timenow = time(); 1916 1917 if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) { 1918 // Already loaded at least once on this page. Are we up to date? 1919 if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) { 1920 // No need to reload - we are on the same page and we loaded prefs just a moment ago. 1921 return; 1922 1923 } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) { 1924 // No change since the lastcheck on this page. 1925 $user->preference['_lastloaded'] = $timenow; 1926 return; 1927 } 1928 } 1929 1930 // OK, so we have to reload all preferences. 1931 $loadedusers[$user->id] = true; 1932 $user->preference = $DB->get_records_menu('user_preferences', array('userid' => $user->id), '', 'name,value'); // All values. 1933 $user->preference['_lastloaded'] = $timenow; 1934 } 1935 1936 /** 1937 * Called from set/unset_user_preferences, so that the prefs can be correctly reloaded in different sessions. 1938 * 1939 * NOTE: internal function, do not call from other code. 1940 * 1941 * @package core 1942 * @access private 1943 * @param integer $userid the user whose prefs were changed. 1944 */ 1945 function mark_user_preferences_changed($userid) { 1946 global $CFG; 1947 1948 if (empty($userid) or isguestuser($userid)) { 1949 // No cache flags for guest and not-logged-in users. 1950 return; 1951 } 1952 1953 set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout); 1954 } 1955 1956 /** 1957 * Sets a preference for the specified user. 1958 * 1959 * If a $user object is submitted it's 'preference' property is used for the preferences cache. 1960 * 1961 * When additional validation/permission check is needed it is better to use {@see useredit_update_user_preference()} 1962 * 1963 * @package core 1964 * @category preference 1965 * @access public 1966 * @param string $name The key to set as preference for the specified user 1967 * @param string $value The value to set for the $name key in the specified user's 1968 * record, null means delete current value. 1969 * @param stdClass|int|null $user A moodle user object or id, null means current user 1970 * @throws coding_exception 1971 * @return bool Always true or exception 1972 */ 1973 function set_user_preference($name, $value, $user = null) { 1974 global $USER, $DB; 1975 1976 if (empty($name) or is_numeric($name) or $name === '_lastloaded') { 1977 throw new coding_exception('Invalid preference name in set_user_preference() call'); 1978 } 1979 1980 if (is_null($value)) { 1981 // Null means delete current. 1982 return unset_user_preference($name, $user); 1983 } else if (is_object($value)) { 1984 throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed'); 1985 } else if (is_array($value)) { 1986 throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed'); 1987 } 1988 // Value column maximum length is 1333 characters. 1989 $value = (string)$value; 1990 if (core_text::strlen($value) > 1333) { 1991 throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column'); 1992 } 1993 1994 if (is_null($user)) { 1995 $user = $USER; 1996 } else if (isset($user->id)) { 1997 // It is a valid object. 1998 } else if (is_numeric($user)) { 1999 $user = (object)array('id' => (int)$user); 2000 } else { 2001 throw new coding_exception('Invalid $user parameter in set_user_preference() call'); 2002 } 2003 2004 check_user_preferences_loaded($user); 2005 2006 if (empty($user->id) or isguestuser($user->id)) { 2007 // No permanent storage for not-logged-in users and guest. 2008 $user->preference[$name] = $value; 2009 return true; 2010 } 2011 2012 if ($preference = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => $name))) { 2013 if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) { 2014 // Preference already set to this value. 2015 return true; 2016 } 2017 $DB->set_field('user_preferences', 'value', $value, array('id' => $preference->id)); 2018 2019 } else { 2020 $preference = new stdClass(); 2021 $preference->userid = $user->id; 2022 $preference->name = $name; 2023 $preference->value = $value; 2024 $DB->insert_record('user_preferences', $preference); 2025 } 2026 2027 // Update value in cache. 2028 $user->preference[$name] = $value; 2029 // Update the $USER in case where we've not a direct reference to $USER. 2030 if ($user !== $USER && $user->id == $USER->id) { 2031 $USER->preference[$name] = $value; 2032 } 2033 2034 // Set reload flag for other sessions. 2035 mark_user_preferences_changed($user->id); 2036 2037 return true; 2038 } 2039 2040 /** 2041 * Sets a whole array of preferences for the current user 2042 * 2043 * If a $user object is submitted it's 'preference' property is used for the preferences cache. 2044 * 2045 * @package core 2046 * @category preference 2047 * @access public 2048 * @param array $prefarray An array of key/value pairs to be set 2049 * @param stdClass|int|null $user A moodle user object or id, null means current user 2050 * @return bool Always true or exception 2051 */ 2052 function set_user_preferences(array $prefarray, $user = null) { 2053 foreach ($prefarray as $name => $value) { 2054 set_user_preference($name, $value, $user); 2055 } 2056 return true; 2057 } 2058 2059 /** 2060 * Unsets a preference completely by deleting it from the database 2061 * 2062 * If a $user object is submitted it's 'preference' property is used for the preferences cache. 2063 * 2064 * @package core 2065 * @category preference 2066 * @access public 2067 * @param string $name The key to unset as preference for the specified user 2068 * @param stdClass|int|null $user A moodle user object or id, null means current user 2069 * @throws coding_exception 2070 * @return bool Always true or exception 2071 */ 2072 function unset_user_preference($name, $user = null) { 2073 global $USER, $DB; 2074 2075 if (empty($name) or is_numeric($name) or $name === '_lastloaded') { 2076 throw new coding_exception('Invalid preference name in unset_user_preference() call'); 2077 } 2078 2079 if (is_null($user)) { 2080 $user = $USER; 2081 } else if (isset($user->id)) { 2082 // It is a valid object. 2083 } else if (is_numeric($user)) { 2084 $user = (object)array('id' => (int)$user); 2085 } else { 2086 throw new coding_exception('Invalid $user parameter in unset_user_preference() call'); 2087 } 2088 2089 check_user_preferences_loaded($user); 2090 2091 if (empty($user->id) or isguestuser($user->id)) { 2092 // No permanent storage for not-logged-in user and guest. 2093 unset($user->preference[$name]); 2094 return true; 2095 } 2096 2097 // Delete from DB. 2098 $DB->delete_records('user_preferences', array('userid' => $user->id, 'name' => $name)); 2099 2100 // Delete the preference from cache. 2101 unset($user->preference[$name]); 2102 // Update the $USER in case where we've not a direct reference to $USER. 2103 if ($user !== $USER && $user->id == $USER->id) { 2104 unset($USER->preference[$name]); 2105 } 2106 2107 // Set reload flag for other sessions. 2108 mark_user_preferences_changed($user->id); 2109 2110 return true; 2111 } 2112 2113 /** 2114 * Used to fetch user preference(s) 2115 * 2116 * If no arguments are supplied this function will return 2117 * all of the current user preferences as an array. 2118 * 2119 * If a name is specified then this function 2120 * attempts to return that particular preference value. If 2121 * none is found, then the optional value $default is returned, 2122 * otherwise null. 2123 * 2124 * If a $user object is submitted it's 'preference' property is used for the preferences cache. 2125 * 2126 * @package core 2127 * @category preference 2128 * @access public 2129 * @param string $name Name of the key to use in finding a preference value 2130 * @param mixed|null $default Value to be returned if the $name key is not set in the user preferences 2131 * @param stdClass|int|null $user A moodle user object or id, null means current user 2132 * @throws coding_exception 2133 * @return string|mixed|null A string containing the value of a single preference. An 2134 * array with all of the preferences or null 2135 */ 2136 function get_user_preferences($name = null, $default = null, $user = null) { 2137 global $USER; 2138 2139 if (is_null($name)) { 2140 // All prefs. 2141 } else if (is_numeric($name) or $name === '_lastloaded') { 2142 throw new coding_exception('Invalid preference name in get_user_preferences() call'); 2143 } 2144 2145 if (is_null($user)) { 2146 $user = $USER; 2147 } else if (isset($user->id)) { 2148 // Is a valid object. 2149 } else if (is_numeric($user)) { 2150 if ($USER->id == $user) { 2151 $user = $USER; 2152 } else { 2153 $user = (object)array('id' => (int)$user); 2154 } 2155 } else { 2156 throw new coding_exception('Invalid $user parameter in get_user_preferences() call'); 2157 } 2158 2159 check_user_preferences_loaded($user); 2160 2161 if (empty($name)) { 2162 // All values. 2163 return $user->preference; 2164 } else if (isset($user->preference[$name])) { 2165 // The single string value. 2166 return $user->preference[$name]; 2167 } else { 2168 // Default value (null if not specified). 2169 return $default; 2170 } 2171 } 2172 2173 // FUNCTIONS FOR HANDLING TIME. 2174 2175 /** 2176 * Given Gregorian date parts in user time produce a GMT timestamp. 2177 * 2178 * @package core 2179 * @category time 2180 * @param int $year The year part to create timestamp of 2181 * @param int $month The month part to create timestamp of 2182 * @param int $day The day part to create timestamp of 2183 * @param int $hour The hour part to create timestamp of 2184 * @param int $minute The minute part to create timestamp of 2185 * @param int $second The second part to create timestamp of 2186 * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset. 2187 * if 99 then default user's timezone is used {@link https://moodledev.io/docs/apis/subsystems/time#timezone} 2188 * @param bool $applydst Toggle Daylight Saving Time, default true, will be 2189 * applied only if timezone is 99 or string. 2190 * @return int GMT timestamp 2191 */ 2192 function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) { 2193 $date = new DateTime('now', core_date::get_user_timezone_object($timezone)); 2194 $date->setDate((int)$year, (int)$month, (int)$day); 2195 $date->setTime((int)$hour, (int)$minute, (int)$second); 2196 2197 $time = $date->getTimestamp(); 2198 2199 if ($time === false) { 2200 throw new coding_exception('getTimestamp() returned false, please ensure you have passed correct values.'. 2201 ' This can fail if year is more than 2038 and OS is 32 bit windows'); 2202 } 2203 2204 // Moodle BC DST stuff. 2205 if (!$applydst) { 2206 $time += dst_offset_on($time, $timezone); 2207 } 2208 2209 return $time; 2210 2211 } 2212 2213 /** 2214 * Format a date/time (seconds) as weeks, days, hours etc as needed 2215 * 2216 * Given an amount of time in seconds, returns string 2217 * formatted nicely as years, days, hours etc as needed 2218 * 2219 * @package core 2220 * @category time 2221 * @uses MINSECS 2222 * @uses HOURSECS 2223 * @uses DAYSECS 2224 * @uses YEARSECS 2225 * @param int $totalsecs Time in seconds 2226 * @param stdClass $str Should be a time object 2227 * @return string A nicely formatted date/time string 2228 */ 2229 function format_time($totalsecs, $str = null) { 2230 2231 $totalsecs = abs($totalsecs); 2232 2233 if (!$str) { 2234 // Create the str structure the slow way. 2235 $str = new stdClass(); 2236 $str->day = get_string('day'); 2237 $str->days = get_string('days'); 2238 $str->hour = get_string('hour'); 2239 $str->hours = get_string('hours'); 2240 $str->min = get_string('min'); 2241 $str->mins = get_string('mins'); 2242 $str->sec = get_string('sec'); 2243 $str->secs = get_string('secs'); 2244 $str->year = get_string('year'); 2245 $str->years = get_string('years'); 2246 } 2247 2248 $years = floor($totalsecs/YEARSECS); 2249 $remainder = $totalsecs - ($years*YEARSECS); 2250 $days = floor($remainder/DAYSECS); 2251 $remainder = $totalsecs - ($days*DAYSECS); 2252 $hours = floor($remainder/HOURSECS); 2253 $remainder = $remainder - ($hours*HOURSECS); 2254 $mins = floor($remainder/MINSECS); 2255 $secs = $remainder - ($mins*MINSECS); 2256 2257 $ss = ($secs == 1) ? $str->sec : $str->secs; 2258 $sm = ($mins == 1) ? $str->min : $str->mins; 2259 $sh = ($hours == 1) ? $str->hour : $str->hours; 2260 $sd = ($days == 1) ? $str->day : $str->days; 2261 $sy = ($years == 1) ? $str->year : $str->years; 2262 2263 $oyears = ''; 2264 $odays = ''; 2265 $ohours = ''; 2266 $omins = ''; 2267 $osecs = ''; 2268 2269 if ($years) { 2270 $oyears = $years .' '. $sy; 2271 } 2272 if ($days) { 2273 $odays = $days .' '. $sd; 2274 } 2275 if ($hours) { 2276 $ohours = $hours .' '. $sh; 2277 } 2278 if ($mins) { 2279 $omins = $mins .' '. $sm; 2280 } 2281 if ($secs) { 2282 $osecs = $secs .' '. $ss; 2283 } 2284 2285 if ($years) { 2286 return trim($oyears .' '. $odays); 2287 } 2288 if ($days) { 2289 return trim($odays .' '. $ohours); 2290 } 2291 if ($hours) { 2292 return trim($ohours .' '. $omins); 2293 } 2294 if ($mins) { 2295 return trim($omins .' '. $osecs); 2296 } 2297 if ($secs) { 2298 return $osecs; 2299 } 2300 return get_string('now'); 2301 } 2302 2303 /** 2304 * Returns a formatted string that represents a date in user time. 2305 * 2306 * @package core 2307 * @category time 2308 * @param int $date the timestamp in UTC, as obtained from the database. 2309 * @param string $format strftime format. You should probably get this using 2310 * get_string('strftime...', 'langconfig'); 2311 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and 2312 * not 99 then daylight saving will not be added. 2313 * {@link https://moodledev.io/docs/apis/subsystems/time#timezone} 2314 * @param bool $fixday If true (default) then the leading zero from %d is removed. 2315 * If false then the leading zero is maintained. 2316 * @param bool $fixhour If true (default) then the leading zero from %I is removed. 2317 * @return string the formatted date/time. 2318 */ 2319 function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) { 2320 $calendartype = \core_calendar\type_factory::get_calendar_instance(); 2321 return $calendartype->timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour); 2322 } 2323 2324 /** 2325 * Returns a html "time" tag with both the exact user date with timezone information 2326 * as a datetime attribute in the W3C format, and the user readable date and time as text. 2327 * 2328 * @package core 2329 * @category time 2330 * @param int $date the timestamp in UTC, as obtained from the database. 2331 * @param string $format strftime format. You should probably get this using 2332 * get_string('strftime...', 'langconfig'); 2333 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and 2334 * not 99 then daylight saving will not be added. 2335 * {@link https://moodledev.io/docs/apis/subsystems/time#timezone} 2336 * @param bool $fixday If true (default) then the leading zero from %d is removed. 2337 * If false then the leading zero is maintained. 2338 * @param bool $fixhour If true (default) then the leading zero from %I is removed. 2339 * @return string the formatted date/time. 2340 */ 2341 function userdate_htmltime($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) { 2342 $userdatestr = userdate($date, $format, $timezone, $fixday, $fixhour); 2343 if (CLI_SCRIPT && !PHPUNIT_TEST) { 2344 return $userdatestr; 2345 } 2346 $machinedate = new DateTime(); 2347 $machinedate->setTimestamp(intval($date)); 2348 $machinedate->setTimezone(core_date::get_user_timezone_object()); 2349 2350 return html_writer::tag('time', $userdatestr, ['datetime' => $machinedate->format(DateTime::W3C)]); 2351 } 2352 2353 /** 2354 * Returns a formatted date ensuring it is UTF-8. 2355 * 2356 * If we are running under Windows convert to Windows encoding and then back to UTF-8 2357 * (because it's impossible to specify UTF-8 to fetch locale info in Win32). 2358 * 2359 * @param int $date the timestamp - since Moodle 2.9 this is a real UTC timestamp 2360 * @param string $format strftime format. 2361 * @param int|float|string $tz the user timezone 2362 * @return string the formatted date/time. 2363 * @since Moodle 2.3.3 2364 */ 2365 function date_format_string($date, $format, $tz = 99) { 2366 2367 date_default_timezone_set(core_date::get_user_timezone($tz)); 2368 2369 if (date('A', 0) === date('A', HOURSECS * 18)) { 2370 $datearray = getdate($date); 2371 $format = str_replace([ 2372 '%P', 2373 '%p', 2374 ], [ 2375 $datearray['hours'] < 12 ? get_string('am', 'langconfig') : get_string('pm', 'langconfig'), 2376 $datearray['hours'] < 12 ? get_string('amcaps', 'langconfig') : get_string('pmcaps', 'langconfig'), 2377 ], $format); 2378 } 2379 2380 $datestring = core_date::strftime($format, $date); 2381 core_date::set_default_server_timezone(); 2382 2383 return $datestring; 2384 } 2385 2386 /** 2387 * Given a $time timestamp in GMT (seconds since epoch), 2388 * returns an array that represents the Gregorian date in user time 2389 * 2390 * @package core 2391 * @category time 2392 * @param int $time Timestamp in GMT 2393 * @param float|int|string $timezone user timezone 2394 * @return array An array that represents the date in user time 2395 */ 2396 function usergetdate($time, $timezone=99) { 2397 if ($time === null) { 2398 // PHP8 and PHP7 return different results when getdate(null) is called. 2399 // Display warning and cast to 0 to make sure the usergetdate() behaves consistently on all versions of PHP. 2400 // In the future versions of Moodle we may consider adding a strict typehint. 2401 debugging('usergetdate() expects parameter $time to be int, null given', DEBUG_DEVELOPER); 2402 $time = 0; 2403 } 2404 2405 date_default_timezone_set(core_date::get_user_timezone($timezone)); 2406 $result = getdate($time); 2407 core_date::set_default_server_timezone(); 2408 2409 return $result; 2410 } 2411 2412 /** 2413 * Given a GMT timestamp (seconds since epoch), offsets it by 2414 * the timezone. eg 3pm in India is 3pm GMT - 7 * 3600 seconds 2415 * 2416 * NOTE: this function does not include DST properly, 2417 * you should use the PHP date stuff instead! 2418 * 2419 * @package core 2420 * @category time 2421 * @param int $date Timestamp in GMT 2422 * @param float|int|string $timezone user timezone 2423 * @return int 2424 */ 2425 function usertime($date, $timezone=99) { 2426 $userdate = new DateTime('@' . $date); 2427 $userdate->setTimezone(core_date::get_user_timezone_object($timezone)); 2428 $dst = dst_offset_on($date, $timezone); 2429 2430 return $date - $userdate->getOffset() + $dst; 2431 } 2432 2433 /** 2434 * Get a formatted string representation of an interval between two unix timestamps. 2435 * 2436 * E.g. 2437 * $intervalstring = get_time_interval_string(12345600, 12345660); 2438 * Will produce the string: 2439 * '0d 0h 1m' 2440 * 2441 * @param int $time1 unix timestamp 2442 * @param int $time2 unix timestamp 2443 * @param string $format string (can be lang string) containing format chars: https://www.php.net/manual/en/dateinterval.format.php. 2444 * @return string the formatted string describing the time difference, e.g. '10d 11h 45m'. 2445 */ 2446 function get_time_interval_string(int $time1, int $time2, string $format = ''): string { 2447 $dtdate = new DateTime(); 2448 $dtdate->setTimeStamp($time1); 2449 $dtdate2 = new DateTime(); 2450 $dtdate2->setTimeStamp($time2); 2451 $interval = $dtdate2->diff($dtdate); 2452 $format = empty($format) ? get_string('dateintervaldayshoursmins', 'langconfig') : $format; 2453 return $interval->format($format); 2454 } 2455 2456 /** 2457 * Given a time, return the GMT timestamp of the most recent midnight 2458 * for the current user. 2459 * 2460 * @package core 2461 * @category time 2462 * @param int $date Timestamp in GMT 2463 * @param float|int|string $timezone user timezone 2464 * @return int Returns a GMT timestamp 2465 */ 2466 function usergetmidnight($date, $timezone=99) { 2467 2468 $userdate = usergetdate($date, $timezone); 2469 2470 // Time of midnight of this user's day, in GMT. 2471 return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone); 2472 2473 } 2474 2475 /** 2476 * Returns a string that prints the user's timezone 2477 * 2478 * @package core 2479 * @category time 2480 * @param float|int|string $timezone user timezone 2481 * @return string 2482 */ 2483 function usertimezone($timezone=99) { 2484 $tz = core_date::get_user_timezone($timezone); 2485 return core_date::get_localised_timezone($tz); 2486 } 2487 2488 /** 2489 * Returns a float or a string which denotes the user's timezone 2490 * 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) 2491 * means that for this timezone there are also DST rules to be taken into account 2492 * Checks various settings and picks the most dominant of those which have a value 2493 * 2494 * @package core 2495 * @category time 2496 * @param float|int|string $tz timezone to calculate GMT time offset before 2497 * calculating user timezone, 99 is default user timezone 2498 * {@link https://moodledev.io/docs/apis/subsystems/time#timezone} 2499 * @return float|string 2500 */ 2501 function get_user_timezone($tz = 99) { 2502 global $USER, $CFG; 2503 2504 $timezones = array( 2505 $tz, 2506 isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99, 2507 isset($USER->timezone) ? $USER->timezone : 99, 2508 isset($CFG->timezone) ? $CFG->timezone : 99, 2509 ); 2510 2511 $tz = 99; 2512 2513 // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array. 2514 foreach ($timezones as $nextvalue) { 2515 if ((empty($tz) && !is_numeric($tz)) || $tz == 99) { 2516 $tz = $nextvalue; 2517 } 2518 } 2519 return is_numeric($tz) ? (float) $tz : $tz; 2520 } 2521 2522 /** 2523 * Calculates the Daylight Saving Offset for a given date/time (timestamp) 2524 * - Note: Daylight saving only works for string timezones and not for float. 2525 * 2526 * @package core 2527 * @category time 2528 * @param int $time must NOT be compensated at all, it has to be a pure timestamp 2529 * @param int|float|string $strtimezone user timezone 2530 * @return int 2531 */ 2532 function dst_offset_on($time, $strtimezone = null) { 2533 $tz = core_date::get_user_timezone($strtimezone); 2534 $date = new DateTime('@' . $time); 2535 $date->setTimezone(new DateTimeZone($tz)); 2536 if ($date->format('I') == '1') { 2537 if ($tz === 'Australia/Lord_Howe') { 2538 return 1800; 2539 } 2540 return 3600; 2541 } 2542 return 0; 2543 } 2544 2545 /** 2546 * Calculates when the day appears in specific month 2547 * 2548 * @package core 2549 * @category time 2550 * @param int $startday starting day of the month 2551 * @param int $weekday The day when week starts (normally taken from user preferences) 2552 * @param int $month The month whose day is sought 2553 * @param int $year The year of the month whose day is sought 2554 * @return int 2555 */ 2556 function find_day_in_month($startday, $weekday, $month, $year) { 2557 $calendartype = \core_calendar\type_factory::get_calendar_instance(); 2558 2559 $daysinmonth = days_in_month($month, $year); 2560 $daysinweek = count($calendartype->get_weekdays()); 2561 2562 if ($weekday == -1) { 2563 // Don't care about weekday, so return: 2564 // abs($startday) if $startday != -1 2565 // $daysinmonth otherwise. 2566 return ($startday == -1) ? $daysinmonth : abs($startday); 2567 } 2568 2569 // From now on we 're looking for a specific weekday. 2570 // Give "end of month" its actual value, since we know it. 2571 if ($startday == -1) { 2572 $startday = -1 * $daysinmonth; 2573 } 2574 2575 // Starting from day $startday, the sign is the direction. 2576 if ($startday < 1) { 2577 $startday = abs($startday); 2578 $lastmonthweekday = dayofweek($daysinmonth, $month, $year); 2579 2580 // This is the last such weekday of the month. 2581 $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday; 2582 if ($lastinmonth > $daysinmonth) { 2583 $lastinmonth -= $daysinweek; 2584 } 2585 2586 // Find the first such weekday <= $startday. 2587 while ($lastinmonth > $startday) { 2588 $lastinmonth -= $daysinweek; 2589 } 2590 2591 return $lastinmonth; 2592 } else { 2593 $indexweekday = dayofweek($startday, $month, $year); 2594 2595 $diff = $weekday - $indexweekday; 2596 if ($diff < 0) { 2597 $diff += $daysinweek; 2598 } 2599 2600 // This is the first such weekday of the month equal to or after $startday. 2601 $firstfromindex = $startday + $diff; 2602 2603 return $firstfromindex; 2604 } 2605 } 2606 2607 /** 2608 * Calculate the number of days in a given month 2609 * 2610 * @package core 2611 * @category time 2612 * @param int $month The month whose day count is sought 2613 * @param int $year The year of the month whose day count is sought 2614 * @return int 2615 */ 2616 function days_in_month($month, $year) { 2617 $calendartype = \core_calendar\type_factory::get_calendar_instance(); 2618 return $calendartype->get_num_days_in_month($year, $month); 2619 } 2620 2621 /** 2622 * Calculate the position in the week of a specific calendar day 2623 * 2624 * @package core 2625 * @category time 2626 * @param int $day The day of the date whose position in the week is sought 2627 * @param int $month The month of the date whose position in the week is sought 2628 * @param int $year The year of the date whose position in the week is sought 2629 * @return int 2630 */ 2631 function dayofweek($day, $month, $year) { 2632 $calendartype = \core_calendar\type_factory::get_calendar_instance(); 2633 return $calendartype->get_weekday($year, $month, $day); 2634 } 2635 2636 // USER AUTHENTICATION AND LOGIN. 2637 2638 /** 2639 * Returns full login url. 2640 * 2641 * Any form submissions for authentication to this URL must include username, 2642 * password as well as a logintoken generated by \core\session\manager::get_login_token(). 2643 * 2644 * @return string login url 2645 */ 2646 function get_login_url() { 2647 global $CFG; 2648 2649 return "$CFG->wwwroot/login/index.php"; 2650 } 2651 2652 /** 2653 * This function checks that the current user is logged in and has the 2654 * required privileges 2655 * 2656 * This function checks that the current user is logged in, and optionally 2657 * whether they are allowed to be in a particular course and view a particular 2658 * course module. 2659 * If they are not logged in, then it redirects them to the site login unless 2660 * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which 2661 * case they are automatically logged in as guests. 2662 * If $courseid is given and the user is not enrolled in that course then the 2663 * user is redirected to the course enrolment page. 2664 * If $cm is given and the course module is hidden and the user is not a teacher 2665 * in the course then the user is redirected to the course home page. 2666 * 2667 * When $cm parameter specified, this function sets page layout to 'module'. 2668 * You need to change it manually later if some other layout needed. 2669 * 2670 * @package core_access 2671 * @category access 2672 * 2673 * @param mixed $courseorid id of the course or course object 2674 * @param bool $autologinguest default true 2675 * @param object $cm course module object 2676 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to 2677 * true. Used to avoid (=false) some scripts (file.php...) to set that variable, 2678 * in order to keep redirects working properly. MDL-14495 2679 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions 2680 * @return mixed Void, exit, and die depending on path 2681 * @throws coding_exception 2682 * @throws require_login_exception 2683 * @throws moodle_exception 2684 */ 2685 function require_login($courseorid = null, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) { 2686 global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT; 2687 2688 // Must not redirect when byteserving already started. 2689 if (!empty($_SERVER['HTTP_RANGE'])) { 2690 $preventredirect = true; 2691 } 2692 2693 if (AJAX_SCRIPT) { 2694 // We cannot redirect for AJAX scripts either. 2695 $preventredirect = true; 2696 } 2697 2698 // Setup global $COURSE, themes, language and locale. 2699 if (!empty($courseorid)) { 2700 if (is_object($courseorid)) { 2701 $course = $courseorid; 2702 } else if ($courseorid == SITEID) { 2703 $course = clone($SITE); 2704 } else { 2705 $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST); 2706 } 2707 if ($cm) { 2708 if ($cm->course != $course->id) { 2709 throw new coding_exception('course and cm parameters in require_login() call do not match!!'); 2710 } 2711 // Make sure we have a $cm from get_fast_modinfo as this contains activity access details. 2712 if (!($cm instanceof cm_info)) { 2713 // Note: nearly all pages call get_fast_modinfo anyway and it does not make any 2714 // db queries so this is not really a performance concern, however it is obviously 2715 // better if you use get_fast_modinfo to get the cm before calling this. 2716 $modinfo = get_fast_modinfo($course); 2717 $cm = $modinfo->get_cm($cm->id); 2718 } 2719 } 2720 } else { 2721 // Do not touch global $COURSE via $PAGE->set_course(), 2722 // the reasons is we need to be able to call require_login() at any time!! 2723 $course = $SITE; 2724 if ($cm) { 2725 throw new coding_exception('cm parameter in require_login() requires valid course parameter!'); 2726 } 2727 } 2728 2729 // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false. 2730 // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future 2731 // risk leading the user back to the AJAX request URL. 2732 if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) { 2733 $setwantsurltome = false; 2734 } 2735 2736 // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour. 2737 if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !empty($CFG->dbsessions)) { 2738 if ($preventredirect) { 2739 throw new require_login_session_timeout_exception(); 2740 } else { 2741 if ($setwantsurltome) { 2742 $SESSION->wantsurl = qualified_me(); 2743 } 2744 redirect(get_login_url()); 2745 } 2746 } 2747 2748 // If the user is not even logged in yet then make sure they are. 2749 if (!isloggedin()) { 2750 if ($autologinguest && !empty($CFG->autologinguests)) { 2751 if (!$guest = get_complete_user_data('id', $CFG->siteguest)) { 2752 // Misconfigured site guest, just redirect to login page. 2753 redirect(get_login_url()); 2754 exit; // Never reached. 2755 } 2756 $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang; 2757 complete_user_login($guest); 2758 $USER->autologinguest = true; 2759 $SESSION->lang = $lang; 2760 } else { 2761 // NOTE: $USER->site check was obsoleted by session test cookie, $USER->confirmed test is in login/index.php. 2762 if ($preventredirect) { 2763 throw new require_login_exception('You are not logged in'); 2764 } 2765 2766 if ($setwantsurltome) { 2767 $SESSION->wantsurl = qualified_me(); 2768 } 2769 2770 // Give auth plugins an opportunity to authenticate or redirect to an external login page 2771 $authsequence = get_enabled_auth_plugins(); // Auths, in sequence. 2772 foreach($authsequence as $authname) { 2773 $authplugin = get_auth_plugin($authname); 2774 $authplugin->pre_loginpage_hook(); 2775 if (isloggedin()) { 2776 if ($cm) { 2777 $modinfo = get_fast_modinfo($course); 2778 $cm = $modinfo->get_cm($cm->id); 2779 } 2780 set_access_log_user(); 2781 break; 2782 } 2783 } 2784 2785 // If we're still not logged in then go to the login page 2786 if (!isloggedin()) { 2787 redirect(get_login_url()); 2788 exit; // Never reached. 2789 } 2790 } 2791 } 2792 2793 // Loginas as redirection if needed. 2794 if ($course->id != SITEID and \core\session\manager::is_loggedinas()) { 2795 if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) { 2796 if ($USER->loginascontext->instanceid != $course->id) { 2797 throw new \moodle_exception('loginasonecourse', '', 2798 $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid); 2799 } 2800 } 2801 } 2802 2803 // Check whether the user should be changing password (but only if it is REALLY them). 2804 if (get_user_preferences('auth_forcepasswordchange') && !\core\session\manager::is_loggedinas()) { 2805 $userauth = get_auth_plugin($USER->auth); 2806 if ($userauth->can_change_password() and !$preventredirect) { 2807 if ($setwantsurltome) { 2808 $SESSION->wantsurl = qualified_me(); 2809 } 2810 if ($changeurl = $userauth->change_password_url()) { 2811 // Use plugin custom url. 2812 redirect($changeurl); 2813 } else { 2814 // Use moodle internal method. 2815 redirect($CFG->wwwroot .'/login/change_password.php'); 2816 } 2817 } else if ($userauth->can_change_password()) { 2818 throw new moodle_exception('forcepasswordchangenotice'); 2819 } else { 2820 throw new moodle_exception('nopasswordchangeforced', 'auth'); 2821 } 2822 } 2823 2824 // Check that the user account is properly set up. If we can't redirect to 2825 // edit their profile and this is not a WS request, perform just the lax check. 2826 // It will allow them to use filepicker on the profile edit page. 2827 2828 if ($preventredirect && !WS_SERVER) { 2829 $usernotfullysetup = user_not_fully_set_up($USER, false); 2830 } else { 2831 $usernotfullysetup = user_not_fully_set_up($USER, true); 2832 } 2833 2834 if ($usernotfullysetup) { 2835 if ($preventredirect) { 2836 throw new moodle_exception('usernotfullysetup'); 2837 } 2838 if ($setwantsurltome) { 2839 $SESSION->wantsurl = qualified_me(); 2840 } 2841 redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&course='. SITEID); 2842 } 2843 2844 // Make sure the USER has a sesskey set up. Used for CSRF protection. 2845 sesskey(); 2846 2847 if (\core\session\manager::is_loggedinas()) { 2848 // During a "logged in as" session we should force all content to be cleaned because the 2849 // logged in user will be viewing potentially malicious user generated content. 2850 // See MDL-63786 for more details. 2851 $CFG->forceclean = true; 2852 } 2853 2854 $afterlogins = get_plugins_with_function('after_require_login', 'lib.php'); 2855 2856 // Do not bother admins with any formalities, except for activities pending deletion. 2857 if (is_siteadmin() && !($cm && $cm->deletioninprogress)) { 2858 // Set the global $COURSE. 2859 if ($cm) { 2860 $PAGE->set_cm($cm, $course); 2861 $PAGE->set_pagelayout('incourse'); 2862 } else if (!empty($courseorid)) { 2863 $PAGE->set_course($course); 2864 } 2865 // Set accesstime or the user will appear offline which messes up messaging. 2866 // Do not update access time for webservice or ajax requests. 2867 if (!WS_SERVER && !AJAX_SCRIPT) { 2868 user_accesstime_log($course->id); 2869 } 2870 2871 foreach ($afterlogins as $plugintype => $plugins) { 2872 foreach ($plugins as $pluginfunction) { 2873 $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); 2874 } 2875 } 2876 return; 2877 } 2878 2879 // Scripts have a chance to declare that $USER->policyagreed should not be checked. 2880 // This is mostly for places where users are actually accepting the policies, to avoid the redirect loop. 2881 if (!defined('NO_SITEPOLICY_CHECK')) { 2882 define('NO_SITEPOLICY_CHECK', false); 2883 } 2884 2885 // Check that the user has agreed to a site policy if there is one - do not test in case of admins. 2886 // Do not test if the script explicitly asked for skipping the site policies check. 2887 // Or if the user auth type is webservice. 2888 if (!$USER->policyagreed && !is_siteadmin() && !NO_SITEPOLICY_CHECK && $USER->auth !== 'webservice') { 2889 $manager = new \core_privacy\local\sitepolicy\manager(); 2890 if ($policyurl = $manager->get_redirect_url(isguestuser())) { 2891 if ($preventredirect) { 2892 throw new moodle_exception('sitepolicynotagreed', 'error', '', $policyurl->out()); 2893 } 2894 if ($setwantsurltome) { 2895 $SESSION->wantsurl = qualified_me(); 2896 } 2897 redirect($policyurl); 2898 } 2899 } 2900 2901 // Fetch the system context, the course context, and prefetch its child contexts. 2902 $sysctx = context_system::instance(); 2903 $coursecontext = context_course::instance($course->id, MUST_EXIST); 2904 if ($cm) { 2905 $cmcontext = context_module::instance($cm->id, MUST_EXIST); 2906 } else { 2907 $cmcontext = null; 2908 } 2909 2910 // If the site is currently under maintenance, then print a message. 2911 if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:maintenanceaccess', $sysctx)) { 2912 if ($preventredirect) { 2913 throw new require_login_exception('Maintenance in progress'); 2914 } 2915 $PAGE->set_context(null); 2916 print_maintenance_message(); 2917 } 2918 2919 // Make sure the course itself is not hidden. 2920 if ($course->id == SITEID) { 2921 // Frontpage can not be hidden. 2922 } else { 2923 if (is_role_switched($course->id)) { 2924 // When switching roles ignore the hidden flag - user had to be in course to do the switch. 2925 } else { 2926 if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) { 2927 // Originally there was also test of parent category visibility, BUT is was very slow in complex queries 2928 // involving "my courses" now it is also possible to simply hide all courses user is not enrolled in :-). 2929 if ($preventredirect) { 2930 throw new require_login_exception('Course is hidden'); 2931 } 2932 $PAGE->set_context(null); 2933 // We need to override the navigation URL as the course won't have been added to the navigation and thus 2934 // the navigation will mess up when trying to find it. 2935 navigation_node::override_active_url(new moodle_url('/')); 2936 notice(get_string('coursehidden'), $CFG->wwwroot .'/'); 2937 } 2938 } 2939 } 2940 2941 // Is the user enrolled? 2942 if ($course->id == SITEID) { 2943 // Everybody is enrolled on the frontpage. 2944 } else { 2945 if (\core\session\manager::is_loggedinas()) { 2946 // Make sure the REAL person can access this course first. 2947 $realuser = \core\session\manager::get_realuser(); 2948 if (!is_enrolled($coursecontext, $realuser->id, '', true) and 2949 !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) { 2950 if ($preventredirect) { 2951 throw new require_login_exception('Invalid course login-as access'); 2952 } 2953 $PAGE->set_context(null); 2954 echo $OUTPUT->header(); 2955 notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/'); 2956 } 2957 } 2958 2959 $access = false; 2960 2961 if (is_role_switched($course->id)) { 2962 // Ok, user had to be inside this course before the switch. 2963 $access = true; 2964 2965 } else if (is_viewing($coursecontext, $USER)) { 2966 // Ok, no need to mess with enrol. 2967 $access = true; 2968 2969 } else { 2970 if (isset($USER->enrol['enrolled'][$course->id])) { 2971 if ($USER->enrol['enrolled'][$course->id] > time()) { 2972 $access = true; 2973 if (isset($USER->enrol['tempguest'][$course->id])) { 2974 unset($USER->enrol['tempguest'][$course->id]); 2975 remove_temp_course_roles($coursecontext); 2976 } 2977 } else { 2978 // Expired. 2979 unset($USER->enrol['enrolled'][$course->id]); 2980 } 2981 } 2982 if (isset($USER->enrol['tempguest'][$course->id])) { 2983 if ($USER->enrol['tempguest'][$course->id] == 0) { 2984 $access = true; 2985 } else if ($USER->enrol['tempguest'][$course->id] > time()) { 2986 $access = true; 2987 } else { 2988 // Expired. 2989 unset($USER->enrol['tempguest'][$course->id]); 2990 remove_temp_course_roles($coursecontext); 2991 } 2992 } 2993 2994 if (!$access) { 2995 // Cache not ok. 2996 $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id); 2997 if ($until !== false) { 2998 // Active participants may always access, a timestamp in the future, 0 (always) or false. 2999 if ($until == 0) { 3000 $until = ENROL_MAX_TIMESTAMP; 3001 } 3002 $USER->enrol['enrolled'][$course->id] = $until; 3003 $access = true; 3004 3005 } else if (core_course_category::can_view_course_info($course)) { 3006 $params = array('courseid' => $course->id, 'status' => ENROL_INSTANCE_ENABLED); 3007 $instances = $DB->get_records('enrol', $params, 'sortorder, id ASC'); 3008 $enrols = enrol_get_plugins(true); 3009 // First ask all enabled enrol instances in course if they want to auto enrol user. 3010 foreach ($instances as $instance) { 3011 if (!isset($enrols[$instance->enrol])) { 3012 continue; 3013 } 3014 // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false. 3015 $until = $enrols[$instance->enrol]->try_autoenrol($instance); 3016 if ($until !== false) { 3017 if ($until == 0) { 3018 $until = ENROL_MAX_TIMESTAMP; 3019 } 3020 $USER->enrol['enrolled'][$course->id] = $until; 3021 $access = true; 3022 break; 3023 } 3024 } 3025 // If not enrolled yet try to gain temporary guest access. 3026 if (!$access) { 3027 foreach ($instances as $instance) { 3028 if (!isset($enrols[$instance->enrol])) { 3029 continue; 3030 } 3031 // Get a duration for the guest access, a timestamp in the future or false. 3032 $until = $enrols[$instance->enrol]->try_guestaccess($instance); 3033 if ($until !== false and $until > time()) { 3034 $USER->enrol['tempguest'][$course->id] = $until; 3035 $access = true; 3036 break; 3037 } 3038 } 3039 } 3040 } else { 3041 // User is not enrolled and is not allowed to browse courses here. 3042 if ($preventredirect) { 3043 throw new require_login_exception('Course is not available'); 3044 } 3045 $PAGE->set_context(null); 3046 // We need to override the navigation URL as the course won't have been added to the navigation and thus 3047 // the navigation will mess up when trying to find it. 3048 navigation_node::override_active_url(new moodle_url('/')); 3049 notice(get_string('coursehidden'), $CFG->wwwroot .'/'); 3050 } 3051 } 3052 } 3053 3054 if (!$access) { 3055 if ($preventredirect) { 3056 throw new require_login_exception('Not enrolled'); 3057 } 3058 if ($setwantsurltome) { 3059 $SESSION->wantsurl = qualified_me(); 3060 } 3061 redirect($CFG->wwwroot .'/enrol/index.php?id='. $course->id); 3062 } 3063 } 3064 3065 // Check whether the activity has been scheduled for deletion. If so, then deny access, even for admins. 3066 if ($cm && $cm->deletioninprogress) { 3067 if ($preventredirect) { 3068 throw new moodle_exception('activityisscheduledfordeletion'); 3069 } 3070 require_once($CFG->dirroot . '/course/lib.php'); 3071 redirect(course_get_url($course), get_string('activityisscheduledfordeletion', 'error')); 3072 } 3073 3074 // Check visibility of activity to current user; includes visible flag, conditional availability, etc. 3075 if ($cm && !$cm->uservisible) { 3076 if ($preventredirect) { 3077 throw new require_login_exception('Activity is hidden'); 3078 } 3079 // Get the error message that activity is not available and why (if explanation can be shown to the user). 3080 $PAGE->set_course($course); 3081 $renderer = $PAGE->get_renderer('course'); 3082 $message = $renderer->course_section_cm_unavailable_error_message($cm); 3083 redirect(course_get_url($course), $message, null, \core\output\notification::NOTIFY_ERROR); 3084 } 3085 3086 // Set the global $COURSE. 3087 if ($cm) { 3088 $PAGE->set_cm($cm, $course); 3089 $PAGE->set_pagelayout('incourse'); 3090 } else if (!empty($courseorid)) { 3091 $PAGE->set_course($course); 3092 } 3093 3094 foreach ($afterlogins as $plugintype => $plugins) { 3095 foreach ($plugins as $pluginfunction) { 3096 $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); 3097 } 3098 } 3099 3100 // Finally access granted, update lastaccess times. 3101 // Do not update access time for webservice or ajax requests. 3102 if (!WS_SERVER && !AJAX_SCRIPT) { 3103 user_accesstime_log($course->id); 3104 } 3105 } 3106 3107 /** 3108 * A convenience function for where we must be logged in as admin 3109 * @return void 3110 */ 3111 function require_admin() { 3112 require_login(null, false); 3113 require_capability('moodle/site:config', context_system::instance()); 3114 } 3115 3116 /** 3117 * This function just makes sure a user is logged out. 3118 * 3119 * @package core_access 3120 * @category access 3121 */ 3122 function require_logout() { 3123 global $USER, $DB; 3124 3125 if (!isloggedin()) { 3126 // This should not happen often, no need for hooks or events here. 3127 \core\session\manager::terminate_current(); 3128 return; 3129 } 3130 3131 // Execute hooks before action. 3132 $authplugins = array(); 3133 $authsequence = get_enabled_auth_plugins(); 3134 foreach ($authsequence as $authname) { 3135 $authplugins[$authname] = get_auth_plugin($authname); 3136 $authplugins[$authname]->prelogout_hook(); 3137 } 3138 3139 // Store info that gets removed during logout. 3140 $sid = session_id(); 3141 $event = \core\event\user_loggedout::create( 3142 array( 3143 'userid' => $USER->id, 3144 'objectid' => $USER->id, 3145 'other' => array('sessionid' => $sid), 3146 ) 3147 ); 3148 if ($session = $DB->get_record('sessions', array('sid'=>$sid))) { 3149 $event->add_record_snapshot('sessions', $session); 3150 } 3151 3152 // Clone of $USER object to be used by auth plugins. 3153 $user = fullclone($USER); 3154 3155 // Delete session record and drop $_SESSION content. 3156 \core\session\manager::terminate_current(); 3157 3158 // Trigger event AFTER action. 3159 $event->trigger(); 3160 3161 // Hook to execute auth plugins redirection after event trigger. 3162 foreach ($authplugins as $authplugin) { 3163 $authplugin->postlogout_hook($user); 3164 } 3165 } 3166 3167 /** 3168 * Weaker version of require_login() 3169 * 3170 * This is a weaker version of {@link require_login()} which only requires login 3171 * when called from within a course rather than the site page, unless 3172 * the forcelogin option is turned on. 3173 * @see require_login() 3174 * 3175 * @package core_access 3176 * @category access 3177 * 3178 * @param mixed $courseorid The course object or id in question 3179 * @param bool $autologinguest Allow autologin guests if that is wanted 3180 * @param object $cm Course activity module if known 3181 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to 3182 * true. Used to avoid (=false) some scripts (file.php...) to set that variable, 3183 * in order to keep redirects working properly. MDL-14495 3184 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions 3185 * @return void 3186 * @throws coding_exception 3187 */ 3188 function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) { 3189 global $CFG, $PAGE, $SITE; 3190 $issite = ((is_object($courseorid) and $courseorid->id == SITEID) 3191 or (!is_object($courseorid) and $courseorid == SITEID)); 3192 if ($issite && !empty($cm) && !($cm instanceof cm_info)) { 3193 // Note: nearly all pages call get_fast_modinfo anyway and it does not make any 3194 // db queries so this is not really a performance concern, however it is obviously 3195 // better if you use get_fast_modinfo to get the cm before calling this. 3196 if (is_object($courseorid)) { 3197 $course = $courseorid; 3198 } else { 3199 $course = clone($SITE); 3200 } 3201 $modinfo = get_fast_modinfo($course); 3202 $cm = $modinfo->get_cm($cm->id); 3203 } 3204 if (!empty($CFG->forcelogin)) { 3205 // Login required for both SITE and courses. 3206 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); 3207 3208 } else if ($issite && !empty($cm) and !$cm->uservisible) { 3209 // Always login for hidden activities. 3210 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); 3211 3212 } else if (isloggedin() && !isguestuser()) { 3213 // User is already logged in. Make sure the login is complete (user is fully setup, policies agreed). 3214 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); 3215 3216 } else if ($issite) { 3217 // Login for SITE not required. 3218 // We still need to instatiate PAGE vars properly so that things that rely on it like navigation function correctly. 3219 if (!empty($courseorid)) { 3220 if (is_object($courseorid)) { 3221 $course = $courseorid; 3222 } else { 3223 $course = clone $SITE; 3224 } 3225 if ($cm) { 3226 if ($cm->course != $course->id) { 3227 throw new coding_exception('course and cm parameters in require_course_login() call do not match!!'); 3228 } 3229 $PAGE->set_cm($cm, $course); 3230 $PAGE->set_pagelayout('incourse'); 3231 } else { 3232 $PAGE->set_course($course); 3233 } 3234 } else { 3235 // If $PAGE->course, and hence $PAGE->context, have not already been set up properly, set them up now. 3236 $PAGE->set_course($PAGE->course); 3237 } 3238 // Do not update access time for webservice or ajax requests. 3239 if (!WS_SERVER && !AJAX_SCRIPT) { 3240 user_accesstime_log(SITEID); 3241 } 3242 return; 3243 3244 } else { 3245 // Course login always required. 3246 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); 3247 } 3248 } 3249 3250 /** 3251 * Validates a user key, checking if the key exists, is not expired and the remote ip is correct. 3252 * 3253 * @param string $keyvalue the key value 3254 * @param string $script unique script identifier 3255 * @param int $instance instance id 3256 * @return stdClass the key entry in the user_private_key table 3257 * @since Moodle 3.2 3258 * @throws moodle_exception 3259 */ 3260 function validate_user_key($keyvalue, $script, $instance) { 3261 global $DB; 3262 3263 if (!$key = $DB->get_record('user_private_key', array('script' => $script, 'value' => $keyvalue, 'instance' => $instance))) { 3264 throw new \moodle_exception('invalidkey'); 3265 } 3266 3267 if (!empty($key->validuntil) and $key->validuntil < time()) { 3268 throw new \moodle_exception('expiredkey'); 3269 } 3270 3271 if ($key->iprestriction) { 3272 $remoteaddr = getremoteaddr(null); 3273 if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) { 3274 throw new \moodle_exception('ipmismatch'); 3275 } 3276 } 3277 return $key; 3278 } 3279 3280 /** 3281 * Require key login. Function terminates with error if key not found or incorrect. 3282 * 3283 * @uses NO_MOODLE_COOKIES 3284 * @uses PARAM_ALPHANUM 3285 * @param string $script unique script identifier 3286 * @param int $instance optional instance id 3287 * @param string $keyvalue The key. If not supplied, this will be fetched from the current session. 3288 * @return int Instance ID 3289 */ 3290 function require_user_key_login($script, $instance = null, $keyvalue = null) { 3291 global $DB; 3292 3293 if (!NO_MOODLE_COOKIES) { 3294 throw new \moodle_exception('sessioncookiesdisable'); 3295 } 3296 3297 // Extra safety. 3298 \core\session\manager::write_close(); 3299 3300 if (null === $keyvalue) { 3301 $keyvalue = required_param('key', PARAM_ALPHANUM); 3302 } 3303 3304 $key = validate_user_key($keyvalue, $script, $instance); 3305 3306 if (!$user = $DB->get_record('user', array('id' => $key->userid))) { 3307 throw new \moodle_exception('invaliduserid'); 3308 } 3309 3310 core_user::require_active_user($user, true, true); 3311 3312 // Emulate normal session. 3313 enrol_check_plugins($user, false); 3314 \core\session\manager::set_user($user); 3315 3316 // Note we are not using normal login. 3317 if (!defined('USER_KEY_LOGIN')) { 3318 define('USER_KEY_LOGIN', true); 3319 } 3320 3321 // Return instance id - it might be empty. 3322 return $key->instance; 3323 } 3324 3325 /** 3326 * Creates a new private user access key. 3327 * 3328 * @param string $script unique target identifier 3329 * @param int $userid 3330 * @param int $instance optional instance id 3331 * @param string $iprestriction optional ip restricted access 3332 * @param int $validuntil key valid only until given data 3333 * @return string access key value 3334 */ 3335 function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) { 3336 global $DB; 3337 3338 $key = new stdClass(); 3339 $key->script = $script; 3340 $key->userid = $userid; 3341 $key->instance = $instance; 3342 $key->iprestriction = $iprestriction; 3343 $key->validuntil = $validuntil; 3344 $key->timecreated = time(); 3345 3346 // Something long and unique. 3347 $key->value = md5($userid.'_'.time().random_string(40)); 3348 while ($DB->record_exists('user_private_key', array('value' => $key->value))) { 3349 // Must be unique. 3350 $key->value = md5($userid.'_'.time().random_string(40)); 3351 } 3352 $DB->insert_record('user_private_key', $key); 3353 return $key->value; 3354 } 3355 3356 /** 3357 * Delete the user's new private user access keys for a particular script. 3358 * 3359 * @param string $script unique target identifier 3360 * @param int $userid 3361 * @return void 3362 */ 3363 function delete_user_key($script, $userid) { 3364 global $DB; 3365 $DB->delete_records('user_private_key', array('script' => $script, 'userid' => $userid)); 3366 } 3367 3368 /** 3369 * Gets a private user access key (and creates one if one doesn't exist). 3370 * 3371 * @param string $script unique target identifier 3372 * @param int $userid 3373 * @param int $instance optional instance id 3374 * @param string $iprestriction optional ip restricted access 3375 * @param int $validuntil key valid only until given date 3376 * @return string access key value 3377 */ 3378 function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) { 3379 global $DB; 3380 3381 if ($key = $DB->get_record('user_private_key', array('script' => $script, 'userid' => $userid, 3382 'instance' => $instance, 'iprestriction' => $iprestriction, 3383 'validuntil' => $validuntil))) { 3384 return $key->value; 3385 } else { 3386 return create_user_key($script, $userid, $instance, $iprestriction, $validuntil); 3387 } 3388 } 3389 3390 3391 /** 3392 * Modify the user table by setting the currently logged in user's last login to now. 3393 * 3394 * @return bool Always returns true 3395 */ 3396 function update_user_login_times() { 3397 global $USER, $DB, $SESSION; 3398 3399 if (isguestuser()) { 3400 // Do not update guest access times/ips for performance. 3401 return true; 3402 } 3403 3404 if (defined('USER_KEY_LOGIN') && USER_KEY_LOGIN === true) { 3405 // Do not update user login time when using user key login. 3406 return true; 3407 } 3408 3409 $now = time(); 3410 3411 $user = new stdClass(); 3412 $user->id = $USER->id; 3413 3414 // Make sure all users that logged in have some firstaccess. 3415 if ($USER->firstaccess == 0) { 3416 $USER->firstaccess = $user->firstaccess = $now; 3417 } 3418 3419 // Store the previous current as lastlogin. 3420 $USER->lastlogin = $user->lastlogin = $USER->currentlogin; 3421 3422 $USER->currentlogin = $user->currentlogin = $now; 3423 3424 // Function user_accesstime_log() may not update immediately, better do it here. 3425 $USER->lastaccess = $user->lastaccess = $now; 3426 $SESSION->userpreviousip = $USER->lastip; 3427 $USER->lastip = $user->lastip = getremoteaddr(); 3428 3429 // Note: do not call user_update_user() here because this is part of the login process, 3430 // the login event means that these fields were updated. 3431 $DB->update_record('user', $user); 3432 return true; 3433 } 3434 3435 /** 3436 * Determines if a user has completed setting up their account. 3437 * 3438 * The lax mode (with $strict = false) has been introduced for special cases 3439 * only where we want to skip certain checks intentionally. This is valid in 3440 * certain mnet or ajax scenarios when the user cannot / should not be 3441 * redirected to edit their profile. In most cases, you should perform the 3442 * strict check. 3443 * 3444 * @param stdClass $user A {@link $USER} object to test for the existence of a valid name and email 3445 * @param bool $strict Be more strict and assert id and custom profile fields set, too 3446 * @return bool 3447 */ 3448 function user_not_fully_set_up($user, $strict = true) { 3449 global $CFG, $SESSION, $USER; 3450 require_once($CFG->dirroot.'/user/profile/lib.php'); 3451 3452 // If the user is setup then store this in the session to avoid re-checking. 3453 // Some edge cases are when the users email starts to bounce or the 3454 // configuration for custom fields has changed while they are logged in so 3455 // we re-check this fully every hour for the rare cases it has changed. 3456 if (isset($USER->id) && isset($user->id) && $USER->id === $user->id && 3457 isset($SESSION->fullysetupstrict) && (time() - $SESSION->fullysetupstrict) < HOURSECS) { 3458 return false; 3459 } 3460 3461 if (isguestuser($user)) { 3462 return false; 3463 } 3464 3465 if (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user)) { 3466 return true; 3467 } 3468 3469 if ($strict) { 3470 if (empty($user->id)) { 3471 // Strict mode can be used with existing accounts only. 3472 return true; 3473 } 3474 if (!profile_has_required_custom_fields_set($user->id)) { 3475 return true; 3476 } 3477 if (isset($USER->id) && isset($user->id) && $USER->id === $user->id) { 3478 $SESSION->fullysetupstrict = time(); 3479 } 3480 } 3481 3482 return false; 3483 } 3484 3485 /** 3486 * Check whether the user has exceeded the bounce threshold 3487 * 3488 * @param stdClass $user A {@link $USER} object 3489 * @return bool true => User has exceeded bounce threshold 3490 */ 3491 function over_bounce_threshold($user) { 3492 global $CFG, $DB; 3493 3494 if (empty($CFG->handlebounces)) { 3495 return false; 3496 } 3497 3498 if (empty($user->id)) { 3499 // No real (DB) user, nothing to do here. 3500 return false; 3501 } 3502 3503 // Set sensible defaults. 3504 if (empty($CFG->minbounces)) { 3505 $CFG->minbounces = 10; 3506 } 3507 if (empty($CFG->bounceratio)) { 3508 $CFG->bounceratio = .20; 3509 } 3510 $bouncecount = 0; 3511 $sendcount = 0; 3512 if ($bounce = $DB->get_record('user_preferences', array ('userid' => $user->id, 'name' => 'email_bounce_count'))) { 3513 $bouncecount = $bounce->value; 3514 } 3515 if ($send = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) { 3516 $sendcount = $send->value; 3517 } 3518 return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio); 3519 } 3520 3521 /** 3522 * Used to increment or reset email sent count 3523 * 3524 * @param stdClass $user object containing an id 3525 * @param bool $reset will reset the count to 0 3526 * @return void 3527 */ 3528 function set_send_count($user, $reset=false) { 3529 global $DB; 3530 3531 if (empty($user->id)) { 3532 // No real (DB) user, nothing to do here. 3533 return; 3534 } 3535 3536 if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) { 3537 $pref->value = (!empty($reset)) ? 0 : $pref->value+1; 3538 $DB->update_record('user_preferences', $pref); 3539 } else if (!empty($reset)) { 3540 // If it's not there and we're resetting, don't bother. Make a new one. 3541 $pref = new stdClass(); 3542 $pref->name = 'email_send_count'; 3543 $pref->value = 1; 3544 $pref->userid = $user->id; 3545 $DB->insert_record('user_preferences', $pref, false); 3546 } 3547 } 3548 3549 /** 3550 * Increment or reset user's email bounce count 3551 * 3552 * @param stdClass $user object containing an id 3553 * @param bool $reset will reset the count to 0 3554 */ 3555 function set_bounce_count($user, $reset=false) { 3556 global $DB; 3557 3558 if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_bounce_count'))) { 3559 $pref->value = (!empty($reset)) ? 0 : $pref->value+1; 3560 $DB->update_record('user_preferences', $pref); 3561 } else if (!empty($reset)) { 3562 // If it's not there and we're resetting, don't bother. Make a new one. 3563 $pref = new stdClass(); 3564 $pref->name = 'email_bounce_count'; 3565 $pref->value = 1; 3566 $pref->userid = $user->id; 3567 $DB->insert_record('user_preferences', $pref, false); 3568 } 3569 } 3570 3571 /** 3572 * Determines if the logged in user is currently moving an activity 3573 * 3574 * @param int $courseid The id of the course being tested 3575 * @return bool 3576 */ 3577 function ismoving($courseid) { 3578 global $USER; 3579 3580 if (!empty($USER->activitycopy)) { 3581 return ($USER->activitycopycourse == $courseid); 3582 } 3583 return false; 3584 } 3585 3586 /** 3587 * Returns a persons full name 3588 * 3589 * Given an object containing all of the users name values, this function returns a string with the full name of the person. 3590 * The result may depend on system settings or language. 'override' will force the alternativefullnameformat to be used. In 3591 * English, fullname as well as alternativefullnameformat is set to 'firstname lastname' by default. But you could have 3592 * fullname set to 'firstname lastname' and alternativefullnameformat set to 'firstname middlename alternatename lastname'. 3593 * 3594 * @param stdClass $user A {@link $USER} object to get full name of. 3595 * @param bool $override If true then the alternativefullnameformat format rather than fullnamedisplay format will be used. 3596 * @return string 3597 */ 3598 function fullname($user, $override=false) { 3599 global $CFG, $SESSION; 3600 3601 if (!isset($user->firstname) and !isset($user->lastname)) { 3602 return ''; 3603 } 3604 3605 // Get all of the name fields. 3606 $allnames = \core_user\fields::get_name_fields(); 3607 if ($CFG->debugdeveloper) { 3608 foreach ($allnames as $allname) { 3609 if (!property_exists($user, $allname)) { 3610 // If all the user name fields are not set in the user object, then notify the programmer that it needs to be fixed. 3611 debugging('You need to update your sql to include additional name fields in the user object.', DEBUG_DEVELOPER); 3612 // Message has been sent, no point in sending the message multiple times. 3613 break; 3614 } 3615 } 3616 } 3617 3618 if (!$override) { 3619 if (!empty($CFG->forcefirstname)) { 3620 $user->firstname = $CFG->forcefirstname; 3621 } 3622 if (!empty($CFG->forcelastname)) { 3623 $user->lastname = $CFG->forcelastname; 3624 } 3625 } 3626 3627 if (!empty($SESSION->fullnamedisplay)) { 3628 $CFG->fullnamedisplay = $SESSION->fullnamedisplay; 3629 } 3630 3631 $template = null; 3632 // If the fullnamedisplay setting is available, set the template to that. 3633 if (isset($CFG->fullnamedisplay)) { 3634 $template = $CFG->fullnamedisplay; 3635 } 3636 // If the template is empty, or set to language, return the language string. 3637 if ((empty($template) || $template == 'language') && !$override) { 3638 return get_string('fullnamedisplay', null, $user); 3639 } 3640 3641 // Check to see if we are displaying according to the alternative full name format. 3642 if ($override) { 3643 if (empty($CFG->alternativefullnameformat) || $CFG->alternativefullnameformat == 'language') { 3644 // Default to show just the user names according to the fullnamedisplay string. 3645 return get_string('fullnamedisplay', null, $user); 3646 } else { 3647 // If the override is true, then change the template to use the complete name. 3648 $template = $CFG->alternativefullnameformat; 3649 } 3650 } 3651 3652 $requirednames = array(); 3653 // With each name, see if it is in the display name template, and add it to the required names array if it is. 3654 foreach ($allnames as $allname) { 3655 if (strpos($template, $allname) !== false) { 3656 $requirednames[] = $allname; 3657 } 3658 } 3659 3660 $displayname = $template; 3661 // Switch in the actual data into the template. 3662 foreach ($requirednames as $altname) { 3663 if (isset($user->$altname)) { 3664 // Using empty() on the below if statement causes breakages. 3665 if ((string)$user->$altname == '') { 3666 $displayname = str_replace($altname, 'EMPTY', $displayname); 3667 } else { 3668 $displayname = str_replace($altname, $user->$altname, $displayname); 3669 } 3670 } else { 3671 $displayname = str_replace($altname, 'EMPTY', $displayname); 3672 } 3673 } 3674 // Tidy up any misc. characters (Not perfect, but gets most characters). 3675 // Don't remove the "u" at the end of the first expression unless you want garbled characters when combining hiragana or 3676 // katakana and parenthesis. 3677 $patterns = array(); 3678 // This regular expression replacement is to fix problems such as 'James () Kirk' Where 'Tiberius' (middlename) has not been 3679 // filled in by a user. 3680 // The special characters are Japanese brackets that are common enough to make allowances for them (not covered by :punct:). 3681 $patterns[] = '/[[:punct:]「」]*EMPTY[[:punct:]「」]*/u'; 3682 // This regular expression is to remove any double spaces in the display name. 3683 $patterns[] = '/\s{2,}/u'; 3684 foreach ($patterns as $pattern) { 3685 $displayname = preg_replace($pattern, ' ', $displayname); 3686 } 3687 3688 // Trimming $displayname will help the next check to ensure that we don't have a display name with spaces. 3689 $displayname = trim($displayname); 3690 if (empty($displayname)) { 3691 // Going with just the first name if no alternate fields are filled out. May be changed later depending on what 3692 // people in general feel is a good setting to fall back on. 3693 $displayname = $user->firstname; 3694 } 3695 return $displayname; 3696 } 3697 3698 /** 3699 * Reduces lines of duplicated code for getting user name fields. 3700 * 3701 * See also {@link user_picture::unalias()} 3702 * 3703 * @param object $addtoobject Object to add user name fields to. 3704 * @param object $secondobject Object that contains user name field information. 3705 * @param string $prefix prefix to be added to all fields (including $additionalfields) e.g. authorfirstname. 3706 * @param array $additionalfields Additional fields to be matched with data in the second object. 3707 * The key can be set to the user table field name. 3708 * @return object User name fields. 3709 */ 3710 function username_load_fields_from_object($addtoobject, $secondobject, $prefix = null, $additionalfields = null) { 3711 $fields = []; 3712 foreach (\core_user\fields::get_name_fields() as $field) { 3713 $fields[$field] = $prefix . $field; 3714 } 3715 if ($additionalfields) { 3716 // Additional fields can specify their own 'alias' such as 'id' => 'userid'. This checks to see if 3717 // the key is a number and then sets the key to the array value. 3718 foreach ($additionalfields as $key => $value) { 3719 if (is_numeric($key)) { 3720 $additionalfields[$value] = $prefix . $value; 3721 unset($additionalfields[$key]); 3722 } else { 3723 $additionalfields[$key] = $prefix . $value; 3724 } 3725 } 3726 $fields = array_merge($fields, $additionalfields); 3727 } 3728 foreach ($fields as $key => $field) { 3729 // Important that we have all of the user name fields present in the object that we are sending back. 3730 $addtoobject->$key = ''; 3731 if (isset($secondobject->$field)) { 3732 $addtoobject->$key = $secondobject->$field; 3733 } 3734 } 3735 return $addtoobject; 3736 } 3737 3738 /** 3739 * Returns an array of values in order of occurance in a provided string. 3740 * The key in the result is the character postion in the string. 3741 * 3742 * @param array $values Values to be found in the string format 3743 * @param string $stringformat The string which may contain values being searched for. 3744 * @return array An array of values in order according to placement in the string format. 3745 */ 3746 function order_in_string($values, $stringformat) { 3747 $valuearray = array(); 3748 foreach ($values as $value) { 3749 $pattern = "/$value\b/"; 3750 // Using preg_match as strpos() may match values that are similar e.g. firstname and firstnamephonetic. 3751 if (preg_match($pattern, $stringformat)) { 3752 $replacement = "thing"; 3753 // Replace the value with something more unique to ensure we get the right position when using strpos(). 3754 $newformat = preg_replace($pattern, $replacement, $stringformat); 3755 $position = strpos($newformat, $replacement); 3756 $valuearray[$position] = $value; 3757 } 3758 } 3759 ksort($valuearray); 3760 return $valuearray; 3761 } 3762 3763 /** 3764 * Returns whether a given authentication plugin exists. 3765 * 3766 * @param string $auth Form of authentication to check for. Defaults to the global setting in {@link $CFG}. 3767 * @return boolean Whether the plugin is available. 3768 */ 3769 function exists_auth_plugin($auth) { 3770 global $CFG; 3771 3772 if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) { 3773 return is_readable("{$CFG->dirroot}/auth/$auth/auth.php"); 3774 } 3775 return false; 3776 } 3777 3778 /** 3779 * Checks if a given plugin is in the list of enabled authentication plugins. 3780 * 3781 * @param string $auth Authentication plugin. 3782 * @return boolean Whether the plugin is enabled. 3783 */ 3784 function is_enabled_auth($auth) { 3785 if (empty($auth)) { 3786 return false; 3787 } 3788 3789 $enabled = get_enabled_auth_plugins(); 3790 3791 return in_array($auth, $enabled); 3792 } 3793 3794 /** 3795 * Returns an authentication plugin instance. 3796 * 3797 * @param string $auth name of authentication plugin 3798 * @return auth_plugin_base An instance of the required authentication plugin. 3799 */ 3800 function get_auth_plugin($auth) { 3801 global $CFG; 3802 3803 // Check the plugin exists first. 3804 if (! exists_auth_plugin($auth)) { 3805 throw new \moodle_exception('authpluginnotfound', 'debug', '', $auth); 3806 } 3807 3808 // Return auth plugin instance. 3809 require_once("{$CFG->dirroot}/auth/$auth/auth.php"); 3810 $class = "auth_plugin_$auth"; 3811 return new $class; 3812 } 3813 3814 /** 3815 * Returns array of active auth plugins. 3816 * 3817 * @param bool $fix fix $CFG->auth if needed. Only set if logged in as admin. 3818 * @return array 3819 */ 3820 function get_enabled_auth_plugins($fix=false) { 3821 global $CFG; 3822 3823 $default = array('manual', 'nologin'); 3824 3825 if (empty($CFG->auth)) { 3826 $auths = array(); 3827 } else { 3828 $auths = explode(',', $CFG->auth); 3829 } 3830 3831 $auths = array_unique($auths); 3832 $oldauthconfig = implode(',', $auths); 3833 foreach ($auths as $k => $authname) { 3834 if (in_array($authname, $default)) { 3835 // The manual and nologin plugin never need to be stored. 3836 unset($auths[$k]); 3837 } else if (!exists_auth_plugin($authname)) { 3838 debugging(get_string('authpluginnotfound', 'debug', $authname)); 3839 unset($auths[$k]); 3840 } 3841 } 3842 3843 // Ideally only explicit interaction from a human admin should trigger a 3844 // change in auth config, see MDL-70424 for details. 3845 if ($fix) { 3846 $newconfig = implode(',', $auths); 3847 if (!isset($CFG->auth) or $newconfig != $CFG->auth) { 3848 add_to_config_log('auth', $oldauthconfig, $newconfig, 'core'); 3849 set_config('auth', $newconfig); 3850 } 3851 } 3852 3853 return (array_merge($default, $auths)); 3854 } 3855 3856 /** 3857 * Returns true if an internal authentication method is being used. 3858 * if method not specified then, global default is assumed 3859 * 3860 * @param string $auth Form of authentication required 3861 * @return bool 3862 */ 3863 function is_internal_auth($auth) { 3864 // Throws error if bad $auth. 3865 $authplugin = get_auth_plugin($auth); 3866 return $authplugin->is_internal(); 3867 } 3868 3869 /** 3870 * Returns true if the user is a 'restored' one. 3871 * 3872 * Used in the login process to inform the user and allow him/her to reset the password 3873 * 3874 * @param string $username username to be checked 3875 * @return bool 3876 */ 3877 function is_restored_user($username) { 3878 global $CFG, $DB; 3879 3880 return $DB->record_exists('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'password' => 'restored')); 3881 } 3882 3883 /** 3884 * Returns an array of user fields 3885 * 3886 * @return array User field/column names 3887 */ 3888 function get_user_fieldnames() { 3889 global $DB; 3890 3891 $fieldarray = $DB->get_columns('user'); 3892 unset($fieldarray['id']); 3893 $fieldarray = array_keys($fieldarray); 3894 3895 return $fieldarray; 3896 } 3897 3898 /** 3899 * Returns the string of the language for the new user. 3900 * 3901 * @return string language for the new user 3902 */ 3903 function get_newuser_language() { 3904 global $CFG, $SESSION; 3905 return (!empty($CFG->autolangusercreation) && !empty($SESSION->lang)) ? $SESSION->lang : $CFG->lang; 3906 } 3907 3908 /** 3909 * Creates a bare-bones user record 3910 * 3911 * @todo Outline auth types and provide code example 3912 * 3913 * @param string $username New user's username to add to record 3914 * @param string $password New user's password to add to record 3915 * @param string $auth Form of authentication required 3916 * @return stdClass A complete user object 3917 */ 3918 function create_user_record($username, $password, $auth = 'manual') { 3919 global $CFG, $DB, $SESSION; 3920 require_once($CFG->dirroot.'/user/profile/lib.php'); 3921 require_once($CFG->dirroot.'/user/lib.php'); 3922 3923 // Just in case check text case. 3924 $username = trim(core_text::strtolower($username)); 3925 3926 $authplugin = get_auth_plugin($auth); 3927 $customfields = $authplugin->get_custom_user_profile_fields(); 3928 $newuser = new stdClass(); 3929 if ($newinfo = $authplugin->get_userinfo($username)) { 3930 $newinfo = truncate_userinfo($newinfo); 3931 foreach ($newinfo as $key => $value) { 3932 if (in_array($key, $authplugin->userfields) || (in_array($key, $customfields))) { 3933 $newuser->$key = $value; 3934 } 3935 } 3936 } 3937 3938 if (!empty($newuser->email)) { 3939 if (email_is_not_allowed($newuser->email)) { 3940 unset($newuser->email); 3941 } 3942 } 3943 3944 $newuser->auth = $auth; 3945 $newuser->username = $username; 3946 3947 // Fix for MDL-8480 3948 // user CFG lang for user if $newuser->lang is empty 3949 // or $user->lang is not an installed language. 3950 if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) { 3951 $newuser->lang = get_newuser_language(); 3952 } 3953 $newuser->confirmed = 1; 3954 $newuser->lastip = getremoteaddr(); 3955 $newuser->timecreated = time(); 3956 $newuser->timemodified = $newuser->timecreated; 3957 $newuser->mnethostid = $CFG->mnet_localhost_id; 3958 3959 $newuser->id = user_create_user($newuser, false, false); 3960 3961 // Save user profile data. 3962 profile_save_data($newuser); 3963 3964 $user = get_complete_user_data('id', $newuser->id); 3965 if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})) { 3966 set_user_preference('auth_forcepasswordchange', 1, $user); 3967 } 3968 // Set the password. 3969 update_internal_user_password($user, $password); 3970 3971 // Trigger event. 3972 \core\event\user_created::create_from_userid($newuser->id)->trigger(); 3973 3974 return $user; 3975 } 3976 3977 /** 3978 * Will update a local user record from an external source (MNET users can not be updated using this method!). 3979 * 3980 * @param string $username user's username to update the record 3981 * @return stdClass A complete user object 3982 */ 3983 function update_user_record($username) { 3984 global $DB, $CFG; 3985 // Just in case check text case. 3986 $username = trim(core_text::strtolower($username)); 3987 3988 $oldinfo = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id), '*', MUST_EXIST); 3989 return update_user_record_by_id($oldinfo->id); 3990 } 3991 3992 /** 3993 * Will update a local user record from an external source (MNET users can not be updated using this method!). 3994 * 3995 * @param int $id user id 3996 * @return stdClass A complete user object 3997 */ 3998 function update_user_record_by_id($id) { 3999 global $DB, $CFG; 4000 require_once($CFG->dirroot."/user/profile/lib.php"); 4001 require_once($CFG->dirroot.'/user/lib.php'); 4002 4003 $params = array('mnethostid' => $CFG->mnet_localhost_id, 'id' => $id, 'deleted' => 0); 4004 $oldinfo = $DB->get_record('user', $params, '*', MUST_EXIST); 4005 4006 $newuser = array(); 4007 $userauth = get_auth_plugin($oldinfo->auth); 4008 4009 if ($newinfo = $userauth->get_userinfo($oldinfo->username)) { 4010 $newinfo = truncate_userinfo($newinfo); 4011 $customfields = $userauth->get_custom_user_profile_fields(); 4012 4013 foreach ($newinfo as $key => $value) { 4014 $iscustom = in_array($key, $customfields); 4015 if (!$iscustom) { 4016 $key = strtolower($key); 4017 } 4018 if ((!property_exists($oldinfo, $key) && !$iscustom) or $key === 'username' or $key === 'id' 4019 or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') { 4020 // Unknown or must not be changed. 4021 continue; 4022 } 4023 if (empty($userauth->config->{'field_updatelocal_' . $key}) || empty($userauth->config->{'field_lock_' . $key})) { 4024 continue; 4025 } 4026 $confval = $userauth->config->{'field_updatelocal_' . $key}; 4027 $lockval = $userauth->config->{'field_lock_' . $key}; 4028 if ($confval === 'onlogin') { 4029 // MDL-4207 Don't overwrite modified user profile values with 4030 // empty LDAP values when 'unlocked if empty' is set. The purpose 4031 // of the setting 'unlocked if empty' is to allow the user to fill 4032 // in a value for the selected field _if LDAP is giving 4033 // nothing_ for this field. Thus it makes sense to let this value 4034 // stand in until LDAP is giving a value for this field. 4035 if (!(empty($value) && $lockval === 'unlockedifempty')) { 4036 if ($iscustom || (in_array($key, $userauth->userfields) && 4037 ((string)$oldinfo->$key !== (string)$value))) { 4038 $newuser[$key] = (string)$value; 4039 } 4040 } 4041 } 4042 } 4043 if ($newuser) { 4044 $newuser['id'] = $oldinfo->id; 4045 $newuser['timemodified'] = time(); 4046 user_update_user((object) $newuser, false, false); 4047 4048 // Save user profile data. 4049 profile_save_data((object) $newuser); 4050 4051 // Trigger event. 4052 \core\event\user_updated::create_from_userid($newuser['id'])->trigger(); 4053 } 4054 } 4055 4056 return get_complete_user_data('id', $oldinfo->id); 4057 } 4058 4059 /** 4060 * Will truncate userinfo as it comes from auth_get_userinfo (from external auth) which may have large fields. 4061 * 4062 * @param array $info Array of user properties to truncate if needed 4063 * @return array The now truncated information that was passed in 4064 */ 4065 function truncate_userinfo(array $info) { 4066 // Define the limits. 4067 $limit = array( 4068 'username' => 100, 4069 'idnumber' => 255, 4070 'firstname' => 100, 4071 'lastname' => 100, 4072 'email' => 100, 4073 'phone1' => 20, 4074 'phone2' => 20, 4075 'institution' => 255, 4076 'department' => 255, 4077 'address' => 255, 4078 'city' => 120, 4079 'country' => 2, 4080 ); 4081 4082 // Apply where needed. 4083 foreach (array_keys($info) as $key) { 4084 if (!empty($limit[$key])) { 4085 $info[$key] = trim(core_text::substr($info[$key], 0, $limit[$key])); 4086 } 4087 } 4088 4089 return $info; 4090 } 4091 4092 /** 4093 * Marks user deleted in internal user database and notifies the auth plugin. 4094 * Also unenrols user from all roles and does other cleanup. 4095 * 4096 * Any plugin that needs to purge user data should register the 'user_deleted' event. 4097 * 4098 * @param stdClass $user full user object before delete 4099 * @return boolean success 4100 * @throws coding_exception if invalid $user parameter detected 4101 */ 4102 function delete_user(stdClass $user) { 4103 global $CFG, $DB, $SESSION; 4104 require_once($CFG->libdir.'/grouplib.php'); 4105 require_once($CFG->libdir.'/gradelib.php'); 4106 require_once($CFG->dirroot.'/message/lib.php'); 4107 require_once($CFG->dirroot.'/user/lib.php'); 4108 4109 // Make sure nobody sends bogus record type as parameter. 4110 if (!property_exists($user, 'id') or !property_exists($user, 'username')) { 4111 throw new coding_exception('Invalid $user parameter in delete_user() detected'); 4112 } 4113 4114 // Better not trust the parameter and fetch the latest info this will be very expensive anyway. 4115 if (!$user = $DB->get_record('user', array('id' => $user->id))) { 4116 debugging('Attempt to delete unknown user account.'); 4117 return false; 4118 } 4119 4120 // There must be always exactly one guest record, originally the guest account was identified by username only, 4121 // now we use $CFG->siteguest for performance reasons. 4122 if ($user->username === 'guest' or isguestuser($user)) { 4123 debugging('Guest user account can not be deleted.'); 4124 return false; 4125 } 4126 4127 // Admin can be theoretically from different auth plugin, but we want to prevent deletion of internal accoutns only, 4128 // if anything goes wrong ppl may force somebody to be admin via config.php setting $CFG->siteadmins. 4129 if ($user->auth === 'manual' and is_siteadmin($user)) { 4130 debugging('Local administrator accounts can not be deleted.'); 4131 return false; 4132 } 4133 4134 // Allow plugins to use this user object before we completely delete it. 4135 if ($pluginsfunction = get_plugins_with_function('pre_user_delete')) { 4136 foreach ($pluginsfunction as $plugintype => $plugins) { 4137 foreach ($plugins as $pluginfunction) { 4138 $pluginfunction($user); 4139 } 4140 } 4141 } 4142 4143 // Keep user record before updating it, as we have to pass this to user_deleted event. 4144 $olduser = clone $user; 4145 4146 // Keep a copy of user context, we need it for event. 4147 $usercontext = context_user::instance($user->id); 4148 4149 // Delete all grades - backup is kept in grade_grades_history table. 4150 grade_user_delete($user->id); 4151 4152 // TODO: remove from cohorts using standard API here. 4153 4154 // Remove user tags. 4155 core_tag_tag::remove_all_item_tags('core', 'user', $user->id); 4156 4157 // Unconditionally unenrol from all courses. 4158 enrol_user_delete($user); 4159 4160 // Unenrol from all roles in all contexts. 4161 // This might be slow but it is really needed - modules might do some extra cleanup! 4162 role_unassign_all(array('userid' => $user->id)); 4163 4164 // Notify the competency subsystem. 4165 \core_competency\api::hook_user_deleted($user->id); 4166 4167 // Now do a brute force cleanup. 4168 4169 // Delete all user events and subscription events. 4170 $DB->delete_records_select('event', 'userid = :userid AND subscriptionid IS NOT NULL', ['userid' => $user->id]); 4171 4172 // Now, delete all calendar subscription from the user. 4173 $DB->delete_records('event_subscriptions', ['userid' => $user->id]); 4174 4175 // Remove from all cohorts. 4176 $DB->delete_records('cohort_members', array('userid' => $user->id)); 4177 4178 // Remove from all groups. 4179 $DB->delete_records('groups_members', array('userid' => $user->id)); 4180 4181 // Brute force unenrol from all courses. 4182 $DB->delete_records('user_enrolments', array('userid' => $user->id)); 4183 4184 // Purge user preferences. 4185 $DB->delete_records('user_preferences', array('userid' => $user->id)); 4186 4187 // Purge user extra profile info. 4188 $DB->delete_records('user_info_data', array('userid' => $user->id)); 4189 4190 // Purge log of previous password hashes. 4191 $DB->delete_records('user_password_history', array('userid' => $user->id)); 4192 4193 // Last course access not necessary either. 4194 $DB->delete_records('user_lastaccess', array('userid' => $user->id)); 4195 // Remove all user tokens. 4196 $DB->delete_records('external_tokens', array('userid' => $user->id)); 4197 4198 // Unauthorise the user for all services. 4199 $DB->delete_records('external_services_users', array('userid' => $user->id)); 4200 4201 // Remove users private keys. 4202 $DB->delete_records('user_private_key', array('userid' => $user->id)); 4203 4204 // Remove users customised pages. 4205 $DB->delete_records('my_pages', array('userid' => $user->id, 'private' => 1)); 4206 4207 // Remove user's oauth2 refresh tokens, if present. 4208 $DB->delete_records('oauth2_refresh_token', array('userid' => $user->id)); 4209 4210 // Delete user from $SESSION->bulk_users. 4211 if (isset($SESSION->bulk_users[$user->id])) { 4212 unset($SESSION->bulk_users[$user->id]); 4213 } 4214 4215 // Force logout - may fail if file based sessions used, sorry. 4216 \core\session\manager::kill_user_sessions($user->id); 4217 4218 // Generate username from email address, or a fake email. 4219 $delemail = !empty($user->email) ? $user->email : $user->username . '.' . $user->id . '@unknownemail.invalid'; 4220 4221 $deltime = time(); 4222 $deltimelength = core_text::strlen((string) $deltime); 4223 4224 // Max username length is 100 chars. Select up to limit - (length of current time + 1 [period character]) from users email. 4225 $delname = clean_param($delemail, PARAM_USERNAME); 4226 $delname = core_text::substr($delname, 0, 100 - ($deltimelength + 1)) . ".{$deltime}"; 4227 4228 // Workaround for bulk deletes of users with the same email address. 4229 while ($DB->record_exists('user', array('username' => $delname))) { // No need to use mnethostid here. 4230 $delname++; 4231 } 4232 4233 // Mark internal user record as "deleted". 4234 $updateuser = new stdClass(); 4235 $updateuser->id = $user->id; 4236 $updateuser->deleted = 1; 4237 $updateuser->username = $delname; // Remember it just in case. 4238 $updateuser->email = md5($user->username);// Store hash of username, useful importing/restoring users. 4239 $updateuser->idnumber = ''; // Clear this field to free it up. 4240 $updateuser->picture = 0; 4241 $updateuser->timemodified = $deltime; 4242 4243 // Don't trigger update event, as user is being deleted. 4244 user_update_user($updateuser, false, false); 4245 4246 // Delete all content associated with the user context, but not the context itself. 4247 $usercontext->delete_content(); 4248 4249 // Delete any search data. 4250 \core_search\manager::context_deleted($usercontext); 4251 4252 // Any plugin that needs to cleanup should register this event. 4253 // Trigger event. 4254 $event = \core\event\user_deleted::create( 4255 array( 4256 'objectid' => $user->id, 4257 'relateduserid' => $user->id, 4258 'context' => $usercontext, 4259 'other' => array( 4260 'username' => $user->username, 4261 'email' => $user->email, 4262 'idnumber' => $user->idnumber, 4263 'picture' => $user->picture, 4264 'mnethostid' => $user->mnethostid 4265 ) 4266 ) 4267 ); 4268 $event->add_record_snapshot('user', $olduser); 4269 $event->trigger(); 4270 4271 // We will update the user's timemodified, as it will be passed to the user_deleted event, which 4272 // should know about this updated property persisted to the user's table. 4273 $user->timemodified = $updateuser->timemodified; 4274 4275 // Notify auth plugin - do not block the delete even when plugin fails. 4276 $authplugin = get_auth_plugin($user->auth); 4277 $authplugin->user_delete($user); 4278 4279 return true; 4280 } 4281 4282 /** 4283 * Retrieve the guest user object. 4284 * 4285 * @return stdClass A {@link $USER} object 4286 */ 4287 function guest_user() { 4288 global $CFG, $DB; 4289 4290 if ($newuser = $DB->get_record('user', array('id' => $CFG->siteguest))) { 4291 $newuser->confirmed = 1; 4292 $newuser->lang = get_newuser_language(); 4293 $newuser->lastip = getremoteaddr(); 4294 } 4295 4296 return $newuser; 4297 } 4298 4299 /** 4300 * Authenticates a user against the chosen authentication mechanism 4301 * 4302 * Given a username and password, this function looks them 4303 * up using the currently selected authentication mechanism, 4304 * and if the authentication is successful, it returns a 4305 * valid $user object from the 'user' table. 4306 * 4307 * Uses auth_ functions from the currently active auth module 4308 * 4309 * After authenticate_user_login() returns success, you will need to 4310 * log that the user has logged in, and call complete_user_login() to set 4311 * the session up. 4312 * 4313 * Note: this function works only with non-mnet accounts! 4314 * 4315 * @param string $username User's username (or also email if $CFG->authloginviaemail enabled) 4316 * @param string $password User's password 4317 * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO 4318 * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists) 4319 * @param mixed logintoken If this is set to a string it is validated against the login token for the session. 4320 * @return stdClass|false A {@link $USER} object or false if error 4321 */ 4322 function authenticate_user_login($username, $password, $ignorelockout=false, &$failurereason=null, $logintoken=false) { 4323 global $CFG, $DB, $PAGE; 4324 require_once("$CFG->libdir/authlib.php"); 4325 4326 if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) { 4327 // we have found the user 4328 4329 } else if (!empty($CFG->authloginviaemail)) { 4330 if ($email = clean_param($username, PARAM_EMAIL)) { 4331 $select = "mnethostid = :mnethostid AND LOWER(email) = LOWER(:email) AND deleted = 0"; 4332 $params = array('mnethostid' => $CFG->mnet_localhost_id, 'email' => $email); 4333 $users = $DB->get_records_select('user', $select, $params, 'id', 'id', 0, 2); 4334 if (count($users) === 1) { 4335 // Use email for login only if unique. 4336 $user = reset($users); 4337 $user = get_complete_user_data('id', $user->id); 4338 $username = $user->username; 4339 } 4340 unset($users); 4341 } 4342 } 4343 4344 // Make sure this request came from the login form. 4345 if (!\core\session\manager::validate_login_token($logintoken)) { 4346 $failurereason = AUTH_LOGIN_FAILED; 4347 4348 // Trigger login failed event (specifying the ID of the found user, if available). 4349 \core\event\user_login_failed::create([ 4350 'userid' => ($user->id ?? 0), 4351 'other' => [ 4352 'username' => $username, 4353 'reason' => $failurereason, 4354 ], 4355 ])->trigger(); 4356 4357 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Invalid Login Token: $username ".$_SERVER['HTTP_USER_AGENT']); 4358 return false; 4359 } 4360 4361 $authsenabled = get_enabled_auth_plugins(); 4362 4363 if ($user) { 4364 // Use manual if auth not set. 4365 $auth = empty($user->auth) ? 'manual' : $user->auth; 4366 4367 if (in_array($user->auth, $authsenabled)) { 4368 $authplugin = get_auth_plugin($user->auth); 4369 $authplugin->pre_user_login_hook($user); 4370 } 4371 4372 if (!empty($user->suspended)) { 4373 $failurereason = AUTH_LOGIN_SUSPENDED; 4374 4375 // Trigger login failed event. 4376 $event = \core\event\user_login_failed::create(array('userid' => $user->id, 4377 'other' => array('username' => $username, 'reason' => $failurereason))); 4378 $event->trigger(); 4379 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']); 4380 return false; 4381 } 4382 if ($auth=='nologin' or !is_enabled_auth($auth)) { 4383 // Legacy way to suspend user. 4384 $failurereason = AUTH_LOGIN_SUSPENDED; 4385 4386 // Trigger login failed event. 4387 $event = \core\event\user_login_failed::create(array('userid' => $user->id, 4388 'other' => array('username' => $username, 'reason' => $failurereason))); 4389 $event->trigger(); 4390 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Disabled Login: $username ".$_SERVER['HTTP_USER_AGENT']); 4391 return false; 4392 } 4393 $auths = array($auth); 4394 4395 } else { 4396 // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user(). 4397 if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'deleted' => 1))) { 4398 $failurereason = AUTH_LOGIN_NOUSER; 4399 4400 // Trigger login failed event. 4401 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, 4402 'reason' => $failurereason))); 4403 $event->trigger(); 4404 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Deleted Login: $username ".$_SERVER['HTTP_USER_AGENT']); 4405 return false; 4406 } 4407 4408 // User does not exist. 4409 $auths = $authsenabled; 4410 $user = new stdClass(); 4411 $user->id = 0; 4412 } 4413 4414 if ($ignorelockout) { 4415 // Some other mechanism protects against brute force password guessing, for example login form might include reCAPTCHA 4416 // or this function is called from a SSO script. 4417 } else if ($user->id) { 4418 // Verify login lockout after other ways that may prevent user login. 4419 if (login_is_lockedout($user)) { 4420 $failurereason = AUTH_LOGIN_LOCKOUT; 4421 4422 // Trigger login failed event. 4423 $event = \core\event\user_login_failed::create(array('userid' => $user->id, 4424 'other' => array('username' => $username, 'reason' => $failurereason))); 4425 $event->trigger(); 4426 4427 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Login lockout: $username ".$_SERVER['HTTP_USER_AGENT']); 4428 return false; 4429 } 4430 } else { 4431 // We can not lockout non-existing accounts. 4432 } 4433 4434 foreach ($auths as $auth) { 4435 $authplugin = get_auth_plugin($auth); 4436 4437 // On auth fail fall through to the next plugin. 4438 if (!$authplugin->user_login($username, $password)) { 4439 continue; 4440 } 4441 4442 // Before performing login actions, check if user still passes password policy, if admin setting is enabled. 4443 if (!empty($CFG->passwordpolicycheckonlogin)) { 4444 $errmsg = ''; 4445 $passed = check_password_policy($password, $errmsg, $user); 4446 if (!$passed) { 4447 // First trigger event for failure. 4448 $failedevent = \core\event\user_password_policy_failed::create_from_user($user); 4449 $failedevent->trigger(); 4450 4451 // If able to change password, set flag and move on. 4452 if ($authplugin->can_change_password()) { 4453 // Check if we are on internal change password page, or service is external, don't show notification. 4454 $internalchangeurl = new moodle_url('/login/change_password.php'); 4455 if (!($PAGE->has_set_url() && $internalchangeurl->compare($PAGE->url)) && $authplugin->is_internal()) { 4456 \core\notification::error(get_string('passwordpolicynomatch', '', $errmsg)); 4457 } 4458 set_user_preference('auth_forcepasswordchange', 1, $user); 4459 } else if ($authplugin->can_reset_password()) { 4460 // Else force a reset if possible. 4461 \core\notification::error(get_string('forcepasswordresetnotice', '', $errmsg)); 4462 redirect(new moodle_url('/login/forgot_password.php')); 4463 } else { 4464 $notifymsg = get_string('forcepasswordresetfailurenotice', '', $errmsg); 4465 // If support page is set, add link for help. 4466 if (!empty($CFG->supportpage)) { 4467 $link = \html_writer::link($CFG->supportpage, $CFG->supportpage); 4468 $link = \html_writer::tag('p', $link); 4469 $notifymsg .= $link; 4470 } 4471 4472 // If no change or reset is possible, add a notification for user. 4473 \core\notification::error($notifymsg); 4474 } 4475 } 4476 } 4477 4478 // Successful authentication. 4479 if ($user->id) { 4480 // User already exists in database. 4481 if (empty($user->auth)) { 4482 // For some reason auth isn't set yet. 4483 $DB->set_field('user', 'auth', $auth, array('id' => $user->id)); 4484 $user->auth = $auth; 4485 } 4486 4487 // If the existing hash is using an out-of-date algorithm (or the legacy md5 algorithm), then we should update to 4488 // the current hash algorithm while we have access to the user's password. 4489 update_internal_user_password($user, $password); 4490 4491 if ($authplugin->is_synchronised_with_external()) { 4492 // Update user record from external DB. 4493 $user = update_user_record_by_id($user->id); 4494 } 4495 } else { 4496 // The user is authenticated but user creation may be disabled. 4497 if (!empty($CFG->authpreventaccountcreation)) { 4498 $failurereason = AUTH_LOGIN_UNAUTHORISED; 4499 4500 // Trigger login failed event. 4501 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, 4502 'reason' => $failurereason))); 4503 $event->trigger(); 4504 4505 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Unknown user, can not create new accounts: $username ". 4506 $_SERVER['HTTP_USER_AGENT']); 4507 return false; 4508 } else { 4509 $user = create_user_record($username, $password, $auth); 4510 } 4511 } 4512 4513 $authplugin->sync_roles($user); 4514 4515 foreach ($authsenabled as $hau) { 4516 $hauth = get_auth_plugin($hau); 4517 $hauth->user_authenticated_hook($user, $username, $password); 4518 } 4519 4520 if (empty($user->id)) { 4521 $failurereason = AUTH_LOGIN_NOUSER; 4522 // Trigger login failed event. 4523 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, 4524 'reason' => $failurereason))); 4525 $event->trigger(); 4526 return false; 4527 } 4528 4529 if (!empty($user->suspended)) { 4530 // Just in case some auth plugin suspended account. 4531 $failurereason = AUTH_LOGIN_SUSPENDED; 4532 // Trigger login failed event. 4533 $event = \core\event\user_login_failed::create(array('userid' => $user->id, 4534 'other' => array('username' => $username, 'reason' => $failurereason))); 4535 $event->trigger(); 4536 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']); 4537 return false; 4538 } 4539 4540 login_attempt_valid($user); 4541 $failurereason = AUTH_LOGIN_OK; 4542 return $user; 4543 } 4544 4545 // Failed if all the plugins have failed. 4546 if (debugging('', DEBUG_ALL)) { 4547 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Failed Login: $username ".$_SERVER['HTTP_USER_AGENT']); 4548 } 4549 4550 if ($user->id) { 4551 login_attempt_failed($user); 4552 $failurereason = AUTH_LOGIN_FAILED; 4553 // Trigger login failed event. 4554 $event = \core\event\user_login_failed::create(array('userid' => $user->id, 4555 'other' => array('username' => $username, 'reason' => $failurereason))); 4556 $event->trigger(); 4557 } else { 4558 $failurereason = AUTH_LOGIN_NOUSER; 4559 // Trigger login failed event. 4560 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, 4561 'reason' => $failurereason))); 4562 $event->trigger(); 4563 } 4564 4565 return false; 4566 } 4567 4568 /** 4569 * Call to complete the user login process after authenticate_user_login() 4570 * has succeeded. It will setup the $USER variable and other required bits 4571 * and pieces. 4572 * 4573 * NOTE: 4574 * - It will NOT log anything -- up to the caller to decide what to log. 4575 * - this function does not set any cookies any more! 4576 * 4577 * @param stdClass $user 4578 * @param array $extrauserinfo 4579 * @return stdClass A {@link $USER} object - BC only, do not use 4580 */ 4581 function complete_user_login($user, array $extrauserinfo = []) { 4582 global $CFG, $DB, $USER, $SESSION; 4583 4584 \core\session\manager::login_user($user); 4585 4586 // Reload preferences from DB. 4587 unset($USER->preference); 4588 check_user_preferences_loaded($USER); 4589 4590 // Update login times. 4591 update_user_login_times(); 4592 4593 // Extra session prefs init. 4594 set_login_session_preferences(); 4595 4596 // Trigger login event. 4597 $event = \core\event\user_loggedin::create( 4598 array( 4599 'userid' => $USER->id, 4600 'objectid' => $USER->id, 4601 'other' => [ 4602 'username' => $USER->username, 4603 'extrauserinfo' => $extrauserinfo 4604 ] 4605 ) 4606 ); 4607 $event->trigger(); 4608 4609 // Check if the user is using a new browser or session (a new MoodleSession cookie is set in that case). 4610 // If the user is accessing from the same IP, ignore everything (most of the time will be a new session in the same browser). 4611 // Skip Web Service requests, CLI scripts, AJAX scripts, and request from the mobile app itself. 4612 $loginip = getremoteaddr(); 4613 $isnewip = isset($SESSION->userpreviousip) && $SESSION->userpreviousip != $loginip; 4614 $isvalidenv = (!WS_SERVER && !CLI_SCRIPT && !NO_MOODLE_COOKIES) || PHPUNIT_TEST; 4615 4616 if (!empty($SESSION->isnewsessioncookie) && $isnewip && $isvalidenv && !\core_useragent::is_moodle_app()) { 4617 4618 $logintime = time(); 4619 $ismoodleapp = false; 4620 $useragent = \core_useragent::get_user_agent_string(); 4621 4622 // Schedule adhoc task to sent a login notification to the user. 4623 $task = new \core\task\send_login_notifications(); 4624 $task->set_userid($USER->id); 4625 $task->set_custom_data(compact('ismoodleapp', 'useragent', 'loginip', 'logintime')); 4626 $task->set_component('core'); 4627 \core\task\manager::queue_adhoc_task($task); 4628 } 4629 4630 // Queue migrating the messaging data, if we need to. 4631 if (!get_user_preferences('core_message_migrate_data', false, $USER->id)) { 4632 // Check if there are any legacy messages to migrate. 4633 if (\core_message\helper::legacy_messages_exist($USER->id)) { 4634 \core_message\task\migrate_message_data::queue_task($USER->id); 4635 } else { 4636 set_user_preference('core_message_migrate_data', true, $USER->id); 4637 } 4638 } 4639 4640 if (isguestuser()) { 4641 // No need to continue when user is THE guest. 4642 return $USER; 4643 } 4644 4645 if (CLI_SCRIPT) { 4646 // We can redirect to password change URL only in browser. 4647 return $USER; 4648 } 4649 4650 // Select password change url. 4651 $userauth = get_auth_plugin($USER->auth); 4652 4653 // Check whether the user should be changing password. 4654 if (get_user_preferences('auth_forcepasswordchange', false)) { 4655 if ($userauth->can_change_password()) { 4656 if ($changeurl = $userauth->change_password_url()) { 4657 redirect($changeurl); 4658 } else { 4659 require_once($CFG->dirroot . '/login/lib.php'); 4660 $SESSION->wantsurl = core_login_get_return_url(); 4661 redirect($CFG->wwwroot.'/login/change_password.php'); 4662 } 4663 } else { 4664 throw new \moodle_exception('nopasswordchangeforced', 'auth'); 4665 } 4666 } 4667 return $USER; 4668 } 4669 4670 /** 4671 * Check a password hash to see if it was hashed using the legacy hash algorithm (md5). 4672 * 4673 * @param string $password String to check. 4674 * @return boolean True if the $password matches the format of an md5 sum. 4675 */ 4676 function password_is_legacy_hash($password) { 4677 return (bool) preg_match('/^[0-9a-f]{32}$/', $password); 4678 } 4679 4680 /** 4681 * Compare password against hash stored in user object to determine if it is valid. 4682 * 4683 * If necessary it also updates the stored hash to the current format. 4684 * 4685 * @param stdClass $user (Password property may be updated). 4686 * @param string $password Plain text password. 4687 * @return bool True if password is valid. 4688 */ 4689 function validate_internal_user_password($user, $password) { 4690 global $CFG; 4691 4692 if ($user->password === AUTH_PASSWORD_NOT_CACHED) { 4693 // Internal password is not used at all, it can not validate. 4694 return false; 4695 } 4696 4697 // If hash isn't a legacy (md5) hash, validate using the library function. 4698 if (!password_is_legacy_hash($user->password)) { 4699 return password_verify($password, $user->password); 4700 } 4701 4702 // Otherwise we need to check for a legacy (md5) hash instead. If the hash 4703 // is valid we can then update it to the new algorithm. 4704 4705 $sitesalt = isset($CFG->passwordsaltmain) ? $CFG->passwordsaltmain : ''; 4706 $validated = false; 4707 4708 if ($user->password === md5($password.$sitesalt) 4709 or $user->password === md5($password) 4710 or $user->password === md5(addslashes($password).$sitesalt) 4711 or $user->password === md5(addslashes($password))) { 4712 // Note: we are intentionally using the addslashes() here because we 4713 // need to accept old password hashes of passwords with magic quotes. 4714 $validated = true; 4715 4716 } else { 4717 for ($i=1; $i<=20; $i++) { // 20 alternative salts should be enough, right? 4718 $alt = 'passwordsaltalt'.$i; 4719 if (!empty($CFG->$alt)) { 4720 if ($user->password === md5($password.$CFG->$alt) or $user->password === md5(addslashes($password).$CFG->$alt)) { 4721 $validated = true; 4722 break; 4723 } 4724 } 4725 } 4726 } 4727 4728 if ($validated) { 4729 // If the password matches the existing md5 hash, update to the 4730 // current hash algorithm while we have access to the user's password. 4731 update_internal_user_password($user, $password); 4732 } 4733 4734 return $validated; 4735 } 4736 4737 /** 4738 * Calculate hash for a plain text password. 4739 * 4740 * @param string $password Plain text password to be hashed. 4741 * @param bool $fasthash If true, use a low cost factor when generating the hash 4742 * This is much faster to generate but makes the hash 4743 * less secure. It is used when lots of hashes need to 4744 * be generated quickly. 4745 * @return string The hashed password. 4746 * 4747 * @throws moodle_exception If a problem occurs while generating the hash. 4748 */ 4749 function hash_internal_user_password($password, $fasthash = false) { 4750 global $CFG; 4751 4752 // Set the cost factor to 4 for fast hashing, otherwise use default cost. 4753 $options = ($fasthash) ? array('cost' => 4) : array(); 4754 4755 $generatedhash = password_hash($password, PASSWORD_DEFAULT, $options); 4756 4757 if ($generatedhash === false || $generatedhash === null) { 4758 throw new moodle_exception('Failed to generate password hash.'); 4759 } 4760 4761 return $generatedhash; 4762 } 4763 4764 /** 4765 * Update password hash in user object (if necessary). 4766 * 4767 * The password is updated if: 4768 * 1. The password has changed (the hash of $user->password is different 4769 * to the hash of $password). 4770 * 2. The existing hash is using an out-of-date algorithm (or the legacy 4771 * md5 algorithm). 4772 * 4773 * Updating the password will modify the $user object and the database 4774 * record to use the current hashing algorithm. 4775 * It will remove Web Services user tokens too. 4776 * 4777 * @param stdClass $user User object (password property may be updated). 4778 * @param string $password Plain text password. 4779 * @param bool $fasthash If true, use a low cost factor when generating the hash 4780 * This is much faster to generate but makes the hash 4781 * less secure. It is used when lots of hashes need to 4782 * be generated quickly. 4783 * @return bool Always returns true. 4784 */ 4785 function update_internal_user_password($user, $password, $fasthash = false) { 4786 global $CFG, $DB; 4787 4788 // Figure out what the hashed password should be. 4789 if (!isset($user->auth)) { 4790 debugging('User record in update_internal_user_password() must include field auth', 4791 DEBUG_DEVELOPER); 4792 $user->auth = $DB->get_field('user', 'auth', array('id' => $user->id)); 4793 } 4794 $authplugin = get_auth_plugin($user->auth); 4795 if ($authplugin->prevent_local_passwords()) { 4796 $hashedpassword = AUTH_PASSWORD_NOT_CACHED; 4797 } else { 4798 $hashedpassword = hash_internal_user_password($password, $fasthash); 4799 } 4800 4801 $algorithmchanged = false; 4802 4803 if ($hashedpassword === AUTH_PASSWORD_NOT_CACHED) { 4804 // Password is not cached, update it if not set to AUTH_PASSWORD_NOT_CACHED. 4805 $passwordchanged = ($user->password !== $hashedpassword); 4806 4807 } else if (isset($user->password)) { 4808 // If verification fails then it means the password has changed. 4809 $passwordchanged = !password_verify($password, $user->password); 4810 $algorithmchanged = password_needs_rehash($user->password, PASSWORD_DEFAULT); 4811 } else { 4812 // While creating new user, password in unset in $user object, to avoid 4813 // saving it with user_create() 4814 $passwordchanged = true; 4815 } 4816 4817 if ($passwordchanged || $algorithmchanged) { 4818 $DB->set_field('user', 'password', $hashedpassword, array('id' => $user->id)); 4819 $user->password = $hashedpassword; 4820 4821 // Trigger event. 4822 $user = $DB->get_record('user', array('id' => $user->id)); 4823 \core\event\user_password_updated::create_from_user($user)->trigger(); 4824 4825 // Remove WS user tokens. 4826 if (!empty($CFG->passwordchangetokendeletion)) { 4827 require_once($CFG->dirroot.'/webservice/lib.php'); 4828 webservice::delete_user_ws_tokens($user->id); 4829 } 4830 } 4831 4832 return true; 4833 } 4834 4835 /** 4836 * Get a complete user record, which includes all the info in the user record. 4837 * 4838 * Intended for setting as $USER session variable 4839 * 4840 * @param string $field The user field to be checked for a given value. 4841 * @param string $value The value to match for $field. 4842 * @param int $mnethostid 4843 * @param bool $throwexception If true, it will throw an exception when there's no record found or when there are multiple records 4844 * found. Otherwise, it will just return false. 4845 * @return mixed False, or A {@link $USER} object. 4846 */ 4847 function get_complete_user_data($field, $value, $mnethostid = null, $throwexception = false) { 4848 global $CFG, $DB; 4849 4850 if (!$field || !$value) { 4851 return false; 4852 } 4853 4854 // Change the field to lowercase. 4855 $field = core_text::strtolower($field); 4856 4857 // List of case insensitive fields. 4858 $caseinsensitivefields = ['email']; 4859 4860 // Username input is forced to lowercase and should be case sensitive. 4861 if ($field == 'username') { 4862 $value = core_text::strtolower($value); 4863 } 4864 4865 // Build the WHERE clause for an SQL query. 4866 $params = array('fieldval' => $value); 4867 4868 // Do a case-insensitive query, if necessary. These are generally very expensive. The performance can be improved on some DBs 4869 // such as MySQL by pre-filtering users with accent-insensitive subselect. 4870 if (in_array($field, $caseinsensitivefields)) { 4871 $fieldselect = $DB->sql_equal($field, ':fieldval', false); 4872 $idsubselect = $DB->sql_equal($field, ':fieldval2', false, false); 4873 $params['fieldval2'] = $value; 4874 } else { 4875 $fieldselect = "$field = :fieldval"; 4876 $idsubselect = ''; 4877 } 4878 $constraints = "$fieldselect AND deleted <> 1"; 4879 4880 // If we are loading user data based on anything other than id, 4881 // we must also restrict our search based on mnet host. 4882 if ($field != 'id') { 4883 if (empty($mnethostid)) { 4884 // If empty, we restrict to local users. 4885 $mnethostid = $CFG->mnet_localhost_id; 4886 } 4887 } 4888 if (!empty($mnethostid)) { 4889 $params['mnethostid'] = $mnethostid; 4890 $constraints .= " AND mnethostid = :mnethostid"; 4891 } 4892 4893 if ($idsubselect) { 4894 $constraints .= " AND id IN (SELECT id FROM {user} WHERE {$idsubselect})"; 4895 } 4896 4897 // Get all the basic user data. 4898 try { 4899 // Make sure that there's only a single record that matches our query. 4900 // For example, when fetching by email, multiple records might match the query as there's no guarantee that email addresses 4901 // are unique. Therefore we can't reliably tell whether the user profile data that we're fetching is the correct one. 4902 $user = $DB->get_record_select('user', $constraints, $params, '*', MUST_EXIST); 4903 } catch (dml_exception $exception) { 4904 if ($throwexception) { 4905 throw $exception; 4906 } else { 4907 // Return false when no records or multiple records were found. 4908 return false; 4909 } 4910 } 4911 4912 // Get various settings and preferences. 4913 4914 // Preload preference cache. 4915 check_user_preferences_loaded($user); 4916 4917 // Load course enrolment related stuff. 4918 $user->lastcourseaccess = array(); // During last session. 4919 $user->currentcourseaccess = array(); // During current session. 4920 if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid' => $user->id))) { 4921 foreach ($lastaccesses as $lastaccess) { 4922 $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess; 4923 } 4924 } 4925 4926 // Add cohort theme. 4927 if (!empty($CFG->allowcohortthemes)) { 4928 require_once($CFG->dirroot . '/cohort/lib.php'); 4929 if ($cohorttheme = cohort_get_user_cohort_theme($user->id)) { 4930 $user->cohorttheme = $cohorttheme; 4931 } 4932 } 4933 4934 // Add the custom profile fields to the user record. 4935 $user->profile = array(); 4936 if (!isguestuser($user)) { 4937 require_once($CFG->dirroot.'/user/profile/lib.php'); 4938 profile_load_custom_fields($user); 4939 } 4940 4941 // Rewrite some variables if necessary. 4942 if (!empty($user->description)) { 4943 // No need to cart all of it around. 4944 $user->description = true; 4945 } 4946 if (isguestuser($user)) { 4947 // Guest language always same as site. 4948 $user->lang = get_newuser_language(); 4949 // Name always in current language. 4950 $user->firstname = get_string('guestuser'); 4951 $user->lastname = ' '; 4952 } 4953 4954 return $user; 4955 } 4956 4957 /** 4958 * Validate a password against the configured password policy 4959 * 4960 * @param string $password the password to be checked against the password policy 4961 * @param string $errmsg the error message to display when the password doesn't comply with the policy. 4962 * @param stdClass $user the user object to perform password validation against. Defaults to null if not provided. 4963 * 4964 * @return bool true if the password is valid according to the policy. false otherwise. 4965 */ 4966 function check_password_policy($password, &$errmsg, $user = null) { 4967 global $CFG; 4968 4969 if (!empty($CFG->passwordpolicy) && !isguestuser($user)) { 4970 $errmsg = ''; 4971 if (core_text::strlen($password) < $CFG->minpasswordlength) { 4972 $errmsg .= '<div>'. get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength) .'</div>'; 4973 } 4974 if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) { 4975 $errmsg .= '<div>'. get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits) .'</div>'; 4976 } 4977 if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) { 4978 $errmsg .= '<div>'. get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower) .'</div>'; 4979 } 4980 if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) { 4981 $errmsg .= '<div>'. get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper) .'</div>'; 4982 } 4983 if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) { 4984 $errmsg .= '<div>'. get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum) .'</div>'; 4985 } 4986 if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) { 4987 $errmsg .= '<div>'. get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars) .'</div>'; 4988 } 4989 4990 // Fire any additional password policy functions from plugins. 4991 // Plugin functions should output an error message string or empty string for success. 4992 $pluginsfunction = get_plugins_with_function('check_password_policy'); 4993 foreach ($pluginsfunction as $plugintype => $plugins) { 4994 foreach ($plugins as $pluginfunction) { 4995 $pluginerr = $pluginfunction($password, $user); 4996 if ($pluginerr) { 4997 $errmsg .= '<div>'. $pluginerr .'</div>'; 4998 } 4999 } 5000 } 5001 } 5002 5003 if ($errmsg == '') { 5004 return true; 5005 } else { 5006 return false; 5007 } 5008 } 5009 5010 5011 /** 5012 * When logging in, this function is run to set certain preferences for the current SESSION. 5013 */ 5014 function set_login_session_preferences() { 5015 global $SESSION; 5016 5017 $SESSION->justloggedin = true; 5018 5019 unset($SESSION->lang); 5020 unset($SESSION->forcelang); 5021 unset($SESSION->load_navigation_admin); 5022 } 5023 5024 5025 /** 5026 * Delete a course, including all related data from the database, and any associated files. 5027 * 5028 * @param mixed $courseorid The id of the course or course object to delete. 5029 * @param bool $showfeedback Whether to display notifications of each action the function performs. 5030 * @return bool true if all the removals succeeded. false if there were any failures. If this 5031 * method returns false, some of the removals will probably have succeeded, and others 5032 * failed, but you have no way of knowing which. 5033 */ 5034 function delete_course($courseorid, $showfeedback = true) { 5035 global $DB; 5036 5037 if (is_object($courseorid)) { 5038 $courseid = $courseorid->id; 5039 $course = $courseorid; 5040 } else { 5041 $courseid = $courseorid; 5042 if (!$course = $DB->get_record('course', array('id' => $courseid))) { 5043 return false; 5044 } 5045 } 5046 $context = context_course::instance($courseid); 5047 5048 // Frontpage course can not be deleted!! 5049 if ($courseid == SITEID) { 5050 return false; 5051 } 5052 5053 // Allow plugins to use this course before we completely delete it. 5054 if ($pluginsfunction = get_plugins_with_function('pre_course_delete')) { 5055 foreach ($pluginsfunction as $plugintype => $plugins) { 5056 foreach ($plugins as $pluginfunction) { 5057 $pluginfunction($course); 5058 } 5059 } 5060 } 5061 5062 // Tell the search manager we are about to delete a course. This prevents us sending updates 5063 // for each individual context being deleted. 5064 \core_search\manager::course_deleting_start($courseid); 5065 5066 $handler = core_course\customfield\course_handler::create(); 5067 $handler->delete_instance($courseid); 5068 5069 // Make the course completely empty. 5070 remove_course_contents($courseid, $showfeedback); 5071 5072 // Delete the course and related context instance. 5073 context_helper::delete_instance(CONTEXT_COURSE, $courseid); 5074 5075 $DB->delete_records("course", array("id" => $courseid)); 5076 $DB->delete_records("course_format_options", array("courseid" => $courseid)); 5077 5078 // Reset all course related caches here. 5079 core_courseformat\base::reset_course_cache($courseid); 5080 5081 // Tell search that we have deleted the course so it can delete course data from the index. 5082 \core_search\manager::course_deleting_finish($courseid); 5083 5084 // Trigger a course deleted event. 5085 $event = \core\event\course_deleted::create(array( 5086 'objectid' => $course->id, 5087 'context' => $context, 5088 'other' => array( 5089 'shortname' => $course->shortname, 5090 'fullname' => $course->fullname, 5091 'idnumber' => $course->idnumber 5092 ) 5093 )); 5094 $event->add_record_snapshot('course', $course); 5095 $event->trigger(); 5096 5097 return true; 5098 } 5099 5100 /** 5101 * Clear a course out completely, deleting all content but don't delete the course itself. 5102 * 5103 * This function does not verify any permissions. 5104 * 5105 * Please note this function also deletes all user enrolments, 5106 * enrolment instances and role assignments by default. 5107 * 5108 * $options: 5109 * - 'keep_roles_and_enrolments' - false by default 5110 * - 'keep_groups_and_groupings' - false by default 5111 * 5112 * @param int $courseid The id of the course that is being deleted 5113 * @param bool $showfeedback Whether to display notifications of each action the function performs. 5114 * @param array $options extra options 5115 * @return bool true if all the removals succeeded. false if there were any failures. If this 5116 * method returns false, some of the removals will probably have succeeded, and others 5117 * failed, but you have no way of knowing which. 5118 */ 5119 function remove_course_contents($courseid, $showfeedback = true, array $options = null) { 5120 global $CFG, $DB, $OUTPUT; 5121 5122 require_once($CFG->libdir.'/badgeslib.php'); 5123 require_once($CFG->libdir.'/completionlib.php'); 5124 require_once($CFG->libdir.'/questionlib.php'); 5125 require_once($CFG->libdir.'/gradelib.php'); 5126 require_once($CFG->dirroot.'/group/lib.php'); 5127 require_once($CFG->dirroot.'/comment/lib.php'); 5128 require_once($CFG->dirroot.'/rating/lib.php'); 5129 require_once($CFG->dirroot.'/notes/lib.php'); 5130 5131 // Handle course badges. 5132 badges_handle_course_deletion($courseid); 5133 5134 // NOTE: these concatenated strings are suboptimal, but it is just extra info... 5135 $strdeleted = get_string('deleted').' - '; 5136 5137 // Some crazy wishlist of stuff we should skip during purging of course content. 5138 $options = (array)$options; 5139 5140 $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); 5141 $coursecontext = context_course::instance($courseid); 5142 $fs = get_file_storage(); 5143 5144 // Delete course completion information, this has to be done before grades and enrols. 5145 $cc = new completion_info($course); 5146 $cc->clear_criteria(); 5147 if ($showfeedback) { 5148 echo $OUTPUT->notification($strdeleted.get_string('completion', 'completion'), 'notifysuccess'); 5149 } 5150 5151 // Remove all data from gradebook - this needs to be done before course modules 5152 // because while deleting this information, the system may need to reference 5153 // the course modules that own the grades. 5154 remove_course_grades($courseid, $showfeedback); 5155 remove_grade_letters($coursecontext, $showfeedback); 5156 5157 // Delete course blocks in any all child contexts, 5158 // they may depend on modules so delete them first. 5159 $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2. 5160 foreach ($childcontexts as $childcontext) { 5161 blocks_delete_all_for_context($childcontext->id); 5162 } 5163 unset($childcontexts); 5164 blocks_delete_all_for_context($coursecontext->id); 5165 if ($showfeedback) { 5166 echo $OUTPUT->notification($strdeleted.get_string('type_block_plural', 'plugin'), 'notifysuccess'); 5167 } 5168 5169 $DB->set_field('course_modules', 'deletioninprogress', '1', ['course' => $courseid]); 5170 rebuild_course_cache($courseid, true); 5171 5172 // Get the list of all modules that are properly installed. 5173 $allmodules = $DB->get_records_menu('modules', array(), '', 'name, id'); 5174 5175 // Delete every instance of every module, 5176 // this has to be done before deleting of course level stuff. 5177 $locations = core_component::get_plugin_list('mod'); 5178 foreach ($locations as $modname => $moddir) { 5179 if ($modname === 'NEWMODULE') { 5180 continue; 5181 } 5182 if (array_key_exists($modname, $allmodules)) { 5183 $sql = "SELECT cm.*, m.id AS modinstance, m.name, '$modname' AS modname 5184 FROM {".$modname."} m 5185 LEFT JOIN {course_modules} cm ON cm.instance = m.id AND cm.module = :moduleid 5186 WHERE m.course = :courseid"; 5187 $instances = $DB->get_records_sql($sql, array('courseid' => $course->id, 5188 'modulename' => $modname, 'moduleid' => $allmodules[$modname])); 5189 5190 include_once("$moddir/lib.php"); // Shows php warning only if plugin defective. 5191 $moddelete = $modname .'_delete_instance'; // Delete everything connected to an instance. 5192 5193 if ($instances) { 5194 foreach ($instances as $cm) { 5195 if ($cm->id) { 5196 // Delete activity context questions and question categories. 5197 question_delete_activity($cm); 5198 // Notify the competency subsystem. 5199 \core_competency\api::hook_course_module_deleted($cm); 5200 5201 // Delete all tag instances associated with the instance of this module. 5202 core_tag_tag::delete_instances("mod_{$modname}", null, context_module::instance($cm->id)->id); 5203 core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id); 5204 } 5205 if (function_exists($moddelete)) { 5206 // This purges all module data in related tables, extra user prefs, settings, etc. 5207 $moddelete($cm->modinstance); 5208 } else { 5209 // NOTE: we should not allow installation of modules with missing delete support! 5210 debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!"); 5211 $DB->delete_records($modname, array('id' => $cm->modinstance)); 5212 } 5213 5214 if ($cm->id) { 5215 // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition. 5216 context_helper::delete_instance(CONTEXT_MODULE, $cm->id); 5217 $DB->delete_records('course_modules_completion', ['coursemoduleid' => $cm->id]); 5218 $DB->delete_records('course_modules_viewed', ['coursemoduleid' => $cm->id]); 5219 $DB->delete_records('course_modules', array('id' => $cm->id)); 5220 rebuild_course_cache($cm->course, true); 5221 } 5222 } 5223 } 5224 if ($instances and $showfeedback) { 5225 echo $OUTPUT->notification($strdeleted.get_string('pluginname', $modname), 'notifysuccess'); 5226 } 5227 } else { 5228 // Ooops, this module is not properly installed, force-delete it in the next block. 5229 } 5230 } 5231 5232 // We have tried to delete everything the nice way - now let's force-delete any remaining module data. 5233 5234 // Delete completion defaults. 5235 $DB->delete_records("course_completion_defaults", array("course" => $courseid)); 5236 5237 // Remove all data from availability and completion tables that is associated 5238 // with course-modules belonging to this course. Note this is done even if the 5239 // features are not enabled now, in case they were enabled previously. 5240 $DB->delete_records_subquery('course_modules_completion', 'coursemoduleid', 'id', 5241 'SELECT id from {course_modules} WHERE course = ?', [$courseid]); 5242 $DB->delete_records_subquery('course_modules_viewed', 'coursemoduleid', 'id', 5243 'SELECT id from {course_modules} WHERE course = ?', [$courseid]); 5244 5245 // Remove course-module data that has not been removed in modules' _delete_instance callbacks. 5246 $cms = $DB->get_records('course_modules', array('course' => $course->id)); 5247 $allmodulesbyid = array_flip($allmodules); 5248 foreach ($cms as $cm) { 5249 if (array_key_exists($cm->module, $allmodulesbyid)) { 5250 try { 5251 $DB->delete_records($allmodulesbyid[$cm->module], array('id' => $cm->instance)); 5252 } catch (Exception $e) { 5253 // Ignore weird or missing table problems. 5254 } 5255 } 5256 context_helper::delete_instance(CONTEXT_MODULE, $cm->id); 5257 $DB->delete_records('course_modules', array('id' => $cm->id)); 5258 rebuild_course_cache($cm->course, true); 5259 } 5260 5261 if ($showfeedback) { 5262 echo $OUTPUT->notification($strdeleted.get_string('type_mod_plural', 'plugin'), 'notifysuccess'); 5263 } 5264 5265 // Delete questions and question categories. 5266 question_delete_course($course); 5267 if ($showfeedback) { 5268 echo $OUTPUT->notification($strdeleted.get_string('questions', 'question'), 'notifysuccess'); 5269 } 5270 5271 // Delete content bank contents. 5272 $cb = new \core_contentbank\contentbank(); 5273 $cbdeleted = $cb->delete_contents($coursecontext); 5274 if ($showfeedback && $cbdeleted) { 5275 echo $OUTPUT->notification($strdeleted.get_string('contentbank', 'contentbank'), 'notifysuccess'); 5276 } 5277 5278 // Make sure there are no subcontexts left - all valid blocks and modules should be already gone. 5279 $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2. 5280 foreach ($childcontexts as $childcontext) { 5281 $childcontext->delete(); 5282 } 5283 unset($childcontexts); 5284 5285 // Remove roles and enrolments by default. 5286 if (empty($options['keep_roles_and_enrolments'])) { 5287 // This hack is used in restore when deleting contents of existing course. 5288 // During restore, we should remove only enrolment related data that the user performing the restore has a 5289 // permission to remove. 5290 $userid = $options['userid'] ?? null; 5291 enrol_course_delete($course, $userid); 5292 role_unassign_all(array('contextid' => $coursecontext->id, 'component' => ''), true); 5293 if ($showfeedback) { 5294 echo $OUTPUT->notification($strdeleted.get_string('type_enrol_plural', 'plugin'), 'notifysuccess'); 5295 } 5296 } 5297 5298 // Delete any groups, removing members and grouping/course links first. 5299 if (empty($options['keep_groups_and_groupings'])) { 5300 groups_delete_groupings($course->id, $showfeedback); 5301 groups_delete_groups($course->id, $showfeedback); 5302 } 5303 5304 // Filters be gone! 5305 filter_delete_all_for_context($coursecontext->id); 5306 5307 // Notes, you shall not pass! 5308 note_delete_all($course->id); 5309 5310 // Die comments! 5311 comment::delete_comments($coursecontext->id); 5312 5313 // Ratings are history too. 5314 $delopt = new stdclass(); 5315 $delopt->contextid = $coursecontext->id; 5316 $rm = new rating_manager(); 5317 $rm->delete_ratings($delopt); 5318 5319 // Delete course tags. 5320 core_tag_tag::remove_all_item_tags('core', 'course', $course->id); 5321 5322 // Give the course format the opportunity to remove its obscure data. 5323 $format = course_get_format($course); 5324 $format->delete_format_data(); 5325 5326 // Notify the competency subsystem. 5327 \core_competency\api::hook_course_deleted($course); 5328 5329 // Delete calendar events. 5330 $DB->delete_records('event', array('courseid' => $course->id)); 5331 $fs->delete_area_files($coursecontext->id, 'calendar'); 5332 5333 // Delete all related records in other core tables that may have a courseid 5334 // This array stores the tables that need to be cleared, as 5335 // table_name => column_name that contains the course id. 5336 $tablestoclear = array( 5337 'backup_courses' => 'courseid', // Scheduled backup stuff. 5338 'user_lastaccess' => 'courseid', // User access info. 5339 ); 5340 foreach ($tablestoclear as $table => $col) { 5341 $DB->delete_records($table, array($col => $course->id)); 5342 } 5343 5344 // Delete all course backup files. 5345 $fs->delete_area_files($coursecontext->id, 'backup'); 5346 5347 // Cleanup course record - remove links to deleted stuff. 5348 $oldcourse = new stdClass(); 5349 $oldcourse->id = $course->id; 5350 $oldcourse->summary = ''; 5351 $oldcourse->cacherev = 0; 5352 $oldcourse->legacyfiles = 0; 5353 if (!empty($options['keep_groups_and_groupings'])) { 5354 $oldcourse->defaultgroupingid = 0; 5355 } 5356 $DB->update_record('course', $oldcourse); 5357 5358 // Delete course sections. 5359 $DB->delete_records('course_sections', array('course' => $course->id)); 5360 5361 // Delete legacy, section and any other course files. 5362 $fs->delete_area_files($coursecontext->id, 'course'); // Files from summary and section. 5363 5364 // Delete all remaining stuff linked to context such as files, comments, ratings, etc. 5365 if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) { 5366 // Easy, do not delete the context itself... 5367 $coursecontext->delete_content(); 5368 } else { 5369 // Hack alert!!!! 5370 // We can not drop all context stuff because it would bork enrolments and roles, 5371 // there might be also files used by enrol plugins... 5372 } 5373 5374 // Delete legacy files - just in case some files are still left there after conversion to new file api, 5375 // also some non-standard unsupported plugins may try to store something there. 5376 fulldelete($CFG->dataroot.'/'.$course->id); 5377 5378 // Delete from cache to reduce the cache size especially makes sense in case of bulk course deletion. 5379 course_modinfo::purge_course_cache($courseid); 5380 5381 // Trigger a course content deleted event. 5382 $event = \core\event\course_content_deleted::create(array( 5383 'objectid' => $course->id, 5384 'context' => $coursecontext, 5385 'other' => array('shortname' => $course->shortname, 5386 'fullname' => $course->fullname, 5387 'options' => $options) // Passing this for legacy reasons. 5388 )); 5389 $event->add_record_snapshot('course', $course); 5390 $event->trigger(); 5391 5392 return true; 5393 } 5394 5395 /** 5396 * Change dates in module - used from course reset. 5397 * 5398 * @param string $modname forum, assignment, etc 5399 * @param array $fields array of date fields from mod table 5400 * @param int $timeshift time difference 5401 * @param int $courseid 5402 * @param int $modid (Optional) passed if specific mod instance in course needs to be updated. 5403 * @return bool success 5404 */ 5405 function shift_course_mod_dates($modname, $fields, $timeshift, $courseid, $modid = 0) { 5406 global $CFG, $DB; 5407 include_once($CFG->dirroot.'/mod/'.$modname.'/lib.php'); 5408 5409 $return = true; 5410 $params = array($timeshift, $courseid); 5411 foreach ($fields as $field) { 5412 $updatesql = "UPDATE {".$modname."} 5413 SET $field = $field + ? 5414 WHERE course=? AND $field<>0"; 5415 if ($modid) { 5416 $updatesql .= ' AND id=?'; 5417 $params[] = $modid; 5418 } 5419 $return = $DB->execute($updatesql, $params) && $return; 5420 } 5421 5422 return $return; 5423 } 5424 5425 /** 5426 * This function will empty a course of user data. 5427 * It will retain the activities and the structure of the course. 5428 * 5429 * @param object $data an object containing all the settings including courseid (without magic quotes) 5430 * @return array status array of array component, item, error 5431 */ 5432 function reset_course_userdata($data) { 5433 global $CFG, $DB; 5434 require_once($CFG->libdir.'/gradelib.php'); 5435 require_once($CFG->libdir.'/completionlib.php'); 5436 require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php'); 5437 require_once($CFG->dirroot.'/group/lib.php'); 5438 5439 $data->courseid = $data->id; 5440 $context = context_course::instance($data->courseid); 5441 5442 $eventparams = array( 5443 'context' => $context, 5444 'courseid' => $data->id, 5445 'other' => array( 5446 'reset_options' => (array) $data 5447 ) 5448 ); 5449 $event = \core\event\course_reset_started::create($eventparams); 5450 $event->trigger(); 5451 5452 // Calculate the time shift of dates. 5453 if (!empty($data->reset_start_date)) { 5454 // Time part of course startdate should be zero. 5455 $data->timeshift = $data->reset_start_date - usergetmidnight($data->reset_start_date_old); 5456 } else { 5457 $data->timeshift = 0; 5458 } 5459 5460 // Result array: component, item, error. 5461 $status = array(); 5462 5463 // Start the resetting. 5464 $componentstr = get_string('general'); 5465 5466 // Move the course start time. 5467 if (!empty($data->reset_start_date) and $data->timeshift) { 5468 // Change course start data. 5469 $DB->set_field('course', 'startdate', $data->reset_start_date, array('id' => $data->courseid)); 5470 // Update all course and group events - do not move activity events. 5471 $updatesql = "UPDATE {event} 5472 SET timestart = timestart + ? 5473 WHERE courseid=? AND instance=0"; 5474 $DB->execute($updatesql, array($data->timeshift, $data->courseid)); 5475 5476 // Update any date activity restrictions. 5477 if ($CFG->enableavailability) { 5478 \availability_date\condition::update_all_dates($data->courseid, $data->timeshift); 5479 } 5480 5481 // Update completion expected dates. 5482 if ($CFG->enablecompletion) { 5483 $modinfo = get_fast_modinfo($data->courseid); 5484 $changed = false; 5485 foreach ($modinfo->get_cms() as $cm) { 5486 if ($cm->completion && !empty($cm->completionexpected)) { 5487 $DB->set_field('course_modules', 'completionexpected', $cm->completionexpected + $data->timeshift, 5488 array('id' => $cm->id)); 5489 $changed = true; 5490 } 5491 } 5492 5493 // Clear course cache if changes made. 5494 if ($changed) { 5495 rebuild_course_cache($data->courseid, true); 5496 } 5497 5498 // Update course date completion criteria. 5499 \completion_criteria_date::update_date($data->courseid, $data->timeshift); 5500 } 5501 5502 $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false); 5503 } 5504 5505 if (!empty($data->reset_end_date)) { 5506 // If the user set a end date value respect it. 5507 $DB->set_field('course', 'enddate', $data->reset_end_date, array('id' => $data->courseid)); 5508 } else if ($data->timeshift > 0 && $data->reset_end_date_old) { 5509 // If there is a time shift apply it to the end date as well. 5510 $enddate = $data->reset_end_date_old + $data->timeshift; 5511 $DB->set_field('course', 'enddate', $enddate, array('id' => $data->courseid)); 5512 } 5513 5514 if (!empty($data->reset_events)) { 5515 $DB->delete_records('event', array('courseid' => $data->courseid)); 5516 $status[] = array('component' => $componentstr, 'item' => get_string('deleteevents', 'calendar'), 'error' => false); 5517 } 5518 5519 if (!empty($data->reset_notes)) { 5520 require_once($CFG->dirroot.'/notes/lib.php'); 5521 note_delete_all($data->courseid); 5522 $status[] = array('component' => $componentstr, 'item' => get_string('deletenotes', 'notes'), 'error' => false); 5523 } 5524 5525 if (!empty($data->delete_blog_associations)) { 5526 require_once($CFG->dirroot.'/blog/lib.php'); 5527 blog_remove_associations_for_course($data->courseid); 5528 $status[] = array('component' => $componentstr, 'item' => get_string('deleteblogassociations', 'blog'), 'error' => false); 5529 } 5530 5531 if (!empty($data->reset_completion)) { 5532 // Delete course and activity completion information. 5533 $course = $DB->get_record('course', array('id' => $data->courseid)); 5534 $cc = new completion_info($course); 5535 $cc->delete_all_completion_data(); 5536 $status[] = array('component' => $componentstr, 5537 'item' => get_string('deletecompletiondata', 'completion'), 'error' => false); 5538 } 5539 5540 if (!empty($data->reset_competency_ratings)) { 5541 \core_competency\api::hook_course_reset_competency_ratings($data->courseid); 5542 $status[] = array('component' => $componentstr, 5543 'item' => get_string('deletecompetencyratings', 'core_competency'), 'error' => false); 5544 } 5545 5546 $componentstr = get_string('roles'); 5547 5548 if (!empty($data->reset_roles_overrides)) { 5549 $children = $context->get_child_contexts(); 5550 foreach ($children as $child) { 5551 $child->delete_capabilities(); 5552 } 5553 $context->delete_capabilities(); 5554 $status[] = array('component' => $componentstr, 'item' => get_string('deletecourseoverrides', 'role'), 'error' => false); 5555 } 5556 5557 if (!empty($data->reset_roles_local)) { 5558 $children = $context->get_child_contexts(); 5559 foreach ($children as $child) { 5560 role_unassign_all(array('contextid' => $child->id)); 5561 } 5562 $status[] = array('component' => $componentstr, 'item' => get_string('deletelocalroles', 'role'), 'error' => false); 5563 } 5564 5565 // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc. 5566 $data->unenrolled = array(); 5567 if (!empty($data->unenrol_users)) { 5568 $plugins = enrol_get_plugins(true); 5569 $instances = enrol_get_instances($data->courseid, true); 5570 foreach ($instances as $key => $instance) { 5571 if (!isset($plugins[$instance->enrol])) { 5572 unset($instances[$key]); 5573 continue; 5574 } 5575 } 5576 5577 $usersroles = enrol_get_course_users_roles($data->courseid); 5578 foreach ($data->unenrol_users as $withroleid) { 5579 if ($withroleid) { 5580 $sql = "SELECT ue.* 5581 FROM {user_enrolments} ue 5582 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid) 5583 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid) 5584 JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)"; 5585 $params = array('courseid' => $data->courseid, 'roleid' => $withroleid, 'courselevel' => CONTEXT_COURSE); 5586 5587 } else { 5588 // Without any role assigned at course context. 5589 $sql = "SELECT ue.* 5590 FROM {user_enrolments} ue 5591 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid) 5592 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid) 5593 LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid) 5594 WHERE ra.id IS null"; 5595 $params = array('courseid' => $data->courseid, 'courselevel' => CONTEXT_COURSE); 5596 } 5597 5598 $rs = $DB->get_recordset_sql($sql, $params); 5599 foreach ($rs as $ue) { 5600 if (!isset($instances[$ue->enrolid])) { 5601 continue; 5602 } 5603 $instance = $instances[$ue->enrolid]; 5604 $plugin = $plugins[$instance->enrol]; 5605 if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) { 5606 continue; 5607 } 5608 5609 if ($withroleid && count($usersroles[$ue->userid]) > 1) { 5610 // If we don't remove all roles and user has more than one role, just remove this role. 5611 role_unassign($withroleid, $ue->userid, $context->id); 5612 5613 unset($usersroles[$ue->userid][$withroleid]); 5614 } else { 5615 // If we remove all roles or user has only one role, unenrol user from course. 5616 $plugin->unenrol_user($instance, $ue->userid); 5617 } 5618 $data->unenrolled[$ue->userid] = $ue->userid; 5619 } 5620 $rs->close(); 5621 } 5622 } 5623 if (!empty($data->unenrolled)) { 5624 $status[] = array( 5625 'component' => $componentstr, 5626 'item' => get_string('unenrol', 'enrol').' ('.count($data->unenrolled).')', 5627 'error' => false 5628 ); 5629 } 5630 5631 $componentstr = get_string('groups'); 5632 5633 // Remove all group members. 5634 if (!empty($data->reset_groups_members)) { 5635 groups_delete_group_members($data->courseid); 5636 $status[] = array('component' => $componentstr, 'item' => get_string('removegroupsmembers', 'group'), 'error' => false); 5637 } 5638 5639 // Remove all groups. 5640 if (!empty($data->reset_groups_remove)) { 5641 groups_delete_groups($data->courseid, false); 5642 $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroups', 'group'), 'error' => false); 5643 } 5644 5645 // Remove all grouping members. 5646 if (!empty($data->reset_groupings_members)) { 5647 groups_delete_groupings_groups($data->courseid, false); 5648 $status[] = array('component' => $componentstr, 'item' => get_string('removegroupingsmembers', 'group'), 'error' => false); 5649 } 5650 5651 // Remove all groupings. 5652 if (!empty($data->reset_groupings_remove)) { 5653 groups_delete_groupings($data->courseid, false); 5654 $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroupings', 'group'), 'error' => false); 5655 } 5656 5657 // Look in every instance of every module for data to delete. 5658 $unsupportedmods = array(); 5659 if ($allmods = $DB->get_records('modules') ) { 5660 foreach ($allmods as $mod) { 5661 $modname = $mod->name; 5662 $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php'; 5663 $moddeleteuserdata = $modname.'_reset_userdata'; // Function to delete user data. 5664 if (file_exists($modfile)) { 5665 if (!$DB->count_records($modname, array('course' => $data->courseid))) { 5666 continue; // Skip mods with no instances. 5667 } 5668 include_once($modfile); 5669 if (function_exists($moddeleteuserdata)) { 5670 $modstatus = $moddeleteuserdata($data); 5671 if (is_array($modstatus)) { 5672 $status = array_merge($status, $modstatus); 5673 } else { 5674 debugging('Module '.$modname.' returned incorrect staus - must be an array!'); 5675 } 5676 } else { 5677 $unsupportedmods[] = $mod; 5678 } 5679 } else { 5680 debugging('Missing lib.php in '.$modname.' module!'); 5681 } 5682 // Update calendar events for all modules. 5683 course_module_bulk_update_calendar_events($modname, $data->courseid); 5684 } 5685 // Purge the course cache after resetting course start date. MDL-76936 5686 if ($data->timeshift) { 5687 course_modinfo::purge_course_cache($data->courseid); 5688 } 5689 } 5690 5691 // Mention unsupported mods. 5692 if (!empty($unsupportedmods)) { 5693 foreach ($unsupportedmods as $mod) { 5694 $status[] = array( 5695 'component' => get_string('modulenameplural', $mod->name), 5696 'item' => '', 5697 'error' => get_string('resetnotimplemented') 5698 ); 5699 } 5700 } 5701 5702 $componentstr = get_string('gradebook', 'grades'); 5703 // Reset gradebook,. 5704 if (!empty($data->reset_gradebook_items)) { 5705 remove_course_grades($data->courseid, false); 5706 grade_grab_course_grades($data->courseid); 5707 grade_regrade_final_grades($data->courseid); 5708 $status[] = array('component' => $componentstr, 'item' => get_string('removeallcourseitems', 'grades'), 'error' => false); 5709 5710 } else if (!empty($data->reset_gradebook_grades)) { 5711 grade_course_reset($data->courseid); 5712 $status[] = array('component' => $componentstr, 'item' => get_string('removeallcoursegrades', 'grades'), 'error' => false); 5713 } 5714 // Reset comments. 5715 if (!empty($data->reset_comments)) { 5716 require_once($CFG->dirroot.'/comment/lib.php'); 5717 comment::reset_course_page_comments($context); 5718 } 5719 5720 $event = \core\event\course_reset_ended::create($eventparams); 5721 $event->trigger(); 5722 5723 return $status; 5724 } 5725 5726 /** 5727 * Generate an email processing address. 5728 * 5729 * @param int $modid 5730 * @param string $modargs 5731 * @return string Returns email processing address 5732 */ 5733 function generate_email_processing_address($modid, $modargs) { 5734 global $CFG; 5735 5736 $header = $CFG->mailprefix . substr(base64_encode(pack('C', $modid)), 0, 2).$modargs; 5737 return $header . substr(md5($header.get_site_identifier()), 0, 16).'@'.$CFG->maildomain; 5738 } 5739 5740 /** 5741 * ? 5742 * 5743 * @todo Finish documenting this function 5744 * 5745 * @param string $modargs 5746 * @param string $body Currently unused 5747 */ 5748 function moodle_process_email($modargs, $body) { 5749 global $DB; 5750 5751 // The first char should be an unencoded letter. We'll take this as an action. 5752 switch ($modargs[0]) { 5753 case 'B': { // Bounce. 5754 list(, $userid) = unpack('V', base64_decode(substr($modargs, 1, 8))); 5755 if ($user = $DB->get_record("user", array('id' => $userid), "id,email")) { 5756 // Check the half md5 of their email. 5757 $md5check = substr(md5($user->email), 0, 16); 5758 if ($md5check == substr($modargs, -16)) { 5759 set_bounce_count($user); 5760 } 5761 // Else maybe they've already changed it? 5762 } 5763 } 5764 break; 5765 // Maybe more later? 5766 } 5767 } 5768 5769 // CORRESPONDENCE. 5770 5771 /** 5772 * Get mailer instance, enable buffering, flush buffer or disable buffering. 5773 * 5774 * @param string $action 'get', 'buffer', 'close' or 'flush' 5775 * @return moodle_phpmailer|null mailer instance if 'get' used or nothing 5776 */ 5777 function get_mailer($action='get') { 5778 global $CFG; 5779 5780 /** @var moodle_phpmailer $mailer */ 5781 static $mailer = null; 5782 static $counter = 0; 5783 5784 if (!isset($CFG->smtpmaxbulk)) { 5785 $CFG->smtpmaxbulk = 1; 5786 } 5787 5788 if ($action == 'get') { 5789 $prevkeepalive = false; 5790 5791 if (isset($mailer) and $mailer->Mailer == 'smtp') { 5792 if ($counter < $CFG->smtpmaxbulk and !$mailer->isError()) { 5793 $counter++; 5794 // Reset the mailer. 5795 $mailer->Priority = 3; 5796 $mailer->CharSet = 'UTF-8'; // Our default. 5797 $mailer->ContentType = "text/plain"; 5798 $mailer->Encoding = "8bit"; 5799 $mailer->From = "root@localhost"; 5800 $mailer->FromName = "Root User"; 5801 $mailer->Sender = ""; 5802 $mailer->Subject = ""; 5803 $mailer->Body = ""; 5804 $mailer->AltBody = ""; 5805 $mailer->ConfirmReadingTo = ""; 5806 5807 $mailer->clearAllRecipients(); 5808 $mailer->clearReplyTos(); 5809 $mailer->clearAttachments(); 5810 $mailer->clearCustomHeaders(); 5811 return $mailer; 5812 } 5813 5814 $prevkeepalive = $mailer->SMTPKeepAlive; 5815 get_mailer('flush'); 5816 } 5817 5818 require_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php'); 5819 $mailer = new moodle_phpmailer(); 5820 5821 $counter = 1; 5822 5823 if ($CFG->smtphosts == 'qmail') { 5824 // Use Qmail system. 5825 $mailer->isQmail(); 5826 5827 } else if (empty($CFG->smtphosts)) { 5828 // Use PHP mail() = sendmail. 5829 $mailer->isMail(); 5830 5831 } else { 5832 // Use SMTP directly. 5833 $mailer->isSMTP(); 5834 if (!empty($CFG->debugsmtp) && (!empty($CFG->debugdeveloper))) { 5835 $mailer->SMTPDebug = 3; 5836 } 5837 // Specify main and backup servers. 5838 $mailer->Host = $CFG->smtphosts; 5839 // Specify secure connection protocol. 5840 $mailer->SMTPSecure = $CFG->smtpsecure; 5841 // Use previous keepalive. 5842 $mailer->SMTPKeepAlive = $prevkeepalive; 5843 5844 if ($CFG->smtpuser) { 5845 // Use SMTP authentication. 5846 $mailer->SMTPAuth = true; 5847 $mailer->Username = $CFG->smtpuser; 5848 $mailer->Password = $CFG->smtppass; 5849 } 5850 } 5851 5852 return $mailer; 5853 } 5854 5855 $nothing = null; 5856 5857 // Keep smtp session open after sending. 5858 if ($action == 'buffer') { 5859 if (!empty($CFG->smtpmaxbulk)) { 5860 get_mailer('flush'); 5861 $m = get_mailer(); 5862 if ($m->Mailer == 'smtp') { 5863 $m->SMTPKeepAlive = true; 5864 } 5865 } 5866 return $nothing; 5867 } 5868 5869 // Close smtp session, but continue buffering. 5870 if ($action == 'flush') { 5871 if (isset($mailer) and $mailer->Mailer == 'smtp') { 5872 if (!empty($mailer->SMTPDebug)) { 5873 echo '<pre>'."\n"; 5874 } 5875 $mailer->SmtpClose(); 5876 if (!empty($mailer->SMTPDebug)) { 5877 echo '</pre>'; 5878 } 5879 } 5880 return $nothing; 5881 } 5882 5883 // Close smtp session, do not buffer anymore. 5884 if ($action == 'close') { 5885 if (isset($mailer) and $mailer->Mailer == 'smtp') { 5886 get_mailer('flush'); 5887 $mailer->SMTPKeepAlive = false; 5888 } 5889 $mailer = null; // Better force new instance. 5890 return $nothing; 5891 } 5892 } 5893 5894 /** 5895 * A helper function to test for email diversion 5896 * 5897 * @param string $email 5898 * @return bool Returns true if the email should be diverted 5899 */ 5900 function email_should_be_diverted($email) { 5901 global $CFG; 5902 5903 if (empty($CFG->divertallemailsto)) { 5904 return false; 5905 } 5906 5907 if (empty($CFG->divertallemailsexcept)) { 5908 return true; 5909 } 5910 5911 $patterns = array_map('trim', preg_split("/[\s,]+/", $CFG->divertallemailsexcept, -1, PREG_SPLIT_NO_EMPTY)); 5912 foreach ($patterns as $pattern) { 5913 if (preg_match("/$pattern/", $email)) { 5914 return false; 5915 } 5916 } 5917 5918 return true; 5919 } 5920 5921 /** 5922 * Generate a unique email Message-ID using the moodle domain and install path 5923 * 5924 * @param string $localpart An optional unique message id prefix. 5925 * @return string The formatted ID ready for appending to the email headers. 5926 */ 5927 function generate_email_messageid($localpart = null) { 5928 global $CFG; 5929 5930 $urlinfo = parse_url($CFG->wwwroot); 5931 $base = '@' . $urlinfo['host']; 5932 5933 // If multiple moodles are on the same domain we want to tell them 5934 // apart so we add the install path to the local part. This means 5935 // that the id local part should never contain a / character so 5936 // we can correctly parse the id to reassemble the wwwroot. 5937 if (isset($urlinfo['path'])) { 5938 $base = $urlinfo['path'] . $base; 5939 } 5940 5941 if (empty($localpart)) { 5942 $localpart = uniqid('', true); 5943 } 5944 5945 // Because we may have an option /installpath suffix to the local part 5946 // of the id we need to escape any / chars which are in the $localpart. 5947 $localpart = str_replace('/', '%2F', $localpart); 5948 5949 return '<' . $localpart . $base . '>'; 5950 } 5951 5952 /** 5953 * Send an email to a specified user 5954 * 5955 * @param stdClass $user A {@link $USER} object 5956 * @param stdClass $from A {@link $USER} object 5957 * @param string $subject plain text subject line of the email 5958 * @param string $messagetext plain text version of the message 5959 * @param string $messagehtml complete html version of the message (optional) 5960 * @param string $attachment a file on the filesystem, either relative to $CFG->dataroot or a full path to a file in one of 5961 * the following directories: $CFG->cachedir, $CFG->dataroot, $CFG->dirroot, $CFG->localcachedir, $CFG->tempdir 5962 * @param string $attachname the name of the file (extension indicates MIME) 5963 * @param bool $usetrueaddress determines whether $from email address should 5964 * be sent out. Will be overruled by user profile setting for maildisplay 5965 * @param string $replyto Email address to reply to 5966 * @param string $replytoname Name of reply to recipient 5967 * @param int $wordwrapwidth custom word wrap width, default 79 5968 * @return bool Returns true if mail was sent OK and false if there was an error. 5969 */ 5970 function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '', $attachment = '', $attachname = '', 5971 $usetrueaddress = true, $replyto = '', $replytoname = '', $wordwrapwidth = 79) { 5972 5973 global $CFG, $PAGE, $SITE; 5974 5975 if (empty($user) or empty($user->id)) { 5976 debugging('Can not send email to null user', DEBUG_DEVELOPER); 5977 return false; 5978 } 5979 5980 if (empty($user->email)) { 5981 debugging('Can not send email to user without email: '.$user->id, DEBUG_DEVELOPER); 5982 return false; 5983 } 5984 5985 if (!empty($user->deleted)) { 5986 debugging('Can not send email to deleted user: '.$user->id, DEBUG_DEVELOPER); 5987 return false; 5988 } 5989 5990 if (defined('BEHAT_SITE_RUNNING')) { 5991 // Fake email sending in behat. 5992 return true; 5993 } 5994 5995 if (!empty($CFG->noemailever)) { 5996 // Hidden setting for development sites, set in config.php if needed. 5997 debugging('Not sending email due to $CFG->noemailever config setting', DEBUG_NORMAL); 5998 return true; 5999 } 6000 6001 if (email_should_be_diverted($user->email)) { 6002 $subject = "[DIVERTED {$user->email}] $subject"; 6003 $user = clone($user); 6004 $user->email = $CFG->divertallemailsto; 6005 } 6006 6007 // Skip mail to suspended users. 6008 if ((isset($user->auth) && $user->auth=='nologin') or (isset($user->suspended) && $user->suspended)) { 6009 return true; 6010 } 6011 6012 if (!validate_email($user->email)) { 6013 // We can not send emails to invalid addresses - it might create security issue or confuse the mailer. 6014 debugging("email_to_user: User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending."); 6015 return false; 6016 } 6017 6018 if (over_bounce_threshold($user)) { 6019 debugging("email_to_user: User $user->id (".fullname($user).") is over bounce threshold! Not sending."); 6020 return false; 6021 } 6022 6023 // TLD .invalid is specifically reserved for invalid domain names. 6024 // For More information, see {@link http://tools.ietf.org/html/rfc2606#section-2}. 6025 if (substr($user->email, -8) == '.invalid') { 6026 debugging("email_to_user: User $user->id (".fullname($user).") email domain ($user->email) is invalid! Not sending."); 6027 return true; // This is not an error. 6028 } 6029 6030 // If the user is a remote mnet user, parse the email text for URL to the 6031 // wwwroot and modify the url to direct the user's browser to login at their 6032 // home site (identity provider - idp) before hitting the link itself. 6033 if (is_mnet_remote_user($user)) { 6034 require_once($CFG->dirroot.'/mnet/lib.php'); 6035 6036 $jumpurl = mnet_get_idp_jump_url($user); 6037 $callback = partial('mnet_sso_apply_indirection', $jumpurl); 6038 6039 $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%", 6040 $callback, 6041 $messagetext); 6042 $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%", 6043 $callback, 6044 $messagehtml); 6045 } 6046 $mail = get_mailer(); 6047 6048 if (!empty($mail->SMTPDebug)) { 6049 echo '<pre>' . "\n"; 6050 } 6051 6052 $temprecipients = array(); 6053 $tempreplyto = array(); 6054 6055 // Make sure that we fall back onto some reasonable no-reply address. 6056 $noreplyaddressdefault = 'noreply@' . get_host_from_url($CFG->wwwroot); 6057 $noreplyaddress = empty($CFG->noreplyaddress) ? $noreplyaddressdefault : $CFG->noreplyaddress; 6058 6059 if (!validate_email($noreplyaddress)) { 6060 debugging('email_to_user: Invalid noreply-email '.s($noreplyaddress)); 6061 $noreplyaddress = $noreplyaddressdefault; 6062 } 6063 6064 // Make up an email address for handling bounces. 6065 if (!empty($CFG->handlebounces)) { 6066 $modargs = 'B'.base64_encode(pack('V', $user->id)).substr(md5($user->email), 0, 16); 6067 $mail->Sender = generate_email_processing_address(0, $modargs); 6068 } else { 6069 $mail->Sender = $noreplyaddress; 6070 } 6071 6072 // Make sure that the explicit replyto is valid, fall back to the implicit one. 6073 if (!empty($replyto) && !validate_email($replyto)) { 6074 debugging('email_to_user: Invalid replyto-email '.s($replyto)); 6075 $replyto = $noreplyaddress; 6076 } 6077 6078 if (is_string($from)) { // So we can pass whatever we want if there is need. 6079 $mail->From = $noreplyaddress; 6080 $mail->FromName = $from; 6081 // Check if using the true address is true, and the email is in the list of allowed domains for sending email, 6082 // and that the senders email setting is either displayed to everyone, or display to only other users that are enrolled 6083 // in a course with the sender. 6084 } else if ($usetrueaddress && can_send_from_real_email_address($from, $user)) { 6085 if (!validate_email($from->email)) { 6086 debugging('email_to_user: Invalid from-email '.s($from->email).' - not sending'); 6087 // Better not to use $noreplyaddress in this case. 6088 return false; 6089 } 6090 $mail->From = $from->email; 6091 $fromdetails = new stdClass(); 6092 $fromdetails->name = fullname($from); 6093 $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot); 6094 $fromdetails->siteshortname = format_string($SITE->shortname); 6095 $fromstring = $fromdetails->name; 6096 if ($CFG->emailfromvia == EMAIL_VIA_ALWAYS) { 6097 $fromstring = get_string('emailvia', 'core', $fromdetails); 6098 } 6099 $mail->FromName = $fromstring; 6100 if (empty($replyto)) { 6101 $tempreplyto[] = array($from->email, fullname($from)); 6102 } 6103 } else { 6104 $mail->From = $noreplyaddress; 6105 $fromdetails = new stdClass(); 6106 $fromdetails->name = fullname($from); 6107 $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot); 6108 $fromdetails->siteshortname = format_string($SITE->shortname); 6109 $fromstring = $fromdetails->name; 6110 if ($CFG->emailfromvia != EMAIL_VIA_NEVER) { 6111 $fromstring = get_string('emailvia', 'core', $fromdetails); 6112 } 6113 $mail->FromName = $fromstring; 6114 if (empty($replyto)) { 6115 $tempreplyto[] = array($noreplyaddress, get_string('noreplyname')); 6116 } 6117 } 6118 6119 if (!empty($replyto)) { 6120 $tempreplyto[] = array($replyto, $replytoname); 6121 } 6122 6123 $temprecipients[] = array($user->email, fullname($user)); 6124 6125 // Set word wrap. 6126 $mail->WordWrap = $wordwrapwidth; 6127 6128 if (!empty($from->customheaders)) { 6129 // Add custom headers. 6130 if (is_array($from->customheaders)) { 6131 foreach ($from->customheaders as $customheader) { 6132 $mail->addCustomHeader($customheader); 6133 } 6134 } else { 6135 $mail->addCustomHeader($from->customheaders); 6136 } 6137 } 6138 6139 // If the X-PHP-Originating-Script email header is on then also add an additional 6140 // header with details of where exactly in moodle the email was triggered from, 6141 // either a call to message_send() or to email_to_user(). 6142 if (ini_get('mail.add_x_header')) { 6143 6144 $stack = debug_backtrace(false); 6145 $origin = $stack[0]; 6146 6147 foreach ($stack as $depth => $call) { 6148 if ($call['function'] == 'message_send') { 6149 $origin = $call; 6150 } 6151 } 6152 6153 $originheader = $CFG->wwwroot . ' => ' . gethostname() . ':' 6154 . str_replace($CFG->dirroot . '/', '', $origin['file']) . ':' . $origin['line']; 6155 $mail->addCustomHeader('X-Moodle-Originating-Script: ' . $originheader); 6156 } 6157 6158 if (!empty($CFG->emailheaders)) { 6159 $headers = array_map('trim', explode("\n", $CFG->emailheaders)); 6160 foreach ($headers as $header) { 6161 if (!empty($header)) { 6162 $mail->addCustomHeader($header); 6163 } 6164 } 6165 } 6166 6167 if (!empty($from->priority)) { 6168 $mail->Priority = $from->priority; 6169 } 6170 6171 $renderer = $PAGE->get_renderer('core'); 6172 $context = array( 6173 'sitefullname' => $SITE->fullname, 6174 'siteshortname' => $SITE->shortname, 6175 'sitewwwroot' => $CFG->wwwroot, 6176 'subject' => $subject, 6177 'prefix' => $CFG->emailsubjectprefix, 6178 'to' => $user->email, 6179 'toname' => fullname($user), 6180 'from' => $mail->From, 6181 'fromname' => $mail->FromName, 6182 ); 6183 if (!empty($tempreplyto[0])) { 6184 $context['replyto'] = $tempreplyto[0][0]; 6185 $context['replytoname'] = $tempreplyto[0][1]; 6186 } 6187 if ($user->id > 0) { 6188 $context['touserid'] = $user->id; 6189 $context['tousername'] = $user->username; 6190 } 6191 6192 if (!empty($user->mailformat) && $user->mailformat == 1) { 6193 // Only process html templates if the user preferences allow html email. 6194 6195 if (!$messagehtml) { 6196 // If no html has been given, BUT there is an html wrapping template then 6197 // auto convert the text to html and then wrap it. 6198 $messagehtml = trim(text_to_html($messagetext)); 6199 } 6200 $context['body'] = $messagehtml; 6201 $messagehtml = $renderer->render_from_template('core/email_html', $context); 6202 } 6203 6204 $context['body'] = html_to_text(nl2br($messagetext)); 6205 $mail->Subject = $renderer->render_from_template('core/email_subject', $context); 6206 $mail->FromName = $renderer->render_from_template('core/email_fromname', $context); 6207 $messagetext = $renderer->render_from_template('core/email_text', $context); 6208 6209 // Autogenerate a MessageID if it's missing. 6210 if (empty($mail->MessageID)) { 6211 $mail->MessageID = generate_email_messageid(); 6212 } 6213 6214 if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) { 6215 // Don't ever send HTML to users who don't want it. 6216 $mail->isHTML(true); 6217 $mail->Encoding = 'quoted-printable'; 6218 $mail->Body = $messagehtml; 6219 $mail->AltBody = "\n$messagetext\n"; 6220 } else { 6221 $mail->IsHTML(false); 6222 $mail->Body = "\n$messagetext\n"; 6223 } 6224 6225 if ($attachment && $attachname) { 6226 if (preg_match( "~\\.\\.~" , $attachment )) { 6227 // Security check for ".." in dir path. 6228 $supportuser = core_user::get_support_user(); 6229 $temprecipients[] = array($supportuser->email, fullname($supportuser, true)); 6230 $mail->addStringAttachment('Error in attachment. User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain'); 6231 } else { 6232 require_once($CFG->libdir.'/filelib.php'); 6233 $mimetype = mimeinfo('type', $attachname); 6234 6235 // Before doing the comparison, make sure that the paths are correct (Windows uses slashes in the other direction). 6236 // The absolute (real) path is also fetched to ensure that comparisons to allowed paths are compared equally. 6237 $attachpath = str_replace('\\', '/', realpath($attachment)); 6238 6239 // Build an array of all filepaths from which attachments can be added (normalised slashes, absolute/real path). 6240 $allowedpaths = array_map(function(string $path): string { 6241 return str_replace('\\', '/', realpath($path)); 6242 }, [ 6243 $CFG->cachedir, 6244 $CFG->dataroot, 6245 $CFG->dirroot, 6246 $CFG->localcachedir, 6247 $CFG->tempdir, 6248 $CFG->localrequestdir, 6249 ]); 6250 6251 // Set addpath to true. 6252 $addpath = true; 6253 6254 // Check if attachment includes one of the allowed paths. 6255 foreach (array_filter($allowedpaths) as $allowedpath) { 6256 // Set addpath to false if the attachment includes one of the allowed paths. 6257 if (strpos($attachpath, $allowedpath) === 0) { 6258 $addpath = false; 6259 break; 6260 } 6261 } 6262 6263 // If the attachment is a full path to a file in the multiple allowed paths, use it as is, 6264 // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons). 6265 if ($addpath == true) { 6266 $attachment = $CFG->dataroot . '/' . $attachment; 6267 } 6268 6269 $mail->addAttachment($attachment, $attachname, 'base64', $mimetype); 6270 } 6271 } 6272 6273 // Check if the email should be sent in an other charset then the default UTF-8. 6274 if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) { 6275 6276 // Use the defined site mail charset or eventually the one preferred by the recipient. 6277 $charset = $CFG->sitemailcharset; 6278 if (!empty($CFG->allowusermailcharset)) { 6279 if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) { 6280 $charset = $useremailcharset; 6281 } 6282 } 6283 6284 // Convert all the necessary strings if the charset is supported. 6285 $charsets = get_list_of_charsets(); 6286 unset($charsets['UTF-8']); 6287 if (in_array($charset, $charsets)) { 6288 $mail->CharSet = $charset; 6289 $mail->FromName = core_text::convert($mail->FromName, 'utf-8', strtolower($charset)); 6290 $mail->Subject = core_text::convert($mail->Subject, 'utf-8', strtolower($charset)); 6291 $mail->Body = core_text::convert($mail->Body, 'utf-8', strtolower($charset)); 6292 $mail->AltBody = core_text::convert($mail->AltBody, 'utf-8', strtolower($charset)); 6293 6294 foreach ($temprecipients as $key => $values) { 6295 $temprecipients[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset)); 6296 } 6297 foreach ($tempreplyto as $key => $values) { 6298 $tempreplyto[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset)); 6299 } 6300 } 6301 } 6302 6303 foreach ($temprecipients as $values) { 6304 $mail->addAddress($values[0], $values[1]); 6305 } 6306 foreach ($tempreplyto as $values) { 6307 $mail->addReplyTo($values[0], $values[1]); 6308 } 6309 6310 if (!empty($CFG->emaildkimselector)) { 6311 $domain = substr(strrchr($mail->From, "@"), 1); 6312 $pempath = "{$CFG->dataroot}/dkim/{$domain}/{$CFG->emaildkimselector}.private"; 6313 if (file_exists($pempath)) { 6314 $mail->DKIM_domain = $domain; 6315 $mail->DKIM_private = $pempath; 6316 $mail->DKIM_selector = $CFG->emaildkimselector; 6317 $mail->DKIM_identity = $mail->From; 6318 } else { 6319 debugging("Email DKIM selector chosen due to {$mail->From} but no certificate found at $pempath", DEBUG_DEVELOPER); 6320 } 6321 } 6322 6323 if ($mail->send()) { 6324 set_send_count($user); 6325 if (!empty($mail->SMTPDebug)) { 6326 echo '</pre>'; 6327 } 6328 return true; 6329 } else { 6330 // Trigger event for failing to send email. 6331 $event = \core\event\email_failed::create(array( 6332 'context' => context_system::instance(), 6333 'userid' => $from->id, 6334 'relateduserid' => $user->id, 6335 'other' => array( 6336 'subject' => $subject, 6337 'message' => $messagetext, 6338 'errorinfo' => $mail->ErrorInfo 6339 ) 6340 )); 6341 $event->trigger(); 6342 if (CLI_SCRIPT) { 6343 mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo); 6344 } 6345 if (!empty($mail->SMTPDebug)) { 6346 echo '</pre>'; 6347 } 6348 return false; 6349 } 6350 } 6351 6352 /** 6353 * Check to see if a user's real email address should be used for the "From" field. 6354 * 6355 * @param object $from The user object for the user we are sending the email from. 6356 * @param object $user The user object that we are sending the email to. 6357 * @param array $unused No longer used. 6358 * @return bool Returns true if we can use the from user's email adress in the "From" field. 6359 */ 6360 function can_send_from_real_email_address($from, $user, $unused = null) { 6361 global $CFG; 6362 if (!isset($CFG->allowedemaildomains) || empty(trim($CFG->allowedemaildomains))) { 6363 return false; 6364 } 6365 $alloweddomains = array_map('trim', explode("\n", $CFG->allowedemaildomains)); 6366 // Email is in the list of allowed domains for sending email, 6367 // and the senders email setting is either displayed to everyone, or display to only other users that are enrolled 6368 // in a course with the sender. 6369 if (\core\ip_utils::is_domain_in_allowed_list(substr($from->email, strpos($from->email, '@') + 1), $alloweddomains) 6370 && ($from->maildisplay == core_user::MAILDISPLAY_EVERYONE 6371 || ($from->maildisplay == core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY 6372 && enrol_get_shared_courses($user, $from, false, true)))) { 6373 return true; 6374 } 6375 return false; 6376 } 6377 6378 /** 6379 * Generate a signoff for emails based on support settings 6380 * 6381 * @return string 6382 */ 6383 function generate_email_signoff() { 6384 global $CFG, $OUTPUT; 6385 6386 $signoff = "\n"; 6387 if (!empty($CFG->supportname)) { 6388 $signoff .= $CFG->supportname."\n"; 6389 } 6390 6391 $supportemail = $OUTPUT->supportemail(['class' => 'font-weight-bold']); 6392 6393 if ($supportemail) { 6394 $signoff .= "\n" . $supportemail . "\n"; 6395 } 6396 6397 return $signoff; 6398 } 6399 6400 /** 6401 * Sets specified user's password and send the new password to the user via email. 6402 * 6403 * @param stdClass $user A {@link $USER} object 6404 * @param bool $fasthash If true, use a low cost factor when generating the hash for speed. 6405 * @return bool|string Returns "true" if mail was sent OK and "false" if there was an error 6406 */ 6407 function setnew_password_and_mail($user, $fasthash = false) { 6408 global $CFG, $DB; 6409 6410 // We try to send the mail in language the user understands, 6411 // unfortunately the filter_string() does not support alternative langs yet 6412 // so multilang will not work properly for site->fullname. 6413 $lang = empty($user->lang) ? get_newuser_language() : $user->lang; 6414 6415 $site = get_site(); 6416 6417 $supportuser = core_user::get_support_user(); 6418 6419 $newpassword = generate_password(); 6420 6421 update_internal_user_password($user, $newpassword, $fasthash); 6422 6423 $a = new stdClass(); 6424 $a->firstname = fullname($user, true); 6425 $a->sitename = format_string($site->fullname); 6426 $a->username = $user->username; 6427 $a->newpassword = $newpassword; 6428 $a->link = $CFG->wwwroot .'/login/?lang='.$lang; 6429 $a->signoff = generate_email_signoff(); 6430 6431 $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang); 6432 6433 $subject = format_string($site->fullname) .': '. (string)new lang_string('newusernewpasswordsubj', '', $a, $lang); 6434 6435 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 6436 return email_to_user($user, $supportuser, $subject, $message); 6437 6438 } 6439 6440 /** 6441 * Resets specified user's password and send the new password to the user via email. 6442 * 6443 * @param stdClass $user A {@link $USER} object 6444 * @return bool Returns true if mail was sent OK and false if there was an error. 6445 */ 6446 function reset_password_and_mail($user) { 6447 global $CFG; 6448 6449 $site = get_site(); 6450 $supportuser = core_user::get_support_user(); 6451 6452 $userauth = get_auth_plugin($user->auth); 6453 if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) { 6454 trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth."); 6455 return false; 6456 } 6457 6458 $newpassword = generate_password(); 6459 6460 if (!$userauth->user_update_password($user, $newpassword)) { 6461 throw new \moodle_exception("cannotsetpassword"); 6462 } 6463 6464 $a = new stdClass(); 6465 $a->firstname = $user->firstname; 6466 $a->lastname = $user->lastname; 6467 $a->sitename = format_string($site->fullname); 6468 $a->username = $user->username; 6469 $a->newpassword = $newpassword; 6470 $a->link = $CFG->wwwroot .'/login/change_password.php'; 6471 $a->signoff = generate_email_signoff(); 6472 6473 $message = get_string('newpasswordtext', '', $a); 6474 6475 $subject = format_string($site->fullname) .': '. get_string('changedpassword'); 6476 6477 unset_user_preference('create_password', $user); // Prevent cron from generating the password. 6478 6479 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 6480 return email_to_user($user, $supportuser, $subject, $message); 6481 } 6482 6483 /** 6484 * Send email to specified user with confirmation text and activation link. 6485 * 6486 * @param stdClass $user A {@link $USER} object 6487 * @param string $confirmationurl user confirmation URL 6488 * @return bool Returns true if mail was sent OK and false if there was an error. 6489 */ 6490 function send_confirmation_email($user, $confirmationurl = null) { 6491 global $CFG; 6492 6493 $site = get_site(); 6494 $supportuser = core_user::get_support_user(); 6495 6496 $data = new stdClass(); 6497 $data->sitename = format_string($site->fullname); 6498 $data->admin = generate_email_signoff(); 6499 6500 $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname)); 6501 6502 if (empty($confirmationurl)) { 6503 $confirmationurl = '/login/confirm.php'; 6504 } 6505 6506 $confirmationurl = new moodle_url($confirmationurl); 6507 // Remove data parameter just in case it was included in the confirmation so we can add it manually later. 6508 $confirmationurl->remove_params('data'); 6509 $confirmationpath = $confirmationurl->out(false); 6510 6511 // We need to custom encode the username to include trailing dots in the link. 6512 // Because of this custom encoding we can't use moodle_url directly. 6513 // Determine if a query string is present in the confirmation url. 6514 $hasquerystring = strpos($confirmationpath, '?') !== false; 6515 // Perform normal url encoding of the username first. 6516 $username = urlencode($user->username); 6517 // Prevent problems with trailing dots not being included as part of link in some mail clients. 6518 $username = str_replace('.', '%2E', $username); 6519 6520 $data->link = $confirmationpath . ( $hasquerystring ? '&' : '?') . 'data='. $user->secret .'/'. $username; 6521 6522 $message = get_string('emailconfirmation', '', $data); 6523 $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true); 6524 6525 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 6526 return email_to_user($user, $supportuser, $subject, $message, $messagehtml); 6527 } 6528 6529 /** 6530 * Sends a password change confirmation email. 6531 * 6532 * @param stdClass $user A {@link $USER} object 6533 * @param stdClass $resetrecord An object tracking metadata regarding password reset request 6534 * @return bool Returns true if mail was sent OK and false if there was an error. 6535 */ 6536 function send_password_change_confirmation_email($user, $resetrecord) { 6537 global $CFG; 6538 6539 $site = get_site(); 6540 $supportuser = core_user::get_support_user(); 6541 $pwresetmins = isset($CFG->pwresettime) ? floor($CFG->pwresettime / MINSECS) : 30; 6542 6543 $data = new stdClass(); 6544 $data->firstname = $user->firstname; 6545 $data->lastname = $user->lastname; 6546 $data->username = $user->username; 6547 $data->sitename = format_string($site->fullname); 6548 $data->link = $CFG->wwwroot .'/login/forgot_password.php?token='. $resetrecord->token; 6549 $data->admin = generate_email_signoff(); 6550 $data->resetminutes = $pwresetmins; 6551 6552 $message = get_string('emailresetconfirmation', '', $data); 6553 $subject = get_string('emailresetconfirmationsubject', '', format_string($site->fullname)); 6554 6555 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 6556 return email_to_user($user, $supportuser, $subject, $message); 6557 6558 } 6559 6560 /** 6561 * Sends an email containing information on how to change your password. 6562 * 6563 * @param stdClass $user A {@link $USER} object 6564 * @return bool Returns true if mail was sent OK and false if there was an error. 6565 */ 6566 function send_password_change_info($user) { 6567 $site = get_site(); 6568 $supportuser = core_user::get_support_user(); 6569 6570 $data = new stdClass(); 6571 $data->firstname = $user->firstname; 6572 $data->lastname = $user->lastname; 6573 $data->username = $user->username; 6574 $data->sitename = format_string($site->fullname); 6575 $data->admin = generate_email_signoff(); 6576 6577 if (!is_enabled_auth($user->auth)) { 6578 $message = get_string('emailpasswordchangeinfodisabled', '', $data); 6579 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname)); 6580 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 6581 return email_to_user($user, $supportuser, $subject, $message); 6582 } 6583 6584 $userauth = get_auth_plugin($user->auth); 6585 ['subject' => $subject, 'message' => $message] = $userauth->get_password_change_info($user); 6586 6587 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 6588 return email_to_user($user, $supportuser, $subject, $message); 6589 } 6590 6591 /** 6592 * Check that an email is allowed. It returns an error message if there was a problem. 6593 * 6594 * @param string $email Content of email 6595 * @return string|false 6596 */ 6597 function email_is_not_allowed($email) { 6598 global $CFG; 6599 6600 // Comparing lowercase domains. 6601 $email = strtolower($email); 6602 if (!empty($CFG->allowemailaddresses)) { 6603 $allowed = explode(' ', strtolower($CFG->allowemailaddresses)); 6604 foreach ($allowed as $allowedpattern) { 6605 $allowedpattern = trim($allowedpattern); 6606 if (!$allowedpattern) { 6607 continue; 6608 } 6609 if (strpos($allowedpattern, '.') === 0) { 6610 if (strpos(strrev($email), strrev($allowedpattern)) === 0) { 6611 // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com". 6612 return false; 6613 } 6614 6615 } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) { 6616 return false; 6617 } 6618 } 6619 return get_string('emailonlyallowed', '', $CFG->allowemailaddresses); 6620 6621 } else if (!empty($CFG->denyemailaddresses)) { 6622 $denied = explode(' ', strtolower($CFG->denyemailaddresses)); 6623 foreach ($denied as $deniedpattern) { 6624 $deniedpattern = trim($deniedpattern); 6625 if (!$deniedpattern) { 6626 continue; 6627 } 6628 if (strpos($deniedpattern, '.') === 0) { 6629 if (strpos(strrev($email), strrev($deniedpattern)) === 0) { 6630 // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com". 6631 return get_string('emailnotallowed', '', $CFG->denyemailaddresses); 6632 } 6633 6634 } else if (strpos(strrev($email), strrev('@'.$deniedpattern)) === 0) { 6635 return get_string('emailnotallowed', '', $CFG->denyemailaddresses); 6636 } 6637 } 6638 } 6639 6640 return false; 6641 } 6642 6643 // FILE HANDLING. 6644 6645 /** 6646 * Returns local file storage instance 6647 * 6648 * @return file_storage 6649 */ 6650 function get_file_storage($reset = false) { 6651 global $CFG; 6652 6653 static $fs = null; 6654 6655 if ($reset) { 6656 $fs = null; 6657 return; 6658 } 6659 6660 if ($fs) { 6661 return $fs; 6662 } 6663 6664 require_once("$CFG->libdir/filelib.php"); 6665 6666 $fs = new file_storage(); 6667 6668 return $fs; 6669 } 6670 6671 /** 6672 * Returns local file storage instance 6673 * 6674 * @return file_browser 6675 */ 6676 function get_file_browser() { 6677 global $CFG; 6678 6679 static $fb = null; 6680 6681 if ($fb) { 6682 return $fb; 6683 } 6684 6685 require_once("$CFG->libdir/filelib.php"); 6686 6687 $fb = new file_browser(); 6688 6689 return $fb; 6690 } 6691 6692 /** 6693 * Returns file packer 6694 * 6695 * @param string $mimetype default application/zip 6696 * @return file_packer 6697 */ 6698 function get_file_packer($mimetype='application/zip') { 6699 global $CFG; 6700 6701 static $fp = array(); 6702 6703 if (isset($fp[$mimetype])) { 6704 return $fp[$mimetype]; 6705 } 6706 6707 switch ($mimetype) { 6708 case 'application/zip': 6709 case 'application/vnd.moodle.profiling': 6710 $classname = 'zip_packer'; 6711 break; 6712 6713 case 'application/x-gzip' : 6714 $classname = 'tgz_packer'; 6715 break; 6716 6717 case 'application/vnd.moodle.backup': 6718 $classname = 'mbz_packer'; 6719 break; 6720 6721 default: 6722 return false; 6723 } 6724 6725 require_once("$CFG->libdir/filestorage/$classname.php"); 6726 $fp[$mimetype] = new $classname(); 6727 6728 return $fp[$mimetype]; 6729 } 6730 6731 /** 6732 * Returns current name of file on disk if it exists. 6733 * 6734 * @param string $newfile File to be verified 6735 * @return string Current name of file on disk if true 6736 */ 6737 function valid_uploaded_file($newfile) { 6738 if (empty($newfile)) { 6739 return ''; 6740 } 6741 if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) { 6742 return $newfile['tmp_name']; 6743 } else { 6744 return ''; 6745 } 6746 } 6747 6748 /** 6749 * Returns the maximum size for uploading files. 6750 * 6751 * There are seven possible upload limits: 6752 * 1. in Apache using LimitRequestBody (no way of checking or changing this) 6753 * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP) 6754 * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP) 6755 * 4. in php.ini for 'post_max_size' (can not be changed inside PHP) 6756 * 5. by the Moodle admin in $CFG->maxbytes 6757 * 6. by the teacher in the current course $course->maxbytes 6758 * 7. by the teacher for the current module, eg $assignment->maxbytes 6759 * 6760 * These last two are passed to this function as arguments (in bytes). 6761 * Anything defined as 0 is ignored. 6762 * The smallest of all the non-zero numbers is returned. 6763 * 6764 * @todo Finish documenting this function 6765 * 6766 * @param int $sitebytes Set maximum size 6767 * @param int $coursebytes Current course $course->maxbytes (in bytes) 6768 * @param int $modulebytes Current module ->maxbytes (in bytes) 6769 * @param bool $unused This parameter has been deprecated and is not used any more. 6770 * @return int The maximum size for uploading files. 6771 */ 6772 function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0, $unused = false) { 6773 6774 if (! $filesize = ini_get('upload_max_filesize')) { 6775 $filesize = '5M'; 6776 } 6777 $minimumsize = get_real_size($filesize); 6778 6779 if ($postsize = ini_get('post_max_size')) { 6780 $postsize = get_real_size($postsize); 6781 if ($postsize < $minimumsize) { 6782 $minimumsize = $postsize; 6783 } 6784 } 6785 6786 if (($sitebytes > 0) and ($sitebytes < $minimumsize)) { 6787 $minimumsize = $sitebytes; 6788 } 6789 6790 if (($coursebytes > 0) and ($coursebytes < $minimumsize)) { 6791 $minimumsize = $coursebytes; 6792 } 6793 6794 if (($modulebytes > 0) and ($modulebytes < $minimumsize)) { 6795 $minimumsize = $modulebytes; 6796 } 6797 6798 return $minimumsize; 6799 } 6800 6801 /** 6802 * Returns the maximum size for uploading files for the current user 6803 * 6804 * This function takes in account {@link get_max_upload_file_size()} the user's capabilities 6805 * 6806 * @param context $context The context in which to check user capabilities 6807 * @param int $sitebytes Set maximum size 6808 * @param int $coursebytes Current course $course->maxbytes (in bytes) 6809 * @param int $modulebytes Current module ->maxbytes (in bytes) 6810 * @param stdClass $user The user 6811 * @param bool $unused This parameter has been deprecated and is not used any more. 6812 * @return int The maximum size for uploading files. 6813 */ 6814 function get_user_max_upload_file_size($context, $sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $user = null, 6815 $unused = false) { 6816 global $USER; 6817 6818 if (empty($user)) { 6819 $user = $USER; 6820 } 6821 6822 if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) { 6823 return USER_CAN_IGNORE_FILE_SIZE_LIMITS; 6824 } 6825 6826 return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes); 6827 } 6828 6829 /** 6830 * Returns an array of possible sizes in local language 6831 * 6832 * Related to {@link get_max_upload_file_size()} - this function returns an 6833 * array of possible sizes in an array, translated to the 6834 * local language. 6835 * 6836 * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes. 6837 * 6838 * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)" 6839 * with the value set to 0. This option will be the first in the list. 6840 * 6841 * @uses SORT_NUMERIC 6842 * @param int $sitebytes Set maximum size 6843 * @param int $coursebytes Current course $course->maxbytes (in bytes) 6844 * @param int $modulebytes Current module ->maxbytes (in bytes) 6845 * @param int|array $custombytes custom upload size/s which will be added to list, 6846 * Only value/s smaller then maxsize will be added to list. 6847 * @return array 6848 */ 6849 function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null) { 6850 global $CFG; 6851 6852 if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) { 6853 return array(); 6854 } 6855 6856 if ($sitebytes == 0) { 6857 // Will get the minimum of upload_max_filesize or post_max_size. 6858 $sitebytes = get_max_upload_file_size(); 6859 } 6860 6861 $filesize = array(); 6862 $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152, 6863 5242880, 10485760, 20971520, 52428800, 104857600, 6864 262144000, 524288000, 786432000, 1073741824, 6865 2147483648, 4294967296, 8589934592); 6866 6867 // If custombytes is given and is valid then add it to the list. 6868 if (is_number($custombytes) and $custombytes > 0) { 6869 $custombytes = (int)$custombytes; 6870 if (!in_array($custombytes, $sizelist)) { 6871 $sizelist[] = $custombytes; 6872 } 6873 } else if (is_array($custombytes)) { 6874 $sizelist = array_unique(array_merge($sizelist, $custombytes)); 6875 } 6876 6877 // Allow maxbytes to be selected if it falls outside the above boundaries. 6878 if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) { 6879 // Note: get_real_size() is used in order to prevent problems with invalid values. 6880 $sizelist[] = get_real_size($CFG->maxbytes); 6881 } 6882 6883 foreach ($sizelist as $sizebytes) { 6884 if ($sizebytes < $maxsize && $sizebytes > 0) { 6885 $filesize[(string)intval($sizebytes)] = display_size($sizebytes, 0); 6886 } 6887 } 6888 6889 $limitlevel = ''; 6890 $displaysize = ''; 6891 if ($modulebytes && 6892 (($modulebytes < $coursebytes || $coursebytes == 0) && 6893 ($modulebytes < $sitebytes || $sitebytes == 0))) { 6894 $limitlevel = get_string('activity', 'core'); 6895 $displaysize = display_size($modulebytes, 0); 6896 $filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list. 6897 6898 } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) { 6899 $limitlevel = get_string('course', 'core'); 6900 $displaysize = display_size($coursebytes, 0); 6901 $filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list. 6902 6903 } else if ($sitebytes) { 6904 $limitlevel = get_string('site', 'core'); 6905 $displaysize = display_size($sitebytes, 0); 6906 $filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list. 6907 } 6908 6909 krsort($filesize, SORT_NUMERIC); 6910 if ($limitlevel) { 6911 $params = (object) array('contextname' => $limitlevel, 'displaysize' => $displaysize); 6912 $filesize = array('0' => get_string('uploadlimitwithsize', 'core', $params)) + $filesize; 6913 } 6914 6915 return $filesize; 6916 } 6917 6918 /** 6919 * Returns an array with all the filenames in all subdirectories, relative to the given rootdir. 6920 * 6921 * If excludefiles is defined, then that file/directory is ignored 6922 * If getdirs is true, then (sub)directories are included in the output 6923 * If getfiles is true, then files are included in the output 6924 * (at least one of these must be true!) 6925 * 6926 * @todo Finish documenting this function. Add examples of $excludefile usage. 6927 * 6928 * @param string $rootdir A given root directory to start from 6929 * @param string|array $excludefiles If defined then the specified file/directory is ignored 6930 * @param bool $descend If true then subdirectories are recursed as well 6931 * @param bool $getdirs If true then (sub)directories are included in the output 6932 * @param bool $getfiles If true then files are included in the output 6933 * @return array An array with all the filenames in all subdirectories, relative to the given rootdir 6934 */ 6935 function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) { 6936 6937 $dirs = array(); 6938 6939 if (!$getdirs and !$getfiles) { // Nothing to show. 6940 return $dirs; 6941 } 6942 6943 if (!is_dir($rootdir)) { // Must be a directory. 6944 return $dirs; 6945 } 6946 6947 if (!$dir = opendir($rootdir)) { // Can't open it for some reason. 6948 return $dirs; 6949 } 6950 6951 if (!is_array($excludefiles)) { 6952 $excludefiles = array($excludefiles); 6953 } 6954 6955 while (false !== ($file = readdir($dir))) { 6956 $firstchar = substr($file, 0, 1); 6957 if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) { 6958 continue; 6959 } 6960 $fullfile = $rootdir .'/'. $file; 6961 if (filetype($fullfile) == 'dir') { 6962 if ($getdirs) { 6963 $dirs[] = $file; 6964 } 6965 if ($descend) { 6966 $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles); 6967 foreach ($subdirs as $subdir) { 6968 $dirs[] = $file .'/'. $subdir; 6969 } 6970 } 6971 } else if ($getfiles) { 6972 $dirs[] = $file; 6973 } 6974 } 6975 closedir($dir); 6976 6977 asort($dirs); 6978 6979 return $dirs; 6980 } 6981 6982 6983 /** 6984 * Adds up all the files in a directory and works out the size. 6985 * 6986 * @param string $rootdir The directory to start from 6987 * @param string $excludefile A file to exclude when summing directory size 6988 * @return int The summed size of all files and subfiles within the root directory 6989 */ 6990 function get_directory_size($rootdir, $excludefile='') { 6991 global $CFG; 6992 6993 // Do it this way if we can, it's much faster. 6994 if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) { 6995 $command = trim($CFG->pathtodu).' -sk '.escapeshellarg($rootdir); 6996 $output = null; 6997 $return = null; 6998 exec($command, $output, $return); 6999 if (is_array($output)) { 7000 // We told it to return k. 7001 return get_real_size(intval($output[0]).'k'); 7002 } 7003 } 7004 7005 if (!is_dir($rootdir)) { 7006 // Must be a directory. 7007 return 0; 7008 } 7009 7010 if (!$dir = @opendir($rootdir)) { 7011 // Can't open it for some reason. 7012 return 0; 7013 } 7014 7015 $size = 0; 7016 7017 while (false !== ($file = readdir($dir))) { 7018 $firstchar = substr($file, 0, 1); 7019 if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) { 7020 continue; 7021 } 7022 $fullfile = $rootdir .'/'. $file; 7023 if (filetype($fullfile) == 'dir') { 7024 $size += get_directory_size($fullfile, $excludefile); 7025 } else { 7026 $size += filesize($fullfile); 7027 } 7028 } 7029 closedir($dir); 7030 7031 return $size; 7032 } 7033 7034 /** 7035 * Converts bytes into display form 7036 * 7037 * @param int $size The size to convert to human readable form 7038 * @param int $decimalplaces If specified, uses fixed number of decimal places 7039 * @param string $fixedunits If specified, uses fixed units (e.g. 'KB') 7040 * @return string Display version of size 7041 */ 7042 function display_size($size, int $decimalplaces = 1, string $fixedunits = ''): string { 7043 7044 static $units; 7045 7046 if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) { 7047 return get_string('unlimited'); 7048 } 7049 7050 if (empty($units)) { 7051 $units[] = get_string('sizeb'); 7052 $units[] = get_string('sizekb'); 7053 $units[] = get_string('sizemb'); 7054 $units[] = get_string('sizegb'); 7055 $units[] = get_string('sizetb'); 7056 $units[] = get_string('sizepb'); 7057 } 7058 7059 switch ($fixedunits) { 7060 case 'PB' : 7061 $magnitude = 5; 7062 break; 7063 case 'TB' : 7064 $magnitude = 4; 7065 break; 7066 case 'GB' : 7067 $magnitude = 3; 7068 break; 7069 case 'MB' : 7070 $magnitude = 2; 7071 break; 7072 case 'KB' : 7073 $magnitude = 1; 7074 break; 7075 case 'B' : 7076 $magnitude = 0; 7077 break; 7078 case '': 7079 $magnitude = floor(log($size, 1024)); 7080 $magnitude = max(0, min(5, $magnitude)); 7081 break; 7082 default: 7083 throw new coding_exception('Unknown fixed units value: ' . $fixedunits); 7084 } 7085 7086 // Special case for magnitude 0 (bytes) - never use decimal places. 7087 $nbsp = "\xc2\xa0"; 7088 if ($magnitude === 0) { 7089 return round($size) . $nbsp . $units[$magnitude]; 7090 } 7091 7092 // Convert to specified units. 7093 $sizeinunit = $size / 1024 ** $magnitude; 7094 7095 // Fixed decimal places. 7096 return sprintf('%.' . $decimalplaces . 'f', $sizeinunit) . $nbsp . $units[$magnitude]; 7097 } 7098 7099 /** 7100 * Cleans a given filename by removing suspicious or troublesome characters 7101 * 7102 * @see clean_param() 7103 * @param string $string file name 7104 * @return string cleaned file name 7105 */ 7106 function clean_filename($string) { 7107 return clean_param($string, PARAM_FILE); 7108 } 7109 7110 // STRING TRANSLATION. 7111 7112 /** 7113 * Returns the code for the current language 7114 * 7115 * @category string 7116 * @return string 7117 */ 7118 function current_language() { 7119 global $CFG, $PAGE, $SESSION, $USER; 7120 7121 if (!empty($SESSION->forcelang)) { 7122 // Allows overriding course-forced language (useful for admins to check 7123 // issues in courses whose language they don't understand). 7124 // Also used by some code to temporarily get language-related information in a 7125 // specific language (see force_current_language()). 7126 $return = $SESSION->forcelang; 7127 7128 } else if (!empty($PAGE->cm->lang)) { 7129 // Activity language, if set. 7130 $return = $PAGE->cm->lang; 7131 7132 } else if (!empty($PAGE->course->id) && $PAGE->course->id != SITEID && !empty($PAGE->course->lang)) { 7133 // Course language can override all other settings for this page. 7134 $return = $PAGE->course->lang; 7135 7136 } else if (!empty($SESSION->lang)) { 7137 // Session language can override other settings. 7138 $return = $SESSION->lang; 7139 7140 } else if (!empty($USER->lang)) { 7141 $return = $USER->lang; 7142 7143 } else if (isset($CFG->lang)) { 7144 $return = $CFG->lang; 7145 7146 } else { 7147 $return = 'en'; 7148 } 7149 7150 // Just in case this slipped in from somewhere by accident. 7151 $return = str_replace('_utf8', '', $return); 7152 7153 return $return; 7154 } 7155 7156 /** 7157 * Fix the current language to the given language code. 7158 * 7159 * @param string $lang The language code to use. 7160 * @return void 7161 */ 7162 function fix_current_language(string $lang): void { 7163 global $CFG, $COURSE, $SESSION, $USER; 7164 7165 if (!get_string_manager()->translation_exists($lang)) { 7166 throw new coding_exception("The language pack for $lang is not available"); 7167 } 7168 7169 $fixglobal = ''; 7170 $fixlang = 'lang'; 7171 if (!empty($SESSION->forcelang)) { 7172 $fixglobal = $SESSION; 7173 $fixlang = 'forcelang'; 7174 } else if (!empty($COURSE->id) && $COURSE->id != SITEID && !empty($COURSE->lang)) { 7175 $fixglobal = $COURSE; 7176 } else if (!empty($SESSION->lang)) { 7177 $fixglobal = $SESSION; 7178 } else if (!empty($USER->lang)) { 7179 $fixglobal = $USER; 7180 } else if (isset($CFG->lang)) { 7181 set_config('lang', $lang); 7182 } 7183 7184 if ($fixglobal) { 7185 $fixglobal->$fixlang = $lang; 7186 } 7187 } 7188 7189 /** 7190 * Returns parent language of current active language if defined 7191 * 7192 * @category string 7193 * @param string $lang null means current language 7194 * @return string 7195 */ 7196 function get_parent_language($lang=null) { 7197 7198 $parentlang = get_string_manager()->get_string('parentlanguage', 'langconfig', null, $lang); 7199 7200 if ($parentlang === 'en') { 7201 $parentlang = ''; 7202 } 7203 7204 return $parentlang; 7205 } 7206 7207 /** 7208 * Force the current language to get strings and dates localised in the given language. 7209 * 7210 * After calling this function, all strings will be provided in the given language 7211 * until this function is called again, or equivalent code is run. 7212 * 7213 * @param string $language 7214 * @return string previous $SESSION->forcelang value 7215 */ 7216 function force_current_language($language) { 7217 global $SESSION; 7218 $sessionforcelang = isset($SESSION->forcelang) ? $SESSION->forcelang : ''; 7219 if ($language !== $sessionforcelang) { 7220 // Setting forcelang to null or an empty string disables its effect. 7221 if (empty($language) || get_string_manager()->translation_exists($language, false)) { 7222 $SESSION->forcelang = $language; 7223 moodle_setlocale(); 7224 } 7225 } 7226 return $sessionforcelang; 7227 } 7228 7229 /** 7230 * Returns current string_manager instance. 7231 * 7232 * The param $forcereload is needed for CLI installer only where the string_manager instance 7233 * must be replaced during the install.php script life time. 7234 * 7235 * @category string 7236 * @param bool $forcereload shall the singleton be released and new instance created instead? 7237 * @return core_string_manager 7238 */ 7239 function get_string_manager($forcereload=false) { 7240 global $CFG; 7241 7242 static $singleton = null; 7243 7244 if ($forcereload) { 7245 $singleton = null; 7246 } 7247 if ($singleton === null) { 7248 if (empty($CFG->early_install_lang)) { 7249 7250 $transaliases = array(); 7251 if (empty($CFG->langlist)) { 7252 $translist = array(); 7253 } else { 7254 $translist = explode(',', $CFG->langlist); 7255 $translist = array_map('trim', $translist); 7256 // Each language in the $CFG->langlist can has an "alias" that would substitute the default language name. 7257 foreach ($translist as $i => $value) { 7258 $parts = preg_split('/\s*\|\s*/', $value, 2); 7259 if (count($parts) == 2) { 7260 $transaliases[$parts[0]] = $parts[1]; 7261 $translist[$i] = $parts[0]; 7262 } 7263 } 7264 } 7265 7266 if (!empty($CFG->config_php_settings['customstringmanager'])) { 7267 $classname = $CFG->config_php_settings['customstringmanager']; 7268 7269 if (class_exists($classname)) { 7270 $implements = class_implements($classname); 7271 7272 if (isset($implements['core_string_manager'])) { 7273 $singleton = new $classname($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases); 7274 return $singleton; 7275 7276 } else { 7277 debugging('Unable to instantiate custom string manager: class '.$classname. 7278 ' does not implement the core_string_manager interface.'); 7279 } 7280 7281 } else { 7282 debugging('Unable to instantiate custom string manager: class '.$classname.' can not be found.'); 7283 } 7284 } 7285 7286 $singleton = new core_string_manager_standard($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases); 7287 7288 } else { 7289 $singleton = new core_string_manager_install(); 7290 } 7291 } 7292 7293 return $singleton; 7294 } 7295 7296 /** 7297 * Returns a localized string. 7298 * 7299 * Returns the translated string specified by $identifier as 7300 * for $module. Uses the same format files as STphp. 7301 * $a is an object, string or number that can be used 7302 * within translation strings 7303 * 7304 * eg 'hello {$a->firstname} {$a->lastname}' 7305 * or 'hello {$a}' 7306 * 7307 * If you would like to directly echo the localized string use 7308 * the function {@link print_string()} 7309 * 7310 * Example usage of this function involves finding the string you would 7311 * like a local equivalent of and using its identifier and module information 7312 * to retrieve it.<br/> 7313 * If you open moodle/lang/en/moodle.php and look near line 278 7314 * you will find a string to prompt a user for their word for 'course' 7315 * <code> 7316 * $string['course'] = 'Course'; 7317 * </code> 7318 * So if you want to display the string 'Course' 7319 * in any language that supports it on your site 7320 * you just need to use the identifier 'course' 7321 * <code> 7322 * $mystring = '<strong>'. get_string('course') .'</strong>'; 7323 * or 7324 * </code> 7325 * If the string you want is in another file you'd take a slightly 7326 * different approach. Looking in moodle/lang/en/calendar.php you find 7327 * around line 75: 7328 * <code> 7329 * $string['typecourse'] = 'Course event'; 7330 * </code> 7331 * If you want to display the string "Course event" in any language 7332 * supported you would use the identifier 'typecourse' and the module 'calendar' 7333 * (because it is in the file calendar.php): 7334 * <code> 7335 * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>'; 7336 * </code> 7337 * 7338 * As a last resort, should the identifier fail to map to a string 7339 * the returned string will be [[ $identifier ]] 7340 * 7341 * In Moodle 2.3 there is a new argument to this function $lazyload. 7342 * Setting $lazyload to true causes get_string to return a lang_string object 7343 * rather than the string itself. The fetching of the string is then put off until 7344 * the string object is first used. The object can be used by calling it's out 7345 * method or by casting the object to a string, either directly e.g. 7346 * (string)$stringobject 7347 * or indirectly by using the string within another string or echoing it out e.g. 7348 * echo $stringobject 7349 * return "<p>{$stringobject}</p>"; 7350 * It is worth noting that using $lazyload and attempting to use the string as an 7351 * array key will cause a fatal error as objects cannot be used as array keys. 7352 * But you should never do that anyway! 7353 * For more information {@link lang_string} 7354 * 7355 * @category string 7356 * @param string $identifier The key identifier for the localized string 7357 * @param string $component The module where the key identifier is stored, 7358 * usually expressed as the filename in the language pack without the 7359 * .php on the end but can also be written as mod/forum or grade/export/xls. 7360 * If none is specified then moodle.php is used. 7361 * @param string|object|array $a An object, string or number that can be used 7362 * within translation strings 7363 * @param bool $lazyload If set to true a string object is returned instead of 7364 * the string itself. The string then isn't calculated until it is first used. 7365 * @return string The localized string. 7366 * @throws coding_exception 7367 */ 7368 function get_string($identifier, $component = '', $a = null, $lazyload = false) { 7369 global $CFG; 7370 7371 // If the lazy load argument has been supplied return a lang_string object 7372 // instead. 7373 // We need to make sure it is true (and a bool) as you will see below there 7374 // used to be a forth argument at one point. 7375 if ($lazyload === true) { 7376 return new lang_string($identifier, $component, $a); 7377 } 7378 7379 if ($CFG->debugdeveloper && clean_param($identifier, PARAM_STRINGID) === '') { 7380 throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.', DEBUG_DEVELOPER); 7381 } 7382 7383 // There is now a forth argument again, this time it is a boolean however so 7384 // we can still check for the old extralocations parameter. 7385 if (!is_bool($lazyload) && !empty($lazyload)) { 7386 debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.'); 7387 } 7388 7389 if (strpos((string)$component, '/') !== false) { 7390 debugging('The module name you passed to get_string is the deprecated format ' . 7391 'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER); 7392 $componentpath = explode('/', $component); 7393 7394 switch ($componentpath[0]) { 7395 case 'mod': 7396 $component = $componentpath[1]; 7397 break; 7398 case 'blocks': 7399 case 'block': 7400 $component = 'block_'.$componentpath[1]; 7401 break; 7402 case 'enrol': 7403 $component = 'enrol_'.$componentpath[1]; 7404 break; 7405 case 'format': 7406 $component = 'format_'.$componentpath[1]; 7407 break; 7408 case 'grade': 7409 $component = 'grade'.$componentpath[1].'_'.$componentpath[2]; 7410 break; 7411 } 7412 } 7413 7414 $result = get_string_manager()->get_string($identifier, $component, $a); 7415 7416 // Debugging feature lets you display string identifier and component. 7417 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) { 7418 $result .= ' {' . $identifier . '/' . $component . '}'; 7419 } 7420 return $result; 7421 } 7422 7423 /** 7424 * Converts an array of strings to their localized value. 7425 * 7426 * @param array $array An array of strings 7427 * @param string $component The language module that these strings can be found in. 7428 * @return stdClass translated strings. 7429 */ 7430 function get_strings($array, $component = '') { 7431 $string = new stdClass; 7432 foreach ($array as $item) { 7433 $string->$item = get_string($item, $component); 7434 } 7435 return $string; 7436 } 7437 7438 /** 7439 * Prints out a translated string. 7440 * 7441 * Prints out a translated string using the return value from the {@link get_string()} function. 7442 * 7443 * Example usage of this function when the string is in the moodle.php file:<br/> 7444 * <code> 7445 * echo '<strong>'; 7446 * print_string('course'); 7447 * echo '</strong>'; 7448 * </code> 7449 * 7450 * Example usage of this function when the string is not in the moodle.php file:<br/> 7451 * <code> 7452 * echo '<h1>'; 7453 * print_string('typecourse', 'calendar'); 7454 * echo '</h1>'; 7455 * </code> 7456 * 7457 * @category string 7458 * @param string $identifier The key identifier for the localized string 7459 * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used. 7460 * @param string|object|array $a An object, string or number that can be used within translation strings 7461 */ 7462 function print_string($identifier, $component = '', $a = null) { 7463 echo get_string($identifier, $component, $a); 7464 } 7465 7466 /** 7467 * Returns a list of charset codes 7468 * 7469 * Returns a list of charset codes. It's hardcoded, so they should be added manually 7470 * (checking that such charset is supported by the texlib library!) 7471 * 7472 * @return array And associative array with contents in the form of charset => charset 7473 */ 7474 function get_list_of_charsets() { 7475 7476 $charsets = array( 7477 'EUC-JP' => 'EUC-JP', 7478 'ISO-2022-JP'=> 'ISO-2022-JP', 7479 'ISO-8859-1' => 'ISO-8859-1', 7480 'SHIFT-JIS' => 'SHIFT-JIS', 7481 'GB2312' => 'GB2312', 7482 'GB18030' => 'GB18030', // GB18030 not supported by typo and mbstring. 7483 'UTF-8' => 'UTF-8'); 7484 7485 asort($charsets); 7486 7487 return $charsets; 7488 } 7489 7490 /** 7491 * Returns a list of valid and compatible themes 7492 * 7493 * @return array 7494 */ 7495 function get_list_of_themes() { 7496 global $CFG; 7497 7498 $themes = array(); 7499 7500 if (!empty($CFG->themelist)) { // Use admin's list of themes. 7501 $themelist = explode(',', $CFG->themelist); 7502 } else { 7503 $themelist = array_keys(core_component::get_plugin_list("theme")); 7504 } 7505 7506 foreach ($themelist as $key => $themename) { 7507 $theme = theme_config::load($themename); 7508 $themes[$themename] = $theme; 7509 } 7510 7511 core_collator::asort_objects_by_method($themes, 'get_theme_name'); 7512 7513 return $themes; 7514 } 7515 7516 /** 7517 * Factory function for emoticon_manager 7518 * 7519 * @return emoticon_manager singleton 7520 */ 7521 function get_emoticon_manager() { 7522 static $singleton = null; 7523 7524 if (is_null($singleton)) { 7525 $singleton = new emoticon_manager(); 7526 } 7527 7528 return $singleton; 7529 } 7530 7531 /** 7532 * Provides core support for plugins that have to deal with emoticons (like HTML editor or emoticon filter). 7533 * 7534 * Whenever this manager mentiones 'emoticon object', the following data 7535 * structure is expected: stdClass with properties text, imagename, imagecomponent, 7536 * altidentifier and altcomponent 7537 * 7538 * @see admin_setting_emoticons 7539 * 7540 * @copyright 2010 David Mudrak 7541 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 7542 */ 7543 class emoticon_manager { 7544 7545 /** 7546 * Returns the currently enabled emoticons 7547 * 7548 * @param boolean $selectable - If true, only return emoticons that should be selectable from a list. 7549 * @return array of emoticon objects 7550 */ 7551 public function get_emoticons($selectable = false) { 7552 global $CFG; 7553 $notselectable = ['martin', 'egg']; 7554 7555 if (empty($CFG->emoticons)) { 7556 return array(); 7557 } 7558 7559 $emoticons = $this->decode_stored_config($CFG->emoticons); 7560 7561 if (!is_array($emoticons)) { 7562 // Something is wrong with the format of stored setting. 7563 debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL); 7564 return array(); 7565 } 7566 if ($selectable) { 7567 foreach ($emoticons as $index => $emote) { 7568 if (in_array($emote->altidentifier, $notselectable)) { 7569 // Skip this one. 7570 unset($emoticons[$index]); 7571 } 7572 } 7573 } 7574 7575 return $emoticons; 7576 } 7577 7578 /** 7579 * Converts emoticon object into renderable pix_emoticon object 7580 * 7581 * @param stdClass $emoticon emoticon object 7582 * @param array $attributes explicit HTML attributes to set 7583 * @return pix_emoticon 7584 */ 7585 public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array()) { 7586 $stringmanager = get_string_manager(); 7587 if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) { 7588 $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent); 7589 } else { 7590 $alt = s($emoticon->text); 7591 } 7592 return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes); 7593 } 7594 7595 /** 7596 * Encodes the array of emoticon objects into a string storable in config table 7597 * 7598 * @see self::decode_stored_config() 7599 * @param array $emoticons array of emtocion objects 7600 * @return string 7601 */ 7602 public function encode_stored_config(array $emoticons) { 7603 return json_encode($emoticons); 7604 } 7605 7606 /** 7607 * Decodes the string into an array of emoticon objects 7608 * 7609 * @see self::encode_stored_config() 7610 * @param string $encoded 7611 * @return string|null 7612 */ 7613 public function decode_stored_config($encoded) { 7614 $decoded = json_decode($encoded); 7615 if (!is_array($decoded)) { 7616 return null; 7617 } 7618 return $decoded; 7619 } 7620 7621 /** 7622 * Returns default set of emoticons supported by Moodle 7623 * 7624 * @return array of sdtClasses 7625 */ 7626 public function default_emoticons() { 7627 return array( 7628 $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'), 7629 $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'), 7630 $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'), 7631 $this->prepare_emoticon_object(";-)", 's/wink', 'wink'), 7632 $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'), 7633 $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'), 7634 $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'), 7635 $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'), 7636 $this->prepare_emoticon_object("B-)", 's/cool', 'cool'), 7637 $this->prepare_emoticon_object("^-)", 's/approve', 'approve'), 7638 $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'), 7639 $this->prepare_emoticon_object(":o)", 's/clown', 'clown'), 7640 $this->prepare_emoticon_object(":-(", 's/sad', 'sad'), 7641 $this->prepare_emoticon_object(":(", 's/sad', 'sad'), 7642 $this->prepare_emoticon_object("8-.", 's/shy', 'shy'), 7643 $this->prepare_emoticon_object(":-I", 's/blush', 'blush'), 7644 $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'), 7645 $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'), 7646 $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'), 7647 $this->prepare_emoticon_object("8-[", 's/angry', 'angry'), 7648 $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'), 7649 $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'), 7650 $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'), 7651 $this->prepare_emoticon_object("}-]", 's/evil', 'evil'), 7652 $this->prepare_emoticon_object("(h)", 's/heart', 'heart'), 7653 $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'), 7654 $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'), 7655 $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'), 7656 $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'), 7657 $this->prepare_emoticon_object("( )", 's/egg', 'egg'), 7658 ); 7659 } 7660 7661 /** 7662 * Helper method preparing the stdClass with the emoticon properties 7663 * 7664 * @param string|array $text or array of strings 7665 * @param string $imagename to be used by {@link pix_emoticon} 7666 * @param string $altidentifier alternative string identifier, null for no alt 7667 * @param string $altcomponent where the alternative string is defined 7668 * @param string $imagecomponent to be used by {@link pix_emoticon} 7669 * @return stdClass 7670 */ 7671 protected function prepare_emoticon_object($text, $imagename, $altidentifier = null, 7672 $altcomponent = 'core_pix', $imagecomponent = 'core') { 7673 return (object)array( 7674 'text' => $text, 7675 'imagename' => $imagename, 7676 'imagecomponent' => $imagecomponent, 7677 'altidentifier' => $altidentifier, 7678 'altcomponent' => $altcomponent, 7679 ); 7680 } 7681 } 7682 7683 // ENCRYPTION. 7684 7685 /** 7686 * rc4encrypt 7687 * 7688 * @param string $data Data to encrypt. 7689 * @return string The now encrypted data. 7690 */ 7691 function rc4encrypt($data) { 7692 return endecrypt(get_site_identifier(), $data, ''); 7693 } 7694 7695 /** 7696 * rc4decrypt 7697 * 7698 * @param string $data Data to decrypt. 7699 * @return string The now decrypted data. 7700 */ 7701 function rc4decrypt($data) { 7702 return endecrypt(get_site_identifier(), $data, 'de'); 7703 } 7704 7705 /** 7706 * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com] 7707 * 7708 * @todo Finish documenting this function 7709 * 7710 * @param string $pwd The password to use when encrypting or decrypting 7711 * @param string $data The data to be decrypted/encrypted 7712 * @param string $case Either 'de' for decrypt or '' for encrypt 7713 * @return string 7714 */ 7715 function endecrypt ($pwd, $data, $case) { 7716 7717 if ($case == 'de') { 7718 $data = urldecode($data); 7719 } 7720 7721 $key[] = ''; 7722 $box[] = ''; 7723 $pwdlength = strlen($pwd); 7724 7725 for ($i = 0; $i <= 255; $i++) { 7726 $key[$i] = ord(substr($pwd, ($i % $pwdlength), 1)); 7727 $box[$i] = $i; 7728 } 7729 7730 $x = 0; 7731 7732 for ($i = 0; $i <= 255; $i++) { 7733 $x = ($x + $box[$i] + $key[$i]) % 256; 7734 $tempswap = $box[$i]; 7735 $box[$i] = $box[$x]; 7736 $box[$x] = $tempswap; 7737 } 7738 7739 $cipher = ''; 7740 7741 $a = 0; 7742 $j = 0; 7743 7744 for ($i = 0; $i < strlen($data); $i++) { 7745 $a = ($a + 1) % 256; 7746 $j = ($j + $box[$a]) % 256; 7747 $temp = $box[$a]; 7748 $box[$a] = $box[$j]; 7749 $box[$j] = $temp; 7750 $k = $box[(($box[$a] + $box[$j]) % 256)]; 7751 $cipherby = ord(substr($data, $i, 1)) ^ $k; 7752 $cipher .= chr($cipherby); 7753 } 7754 7755 if ($case == 'de') { 7756 $cipher = urldecode(urlencode($cipher)); 7757 } else { 7758 $cipher = urlencode($cipher); 7759 } 7760 7761 return $cipher; 7762 } 7763 7764 // ENVIRONMENT CHECKING. 7765 7766 /** 7767 * This method validates a plug name. It is much faster than calling clean_param. 7768 * 7769 * @param string $name a string that might be a plugin name. 7770 * @return bool if this string is a valid plugin name. 7771 */ 7772 function is_valid_plugin_name($name) { 7773 // This does not work for 'mod', bad luck, use any other type. 7774 return core_component::is_valid_plugin_name('tool', $name); 7775 } 7776 7777 /** 7778 * Get a list of all the plugins of a given type that define a certain API function 7779 * in a certain file. The plugin component names and function names are returned. 7780 * 7781 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'. 7782 * @param string $function the part of the name of the function after the 7783 * frankenstyle prefix. e.g 'hook' if you are looking for functions with 7784 * names like report_courselist_hook. 7785 * @param string $file the name of file within the plugin that defines the 7786 * function. Defaults to lib.php. 7787 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum') 7788 * and the function names as values (e.g. 'report_courselist_hook', 'forum_hook'). 7789 */ 7790 function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php') { 7791 global $CFG; 7792 7793 // We don't include here as all plugin types files would be included. 7794 $plugins = get_plugins_with_function($function, $file, false); 7795 7796 if (empty($plugins[$plugintype])) { 7797 return array(); 7798 } 7799 7800 $allplugins = core_component::get_plugin_list($plugintype); 7801 7802 // Reformat the array and include the files. 7803 $pluginfunctions = array(); 7804 foreach ($plugins[$plugintype] as $pluginname => $functionname) { 7805 7806 // Check that it has not been removed and the file is still available. 7807 if (!empty($allplugins[$pluginname])) { 7808 7809 $filepath = $allplugins[$pluginname] . DIRECTORY_SEPARATOR . $file; 7810 if (file_exists($filepath)) { 7811 include_once($filepath); 7812 7813 // Now that the file is loaded, we must verify the function still exists. 7814 if (function_exists($functionname)) { 7815 $pluginfunctions[$plugintype . '_' . $pluginname] = $functionname; 7816 } else { 7817 // Invalidate the cache for next run. 7818 \cache_helper::invalidate_by_definition('core', 'plugin_functions'); 7819 } 7820 } 7821 } 7822 } 7823 7824 return $pluginfunctions; 7825 } 7826 7827 /** 7828 * Get a list of all the plugins that define a certain API function in a certain file. 7829 * 7830 * @param string $function the part of the name of the function after the 7831 * frankenstyle prefix. e.g 'hook' if you are looking for functions with 7832 * names like report_courselist_hook. 7833 * @param string $file the name of file within the plugin that defines the 7834 * function. Defaults to lib.php. 7835 * @param bool $include Whether to include the files that contain the functions or not. 7836 * @return array with [plugintype][plugin] = functionname 7837 */ 7838 function get_plugins_with_function($function, $file = 'lib.php', $include = true) { 7839 global $CFG; 7840 7841 if (during_initial_install() || isset($CFG->upgraderunning)) { 7842 // API functions _must not_ be called during an installation or upgrade. 7843 return []; 7844 } 7845 7846 $cache = \cache::make('core', 'plugin_functions'); 7847 7848 // Including both although I doubt that we will find two functions definitions with the same name. 7849 // Clearning the filename as cache_helper::hash_key only allows a-zA-Z0-9_. 7850 $key = $function . '_' . clean_param($file, PARAM_ALPHA); 7851 $pluginfunctions = $cache->get($key); 7852 $dirty = false; 7853 7854 // Use the plugin manager to check that plugins are currently installed. 7855 $pluginmanager = \core_plugin_manager::instance(); 7856 7857 if ($pluginfunctions !== false) { 7858 7859 // Checking that the files are still available. 7860 foreach ($pluginfunctions as $plugintype => $plugins) { 7861 7862 $allplugins = \core_component::get_plugin_list($plugintype); 7863 $installedplugins = $pluginmanager->get_installed_plugins($plugintype); 7864 foreach ($plugins as $plugin => $function) { 7865 if (!isset($installedplugins[$plugin])) { 7866 // Plugin code is still present on disk but it is not installed. 7867 $dirty = true; 7868 break 2; 7869 } 7870 7871 // Cache might be out of sync with the codebase, skip the plugin if it is not available. 7872 if (empty($allplugins[$plugin])) { 7873 $dirty = true; 7874 break 2; 7875 } 7876 7877 $fileexists = file_exists($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file); 7878 if ($include && $fileexists) { 7879 // Include the files if it was requested. 7880 include_once($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file); 7881 } else if (!$fileexists) { 7882 // If the file is not available any more it should not be returned. 7883 $dirty = true; 7884 break 2; 7885 } 7886 7887 // Check if the function still exists in the file. 7888 if ($include && !function_exists($function)) { 7889 $dirty = true; 7890 break 2; 7891 } 7892 } 7893 } 7894 7895 // If the cache is dirty, we should fall through and let it rebuild. 7896 if (!$dirty) { 7897 return $pluginfunctions; 7898 } 7899 } 7900 7901 $pluginfunctions = array(); 7902 7903 // To fill the cached. Also, everything should continue working with cache disabled. 7904 $plugintypes = \core_component::get_plugin_types(); 7905 foreach ($plugintypes as $plugintype => $unused) { 7906 7907 // We need to include files here. 7908 $pluginswithfile = \core_component::get_plugin_list_with_file($plugintype, $file, true); 7909 $installedplugins = $pluginmanager->get_installed_plugins($plugintype); 7910 foreach ($pluginswithfile as $plugin => $notused) { 7911 7912 if (!isset($installedplugins[$plugin])) { 7913 continue; 7914 } 7915 7916 $fullfunction = $plugintype . '_' . $plugin . '_' . $function; 7917 7918 $pluginfunction = false; 7919 if (function_exists($fullfunction)) { 7920 // Function exists with standard name. Store, indexed by frankenstyle name of plugin. 7921 $pluginfunction = $fullfunction; 7922 7923 } else if ($plugintype === 'mod') { 7924 // For modules, we also allow plugin without full frankenstyle but just starting with the module name. 7925 $shortfunction = $plugin . '_' . $function; 7926 if (function_exists($shortfunction)) { 7927 $pluginfunction = $shortfunction; 7928 } 7929 } 7930 7931 if ($pluginfunction) { 7932 if (empty($pluginfunctions[$plugintype])) { 7933 $pluginfunctions[$plugintype] = array(); 7934 } 7935 $pluginfunctions[$plugintype][$plugin] = $pluginfunction; 7936 } 7937 7938 } 7939 } 7940 $cache->set($key, $pluginfunctions); 7941 7942 return $pluginfunctions; 7943 7944 } 7945 7946 /** 7947 * Lists plugin-like directories within specified directory 7948 * 7949 * This function was originally used for standard Moodle plugins, please use 7950 * new core_component::get_plugin_list() now. 7951 * 7952 * This function is used for general directory listing and backwards compatility. 7953 * 7954 * @param string $directory relative directory from root 7955 * @param string $exclude dir name to exclude from the list (defaults to none) 7956 * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot) 7957 * @return array Sorted array of directory names found under the requested parameters 7958 */ 7959 function get_list_of_plugins($directory='mod', $exclude='', $basedir='') { 7960 global $CFG; 7961 7962 $plugins = array(); 7963 7964 if (empty($basedir)) { 7965 $basedir = $CFG->dirroot .'/'. $directory; 7966 7967 } else { 7968 $basedir = $basedir .'/'. $directory; 7969 } 7970 7971 if ($CFG->debugdeveloper and empty($exclude)) { 7972 // Make sure devs do not use this to list normal plugins, 7973 // this is intended for general directories that are not plugins! 7974 7975 $subtypes = core_component::get_plugin_types(); 7976 if (in_array($basedir, $subtypes)) { 7977 debugging('get_list_of_plugins() should not be used to list real plugins, use core_component::get_plugin_list() instead!', DEBUG_DEVELOPER); 7978 } 7979 unset($subtypes); 7980 } 7981 7982 $ignorelist = array_flip(array_filter([ 7983 'CVS', 7984 '_vti_cnf', 7985 'amd', 7986 'classes', 7987 'simpletest', 7988 'tests', 7989 'templates', 7990 'yui', 7991 $exclude, 7992 ])); 7993 7994 if (file_exists($basedir) && filetype($basedir) == 'dir') { 7995 if (!$dirhandle = opendir($basedir)) { 7996 debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER); 7997 return array(); 7998 } 7999 while (false !== ($dir = readdir($dirhandle))) { 8000 if (strpos($dir, '.') === 0) { 8001 // Ignore directories starting with . 8002 // These are treated as hidden directories. 8003 continue; 8004 } 8005 if (array_key_exists($dir, $ignorelist)) { 8006 // This directory features on the ignore list. 8007 continue; 8008 } 8009 if (filetype($basedir .'/'. $dir) != 'dir') { 8010 continue; 8011 } 8012 $plugins[] = $dir; 8013 } 8014 closedir($dirhandle); 8015 } 8016 if ($plugins) { 8017 asort($plugins); 8018 } 8019 return $plugins; 8020 } 8021 8022 /** 8023 * Invoke plugin's callback functions 8024 * 8025 * @param string $type plugin type e.g. 'mod' 8026 * @param string $name plugin name 8027 * @param string $feature feature name 8028 * @param string $action feature's action 8029 * @param array $params parameters of callback function, should be an array 8030 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null. 8031 * @return mixed 8032 * 8033 * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743 8034 */ 8035 function plugin_callback($type, $name, $feature, $action, $params = null, $default = null) { 8036 return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default); 8037 } 8038 8039 /** 8040 * Invoke component's callback functions 8041 * 8042 * @param string $component frankenstyle component name, e.g. 'mod_quiz' 8043 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron' 8044 * @param array $params parameters of callback function 8045 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null. 8046 * @return mixed 8047 */ 8048 function component_callback($component, $function, array $params = array(), $default = null) { 8049 8050 $functionname = component_callback_exists($component, $function); 8051 8052 if ($params && (array_keys($params) !== range(0, count($params) - 1))) { 8053 // PHP 8 allows to have associative arrays in the call_user_func_array() parameters but 8054 // PHP 7 does not. Using associative arrays can result in different behavior in different PHP versions. 8055 // See https://php.watch/versions/8.0/named-parameters#named-params-call_user_func_array 8056 // This check can be removed when minimum PHP version for Moodle is raised to 8. 8057 debugging('Parameters array can not be an associative array while Moodle supports both PHP 7 and PHP 8.', 8058 DEBUG_DEVELOPER); 8059 $params = array_values($params); 8060 } 8061 8062 if ($functionname) { 8063 // Function exists, so just return function result. 8064 $ret = call_user_func_array($functionname, $params); 8065 if (is_null($ret)) { 8066 return $default; 8067 } else { 8068 return $ret; 8069 } 8070 } 8071 return $default; 8072 } 8073 8074 /** 8075 * Determine if a component callback exists and return the function name to call. Note that this 8076 * function will include the required library files so that the functioname returned can be 8077 * called directly. 8078 * 8079 * @param string $component frankenstyle component name, e.g. 'mod_quiz' 8080 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron' 8081 * @return mixed Complete function name to call if the callback exists or false if it doesn't. 8082 * @throws coding_exception if invalid component specfied 8083 */ 8084 function component_callback_exists($component, $function) { 8085 global $CFG; // This is needed for the inclusions. 8086 8087 $cleancomponent = clean_param($component, PARAM_COMPONENT); 8088 if (empty($cleancomponent)) { 8089 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component); 8090 } 8091 $component = $cleancomponent; 8092 8093 list($type, $name) = core_component::normalize_component($component); 8094 $component = $type . '_' . $name; 8095 8096 $oldfunction = $name.'_'.$function; 8097 $function = $component.'_'.$function; 8098 8099 $dir = core_component::get_component_directory($component); 8100 if (empty($dir)) { 8101 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component); 8102 } 8103 8104 // Load library and look for function. 8105 if (file_exists($dir.'/lib.php')) { 8106 require_once($dir.'/lib.php'); 8107 } 8108 8109 if (!function_exists($function) and function_exists($oldfunction)) { 8110 if ($type !== 'mod' and $type !== 'core') { 8111 debugging("Please use new function name $function instead of legacy $oldfunction", DEBUG_DEVELOPER); 8112 } 8113 $function = $oldfunction; 8114 } 8115 8116 if (function_exists($function)) { 8117 return $function; 8118 } 8119 return false; 8120 } 8121 8122 /** 8123 * Call the specified callback method on the provided class. 8124 * 8125 * If the callback returns null, then the default value is returned instead. 8126 * If the class does not exist, then the default value is returned. 8127 * 8128 * @param string $classname The name of the class to call upon. 8129 * @param string $methodname The name of the staticically defined method on the class. 8130 * @param array $params The arguments to pass into the method. 8131 * @param mixed $default The default value. 8132 * @return mixed The return value. 8133 */ 8134 function component_class_callback($classname, $methodname, array $params, $default = null) { 8135 if (!class_exists($classname)) { 8136 return $default; 8137 } 8138 8139 if (!method_exists($classname, $methodname)) { 8140 return $default; 8141 } 8142 8143 $fullfunction = $classname . '::' . $methodname; 8144 $result = call_user_func_array($fullfunction, $params); 8145 8146 if (null === $result) { 8147 return $default; 8148 } else { 8149 return $result; 8150 } 8151 } 8152 8153 /** 8154 * Checks whether a plugin supports a specified feature. 8155 * 8156 * @param string $type Plugin type e.g. 'mod' 8157 * @param string $name Plugin name e.g. 'forum' 8158 * @param string $feature Feature code (FEATURE_xx constant) 8159 * @param mixed $default default value if feature support unknown 8160 * @return mixed Feature result (false if not supported, null if feature is unknown, 8161 * otherwise usually true but may have other feature-specific value such as array) 8162 * @throws coding_exception 8163 */ 8164 function plugin_supports($type, $name, $feature, $default = null) { 8165 global $CFG; 8166 8167 if ($type === 'mod' and $name === 'NEWMODULE') { 8168 // Somebody forgot to rename the module template. 8169 return false; 8170 } 8171 8172 $component = clean_param($type . '_' . $name, PARAM_COMPONENT); 8173 if (empty($component)) { 8174 throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name); 8175 } 8176 8177 $function = null; 8178 8179 if ($type === 'mod') { 8180 // We need this special case because we support subplugins in modules, 8181 // otherwise it would end up in infinite loop. 8182 if (file_exists("$CFG->dirroot/mod/$name/lib.php")) { 8183 include_once("$CFG->dirroot/mod/$name/lib.php"); 8184 $function = $component.'_supports'; 8185 if (!function_exists($function)) { 8186 // Legacy non-frankenstyle function name. 8187 $function = $name.'_supports'; 8188 } 8189 } 8190 8191 } else { 8192 if (!$path = core_component::get_plugin_directory($type, $name)) { 8193 // Non existent plugin type. 8194 return false; 8195 } 8196 if (file_exists("$path/lib.php")) { 8197 include_once("$path/lib.php"); 8198 $function = $component.'_supports'; 8199 } 8200 } 8201 8202 if ($function and function_exists($function)) { 8203 $supports = $function($feature); 8204 if (is_null($supports)) { 8205 // Plugin does not know - use default. 8206 return $default; 8207 } else { 8208 return $supports; 8209 } 8210 } 8211 8212 // Plugin does not care, so use default. 8213 return $default; 8214 } 8215 8216 /** 8217 * Returns true if the current version of PHP is greater that the specified one. 8218 * 8219 * @todo Check PHP version being required here is it too low? 8220 * 8221 * @param string $version The version of php being tested. 8222 * @return bool 8223 */ 8224 function check_php_version($version='5.2.4') { 8225 return (version_compare(phpversion(), $version) >= 0); 8226 } 8227 8228 /** 8229 * Determine if moodle installation requires update. 8230 * 8231 * Checks version numbers of main code and all plugins to see 8232 * if there are any mismatches. 8233 * 8234 * @return bool 8235 */ 8236 function moodle_needs_upgrading() { 8237 global $CFG; 8238 8239 if (empty($CFG->version)) { 8240 return true; 8241 } 8242 8243 // There is no need to purge plugininfo caches here because 8244 // these caches are not used during upgrade and they are purged after 8245 // every upgrade. 8246 8247 if (empty($CFG->allversionshash)) { 8248 return true; 8249 } 8250 8251 $hash = core_component::get_all_versions_hash(); 8252 8253 return ($hash !== $CFG->allversionshash); 8254 } 8255 8256 /** 8257 * Returns the major version of this site 8258 * 8259 * Moodle version numbers consist of three numbers separated by a dot, for 8260 * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so 8261 * called major version. This function extracts the major version from either 8262 * $CFG->release (default) or eventually from the $release variable defined in 8263 * the main version.php. 8264 * 8265 * @param bool $fromdisk should the version if source code files be used 8266 * @return string|false the major version like '2.3', false if could not be determined 8267 */ 8268 function moodle_major_version($fromdisk = false) { 8269 global $CFG; 8270 8271 if ($fromdisk) { 8272 $release = null; 8273 require($CFG->dirroot.'/version.php'); 8274 if (empty($release)) { 8275 return false; 8276 } 8277 8278 } else { 8279 if (empty($CFG->release)) { 8280 return false; 8281 } 8282 $release = $CFG->release; 8283 } 8284 8285 if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) { 8286 return $matches[0]; 8287 } else { 8288 return false; 8289 } 8290 } 8291 8292 // MISCELLANEOUS. 8293 8294 /** 8295 * Gets the system locale 8296 * 8297 * @return string Retuns the current locale. 8298 */ 8299 function moodle_getlocale() { 8300 global $CFG; 8301 8302 // Fetch the correct locale based on ostype. 8303 if ($CFG->ostype == 'WINDOWS') { 8304 $stringtofetch = 'localewin'; 8305 } else { 8306 $stringtofetch = 'locale'; 8307 } 8308 8309 if (!empty($CFG->locale)) { // Override locale for all language packs. 8310 return $CFG->locale; 8311 } 8312 8313 return get_string($stringtofetch, 'langconfig'); 8314 } 8315 8316 /** 8317 * Sets the system locale 8318 * 8319 * @category string 8320 * @param string $locale Can be used to force a locale 8321 */ 8322 function moodle_setlocale($locale='') { 8323 global $CFG; 8324 8325 static $currentlocale = ''; // Last locale caching. 8326 8327 $oldlocale = $currentlocale; 8328 8329 // The priority is the same as in get_string() - parameter, config, course, session, user, global language. 8330 if (!empty($locale)) { 8331 $currentlocale = $locale; 8332 } else { 8333 $currentlocale = moodle_getlocale(); 8334 } 8335 8336 // Do nothing if locale already set up. 8337 if ($oldlocale == $currentlocale) { 8338 return; 8339 } 8340 8341 // Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values, 8342 // set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7 8343 // Some day, numeric, monetary and other categories should be set too, I think. :-/. 8344 8345 // Get current values. 8346 $monetary= setlocale (LC_MONETARY, 0); 8347 $numeric = setlocale (LC_NUMERIC, 0); 8348 $ctype = setlocale (LC_CTYPE, 0); 8349 if ($CFG->ostype != 'WINDOWS') { 8350 $messages= setlocale (LC_MESSAGES, 0); 8351 } 8352 // Set locale to all. 8353 $result = setlocale (LC_ALL, $currentlocale); 8354 // If setting of locale fails try the other utf8 or utf-8 variant, 8355 // some operating systems support both (Debian), others just one (OSX). 8356 if ($result === false) { 8357 if (stripos($currentlocale, '.UTF-8') !== false) { 8358 $newlocale = str_ireplace('.UTF-8', '.UTF8', $currentlocale); 8359 setlocale (LC_ALL, $newlocale); 8360 } else if (stripos($currentlocale, '.UTF8') !== false) { 8361 $newlocale = str_ireplace('.UTF8', '.UTF-8', $currentlocale); 8362 setlocale (LC_ALL, $newlocale); 8363 } 8364 } 8365 // Set old values. 8366 setlocale (LC_MONETARY, $monetary); 8367 setlocale (LC_NUMERIC, $numeric); 8368 if ($CFG->ostype != 'WINDOWS') { 8369 setlocale (LC_MESSAGES, $messages); 8370 } 8371 if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') { 8372 // To workaround a well-known PHP problem with Turkish letter Ii. 8373 setlocale (LC_CTYPE, $ctype); 8374 } 8375 } 8376 8377 /** 8378 * Count words in a string. 8379 * 8380 * Words are defined as things between whitespace. 8381 * 8382 * @category string 8383 * @param string $string The text to be searched for words. May be HTML. 8384 * @param int|null $format 8385 * @return int The count of words in the specified string 8386 */ 8387 function count_words($string, $format = null) { 8388 // Before stripping tags, add a space after the close tag of anything that is not obviously inline. 8389 // Also, br is a special case because it definitely delimits a word, but has no close tag. 8390 $string = preg_replace('~ 8391 ( # Capture the tag we match. 8392 </ # Start of close tag. 8393 (?! # Do not match any of these specific close tag names. 8394 a> | b> | del> | em> | i> | 8395 ins> | s> | small> | span> | 8396 strong> | sub> | sup> | u> 8397 ) 8398 \w+ # But, apart from those execptions, match any tag name. 8399 > # End of close tag. 8400 | 8401 <br> | <br\s*/> # Special cases that are not close tags. 8402 ) 8403 ~x', '$1 ', $string); // Add a space after the close tag. 8404 if ($format !== null && $format != FORMAT_PLAIN) { 8405 // Match the usual text cleaning before display. 8406 // Ideally we should apply multilang filter only here, other filters might add extra text. 8407 $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]); 8408 } 8409 // Now remove HTML tags. 8410 $string = strip_tags($string); 8411 // Decode HTML entities. 8412 $string = html_entity_decode($string, ENT_COMPAT); 8413 8414 // Now, the word count is the number of blocks of characters separated 8415 // by any sort of space. That seems to be the definition used by all other systems. 8416 // To be precise about what is considered to separate words: 8417 // * Anything that Unicode considers a 'Separator' 8418 // * Anything that Unicode considers a 'Control character' 8419 // * An em- or en- dash. 8420 return count(preg_split('~[\p{Z}\p{Cc}—–]+~u', $string, -1, PREG_SPLIT_NO_EMPTY)); 8421 } 8422 8423 /** 8424 * Count letters in a string. 8425 * 8426 * Letters are defined as chars not in tags and different from whitespace. 8427 * 8428 * @category string 8429 * @param string $string The text to be searched for letters. May be HTML. 8430 * @param int|null $format 8431 * @return int The count of letters in the specified text. 8432 */ 8433 function count_letters($string, $format = null) { 8434 if ($format !== null && $format != FORMAT_PLAIN) { 8435 // Match the usual text cleaning before display. 8436 // Ideally we should apply multilang filter only here, other filters might add extra text. 8437 $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]); 8438 } 8439 $string = strip_tags($string); // Tags are out now. 8440 $string = html_entity_decode($string, ENT_COMPAT); 8441 $string = preg_replace('/[[:space:]]*/', '', $string); // Whitespace are out now. 8442 8443 return core_text::strlen($string); 8444 } 8445 8446 /** 8447 * Generate and return a random string of the specified length. 8448 * 8449 * @param int $length The length of the string to be created. 8450 * @return string 8451 */ 8452 function random_string($length=15) { 8453 $randombytes = random_bytes_emulate($length); 8454 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 8455 $pool .= 'abcdefghijklmnopqrstuvwxyz'; 8456 $pool .= '0123456789'; 8457 $poollen = strlen($pool); 8458 $string = ''; 8459 for ($i = 0; $i < $length; $i++) { 8460 $rand = ord($randombytes[$i]); 8461 $string .= substr($pool, ($rand%($poollen)), 1); 8462 } 8463 return $string; 8464 } 8465 8466 /** 8467 * Generate a complex random string (useful for md5 salts) 8468 * 8469 * This function is based on the above {@link random_string()} however it uses a 8470 * larger pool of characters and generates a string between 24 and 32 characters 8471 * 8472 * @param int $length Optional if set generates a string to exactly this length 8473 * @return string 8474 */ 8475 function complex_random_string($length=null) { 8476 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 8477 $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} '; 8478 $poollen = strlen($pool); 8479 if ($length===null) { 8480 $length = floor(rand(24, 32)); 8481 } 8482 $randombytes = random_bytes_emulate($length); 8483 $string = ''; 8484 for ($i = 0; $i < $length; $i++) { 8485 $rand = ord($randombytes[$i]); 8486 $string .= $pool[($rand%$poollen)]; 8487 } 8488 return $string; 8489 } 8490 8491 /** 8492 * Try to generates cryptographically secure pseudo-random bytes. 8493 * 8494 * Note this is achieved by fallbacking between: 8495 * - PHP 7 random_bytes(). 8496 * - OpenSSL openssl_random_pseudo_bytes(). 8497 * - In house random generator getting its entropy from various, hard to guess, pseudo-random sources. 8498 * 8499 * @param int $length requested length in bytes 8500 * @return string binary data 8501 */ 8502 function random_bytes_emulate($length) { 8503 global $CFG; 8504 if ($length <= 0) { 8505 debugging('Invalid random bytes length', DEBUG_DEVELOPER); 8506 return ''; 8507 } 8508 if (function_exists('random_bytes')) { 8509 // Use PHP 7 goodness. 8510 $hash = @random_bytes($length); 8511 if ($hash !== false) { 8512 return $hash; 8513 } 8514 } 8515 if (function_exists('openssl_random_pseudo_bytes')) { 8516 // If you have the openssl extension enabled. 8517 $hash = openssl_random_pseudo_bytes($length); 8518 if ($hash !== false) { 8519 return $hash; 8520 } 8521 } 8522 8523 // Bad luck, there is no reliable random generator, let's just slowly hash some unique stuff that is hard to guess. 8524 $staticdata = serialize($CFG) . serialize($_SERVER); 8525 $hash = ''; 8526 do { 8527 $hash .= sha1($staticdata . microtime(true) . uniqid('', true), true); 8528 } while (strlen($hash) < $length); 8529 8530 return substr($hash, 0, $length); 8531 } 8532 8533 /** 8534 * Given some text (which may contain HTML) and an ideal length, 8535 * this function truncates the text neatly on a word boundary if possible 8536 * 8537 * @category string 8538 * @param string $text text to be shortened 8539 * @param int $ideal ideal string length 8540 * @param boolean $exact if false, $text will not be cut mid-word 8541 * @param string $ending The string to append if the passed string is truncated 8542 * @return string $truncate shortened string 8543 */ 8544 function shorten_text($text, $ideal=30, $exact = false, $ending='...') { 8545 // If the plain text is shorter than the maximum length, return the whole text. 8546 if (core_text::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) { 8547 return $text; 8548 } 8549 8550 // Splits on HTML tags. Each open/close/empty tag will be the first thing 8551 // and only tag in its 'line'. 8552 preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER); 8553 8554 $totallength = core_text::strlen($ending); 8555 $truncate = ''; 8556 8557 // This array stores information about open and close tags and their position 8558 // in the truncated string. Each item in the array is an object with fields 8559 // ->open (true if open), ->tag (tag name in lower case), and ->pos 8560 // (byte position in truncated text). 8561 $tagdetails = array(); 8562 8563 foreach ($lines as $linematchings) { 8564 // If there is any html-tag in this line, handle it and add it (uncounted) to the output. 8565 if (!empty($linematchings[1])) { 8566 // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>). 8567 if (!preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $linematchings[1])) { 8568 if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $linematchings[1], $tagmatchings)) { 8569 // Record closing tag. 8570 $tagdetails[] = (object) array( 8571 'open' => false, 8572 'tag' => core_text::strtolower($tagmatchings[1]), 8573 'pos' => core_text::strlen($truncate), 8574 ); 8575 8576 } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $linematchings[1], $tagmatchings)) { 8577 // Record opening tag. 8578 $tagdetails[] = (object) array( 8579 'open' => true, 8580 'tag' => core_text::strtolower($tagmatchings[1]), 8581 'pos' => core_text::strlen($truncate), 8582 ); 8583 } else if (preg_match('/^<!--\[if\s.*?\]>$/s', $linematchings[1], $tagmatchings)) { 8584 $tagdetails[] = (object) array( 8585 'open' => true, 8586 'tag' => core_text::strtolower('if'), 8587 'pos' => core_text::strlen($truncate), 8588 ); 8589 } else if (preg_match('/^<!--<!\[endif\]-->$/s', $linematchings[1], $tagmatchings)) { 8590 $tagdetails[] = (object) array( 8591 'open' => false, 8592 'tag' => core_text::strtolower('if'), 8593 'pos' => core_text::strlen($truncate), 8594 ); 8595 } 8596 } 8597 // Add html-tag to $truncate'd text. 8598 $truncate .= $linematchings[1]; 8599 } 8600 8601 // Calculate the length of the plain text part of the line; handle entities as one character. 8602 $contentlength = core_text::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $linematchings[2])); 8603 if ($totallength + $contentlength > $ideal) { 8604 // The number of characters which are left. 8605 $left = $ideal - $totallength; 8606 $entitieslength = 0; 8607 // Search for html entities. 8608 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)) { 8609 // Calculate the real length of all entities in the legal range. 8610 foreach ($entities[0] as $entity) { 8611 if ($entity[1]+1-$entitieslength <= $left) { 8612 $left--; 8613 $entitieslength += core_text::strlen($entity[0]); 8614 } else { 8615 // No more characters left. 8616 break; 8617 } 8618 } 8619 } 8620 $breakpos = $left + $entitieslength; 8621 8622 // If the words shouldn't be cut in the middle... 8623 if (!$exact) { 8624 // Search the last occurence of a space. 8625 for (; $breakpos > 0; $breakpos--) { 8626 if ($char = core_text::substr($linematchings[2], $breakpos, 1)) { 8627 if ($char === '.' or $char === ' ') { 8628 $breakpos += 1; 8629 break; 8630 } else if (strlen($char) > 2) { 8631 // Chinese/Japanese/Korean text can be truncated at any UTF-8 character boundary. 8632 $breakpos += 1; 8633 break; 8634 } 8635 } 8636 } 8637 } 8638 if ($breakpos == 0) { 8639 // This deals with the test_shorten_text_no_spaces case. 8640 $breakpos = $left + $entitieslength; 8641 } else if ($breakpos > $left + $entitieslength) { 8642 // This deals with the previous for loop breaking on the first char. 8643 $breakpos = $left + $entitieslength; 8644 } 8645 8646 $truncate .= core_text::substr($linematchings[2], 0, $breakpos); 8647 // Maximum length is reached, so get off the loop. 8648 break; 8649 } else { 8650 $truncate .= $linematchings[2]; 8651 $totallength += $contentlength; 8652 } 8653 8654 // If the maximum length is reached, get off the loop. 8655 if ($totallength >= $ideal) { 8656 break; 8657 } 8658 } 8659 8660 // Add the defined ending to the text. 8661 $truncate .= $ending; 8662 8663 // Now calculate the list of open html tags based on the truncate position. 8664 $opentags = array(); 8665 foreach ($tagdetails as $taginfo) { 8666 if ($taginfo->open) { 8667 // Add tag to the beginning of $opentags list. 8668 array_unshift($opentags, $taginfo->tag); 8669 } else { 8670 // Can have multiple exact same open tags, close the last one. 8671 $pos = array_search($taginfo->tag, array_reverse($opentags, true)); 8672 if ($pos !== false) { 8673 unset($opentags[$pos]); 8674 } 8675 } 8676 } 8677 8678 // Close all unclosed html-tags. 8679 foreach ($opentags as $tag) { 8680 if ($tag === 'if') { 8681 $truncate .= '<!--<![endif]-->'; 8682 } else { 8683 $truncate .= '</' . $tag . '>'; 8684 } 8685 } 8686 8687 return $truncate; 8688 } 8689 8690 /** 8691 * Shortens a given filename by removing characters positioned after the ideal string length. 8692 * When the filename is too long, the file cannot be created on the filesystem due to exceeding max byte size. 8693 * Limiting the filename to a certain size (considering multibyte characters) will prevent this. 8694 * 8695 * @param string $filename file name 8696 * @param int $length ideal string length 8697 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness. 8698 * @return string $shortened shortened file name 8699 */ 8700 function shorten_filename($filename, $length = MAX_FILENAME_SIZE, $includehash = false) { 8701 $shortened = $filename; 8702 // Extract a part of the filename if it's char size exceeds the ideal string length. 8703 if (core_text::strlen($filename) > $length) { 8704 // Exclude extension if present in filename. 8705 $mimetypes = get_mimetypes_array(); 8706 $extension = pathinfo($filename, PATHINFO_EXTENSION); 8707 if ($extension && !empty($mimetypes[$extension])) { 8708 $basename = pathinfo($filename, PATHINFO_FILENAME); 8709 $hash = empty($includehash) ? '' : ' - ' . substr(sha1($basename), 0, 10); 8710 $shortened = core_text::substr($basename, 0, $length - strlen($hash)) . $hash; 8711 $shortened .= '.' . $extension; 8712 } else { 8713 $hash = empty($includehash) ? '' : ' - ' . substr(sha1($filename), 0, 10); 8714 $shortened = core_text::substr($filename, 0, $length - strlen($hash)) . $hash; 8715 } 8716 } 8717 return $shortened; 8718 } 8719 8720 /** 8721 * Shortens a given array of filenames by removing characters positioned after the ideal string length. 8722 * 8723 * @param array $path The paths to reduce the length. 8724 * @param int $length Ideal string length 8725 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness. 8726 * @return array $result Shortened paths in array. 8727 */ 8728 function shorten_filenames(array $path, $length = MAX_FILENAME_SIZE, $includehash = false) { 8729 $result = null; 8730 8731 $result = array_reduce($path, function($carry, $singlepath) use ($length, $includehash) { 8732 $carry[] = shorten_filename($singlepath, $length, $includehash); 8733 return $carry; 8734 }, []); 8735 8736 return $result; 8737 } 8738 8739 /** 8740 * Given dates in seconds, how many weeks is the date from startdate 8741 * The first week is 1, the second 2 etc ... 8742 * 8743 * @param int $startdate Timestamp for the start date 8744 * @param int $thedate Timestamp for the end date 8745 * @return string 8746 */ 8747 function getweek ($startdate, $thedate) { 8748 if ($thedate < $startdate) { 8749 return 0; 8750 } 8751 8752 return floor(($thedate - $startdate) / WEEKSECS) + 1; 8753 } 8754 8755 /** 8756 * Returns a randomly generated password of length $maxlen. inspired by 8757 * 8758 * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and 8759 * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254} 8760 * 8761 * @param int $maxlen The maximum size of the password being generated. 8762 * @return string 8763 */ 8764 function generate_password($maxlen=10) { 8765 global $CFG; 8766 8767 if (empty($CFG->passwordpolicy)) { 8768 $fillers = PASSWORD_DIGITS; 8769 $wordlist = file($CFG->wordlist); 8770 $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]); 8771 $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]); 8772 $filler1 = $fillers[rand(0, strlen($fillers) - 1)]; 8773 $password = $word1 . $filler1 . $word2; 8774 } else { 8775 $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0; 8776 $digits = $CFG->minpassworddigits; 8777 $lower = $CFG->minpasswordlower; 8778 $upper = $CFG->minpasswordupper; 8779 $nonalphanum = $CFG->minpasswordnonalphanum; 8780 $total = $lower + $upper + $digits + $nonalphanum; 8781 // Var minlength should be the greater one of the two ( $minlen and $total ). 8782 $minlen = $minlen < $total ? $total : $minlen; 8783 // Var maxlen can never be smaller than minlen. 8784 $maxlen = $minlen > $maxlen ? $minlen : $maxlen; 8785 $additional = $maxlen - $total; 8786 8787 // Make sure we have enough characters to fulfill 8788 // complexity requirements. 8789 $passworddigits = PASSWORD_DIGITS; 8790 while ($digits > strlen($passworddigits)) { 8791 $passworddigits .= PASSWORD_DIGITS; 8792 } 8793 $passwordlower = PASSWORD_LOWER; 8794 while ($lower > strlen($passwordlower)) { 8795 $passwordlower .= PASSWORD_LOWER; 8796 } 8797 $passwordupper = PASSWORD_UPPER; 8798 while ($upper > strlen($passwordupper)) { 8799 $passwordupper .= PASSWORD_UPPER; 8800 } 8801 $passwordnonalphanum = PASSWORD_NONALPHANUM; 8802 while ($nonalphanum > strlen($passwordnonalphanum)) { 8803 $passwordnonalphanum .= PASSWORD_NONALPHANUM; 8804 } 8805 8806 // Now mix and shuffle it all. 8807 $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) . 8808 substr(str_shuffle ($passwordupper), 0, $upper) . 8809 substr(str_shuffle ($passworddigits), 0, $digits) . 8810 substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) . 8811 substr(str_shuffle ($passwordlower . 8812 $passwordupper . 8813 $passworddigits . 8814 $passwordnonalphanum), 0 , $additional)); 8815 } 8816 8817 return substr ($password, 0, $maxlen); 8818 } 8819 8820 /** 8821 * Given a float, prints it nicely. 8822 * Localized floats must not be used in calculations! 8823 * 8824 * The stripzeros feature is intended for making numbers look nicer in small 8825 * areas where it is not necessary to indicate the degree of accuracy by showing 8826 * ending zeros. If you turn it on with $decimalpoints set to 3, for example, 8827 * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'. 8828 * 8829 * @param float $float The float to print 8830 * @param int $decimalpoints The number of decimal places to print. -1 is a special value for auto detect (full precision). 8831 * @param bool $localized use localized decimal separator 8832 * @param bool $stripzeros If true, removes final zeros after decimal point. It will be ignored and the trailing zeros after 8833 * the decimal point are always striped if $decimalpoints is -1. 8834 * @return string locale float 8835 */ 8836 function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=false) { 8837 if (is_null($float)) { 8838 return ''; 8839 } 8840 if ($localized) { 8841 $separator = get_string('decsep', 'langconfig'); 8842 } else { 8843 $separator = '.'; 8844 } 8845 if ($decimalpoints == -1) { 8846 // The following counts the number of decimals. 8847 // It is safe as both floatval() and round() functions have same behaviour when non-numeric values are provided. 8848 $floatval = floatval($float); 8849 for ($decimalpoints = 0; $floatval != round($float, $decimalpoints); $decimalpoints++); 8850 } 8851 8852 $result = number_format($float, $decimalpoints, $separator, ''); 8853 if ($stripzeros && $decimalpoints > 0) { 8854 // Remove zeros and final dot if not needed. 8855 // However, only do this if there is a decimal point! 8856 $result = preg_replace('~(' . preg_quote($separator, '~') . ')?0+$~', '', $result); 8857 } 8858 return $result; 8859 } 8860 8861 /** 8862 * Converts locale specific floating point/comma number back to standard PHP float value 8863 * Do NOT try to do any math operations before this conversion on any user submitted floats! 8864 * 8865 * @param string $localefloat locale aware float representation 8866 * @param bool $strict If true, then check the input and return false if it is not a valid number. 8867 * @return mixed float|bool - false or the parsed float. 8868 */ 8869 function unformat_float($localefloat, $strict = false) { 8870 $localefloat = trim((string)$localefloat); 8871 8872 if ($localefloat == '') { 8873 return null; 8874 } 8875 8876 $localefloat = str_replace(' ', '', $localefloat); // No spaces - those might be used as thousand separators. 8877 $localefloat = str_replace(get_string('decsep', 'langconfig'), '.', $localefloat); 8878 8879 if ($strict && !is_numeric($localefloat)) { 8880 return false; 8881 } 8882 8883 return (float)$localefloat; 8884 } 8885 8886 /** 8887 * Given a simple array, this shuffles it up just like shuffle() 8888 * Unlike PHP's shuffle() this function works on any machine. 8889 * 8890 * @param array $array The array to be rearranged 8891 * @return array 8892 */ 8893 function swapshuffle($array) { 8894 8895 $last = count($array) - 1; 8896 for ($i = 0; $i <= $last; $i++) { 8897 $from = rand(0, $last); 8898 $curr = $array[$i]; 8899 $array[$i] = $array[$from]; 8900 $array[$from] = $curr; 8901 } 8902 return $array; 8903 } 8904 8905 /** 8906 * Like {@link swapshuffle()}, but works on associative arrays 8907 * 8908 * @param array $array The associative array to be rearranged 8909 * @return array 8910 */ 8911 function swapshuffle_assoc($array) { 8912 8913 $newarray = array(); 8914 $newkeys = swapshuffle(array_keys($array)); 8915 8916 foreach ($newkeys as $newkey) { 8917 $newarray[$newkey] = $array[$newkey]; 8918 } 8919 return $newarray; 8920 } 8921 8922 /** 8923 * Given an arbitrary array, and a number of draws, 8924 * this function returns an array with that amount 8925 * of items. The indexes are retained. 8926 * 8927 * @todo Finish documenting this function 8928 * 8929 * @param array $array 8930 * @param int $draws 8931 * @return array 8932 */ 8933 function draw_rand_array($array, $draws) { 8934 8935 $return = array(); 8936 8937 $last = count($array); 8938 8939 if ($draws > $last) { 8940 $draws = $last; 8941 } 8942 8943 while ($draws > 0) { 8944 $last--; 8945 8946 $keys = array_keys($array); 8947 $rand = rand(0, $last); 8948 8949 $return[$keys[$rand]] = $array[$keys[$rand]]; 8950 unset($array[$keys[$rand]]); 8951 8952 $draws--; 8953 } 8954 8955 return $return; 8956 } 8957 8958 /** 8959 * Calculate the difference between two microtimes 8960 * 8961 * @param string $a The first Microtime 8962 * @param string $b The second Microtime 8963 * @return string 8964 */ 8965 function microtime_diff($a, $b) { 8966 list($adec, $asec) = explode(' ', $a); 8967 list($bdec, $bsec) = explode(' ', $b); 8968 return $bsec - $asec + $bdec - $adec; 8969 } 8970 8971 /** 8972 * Given a list (eg a,b,c,d,e) this function returns 8973 * an array of 1->a, 2->b, 3->c etc 8974 * 8975 * @param string $list The string to explode into array bits 8976 * @param string $separator The separator used within the list string 8977 * @return array The now assembled array 8978 */ 8979 function make_menu_from_list($list, $separator=',') { 8980 8981 $array = array_reverse(explode($separator, $list), true); 8982 foreach ($array as $key => $item) { 8983 $outarray[$key+1] = trim($item); 8984 } 8985 return $outarray; 8986 } 8987 8988 /** 8989 * Creates an array that represents all the current grades that 8990 * can be chosen using the given grading type. 8991 * 8992 * Negative numbers 8993 * are scales, zero is no grade, and positive numbers are maximum 8994 * grades. 8995 * 8996 * @todo Finish documenting this function or better deprecated this completely! 8997 * 8998 * @param int $gradingtype 8999 * @return array 9000 */ 9001 function make_grades_menu($gradingtype) { 9002 global $DB; 9003 9004 $grades = array(); 9005 if ($gradingtype < 0) { 9006 if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) { 9007 return make_menu_from_list($scale->scale); 9008 } 9009 } else if ($gradingtype > 0) { 9010 for ($i=$gradingtype; $i>=0; $i--) { 9011 $grades[$i] = $i .' / '. $gradingtype; 9012 } 9013 return $grades; 9014 } 9015 return $grades; 9016 } 9017 9018 /** 9019 * make_unique_id_code 9020 * 9021 * @todo Finish documenting this function 9022 * 9023 * @uses $_SERVER 9024 * @param string $extra Extra string to append to the end of the code 9025 * @return string 9026 */ 9027 function make_unique_id_code($extra = '') { 9028 9029 $hostname = 'unknownhost'; 9030 if (!empty($_SERVER['HTTP_HOST'])) { 9031 $hostname = $_SERVER['HTTP_HOST']; 9032 } else if (!empty($_ENV['HTTP_HOST'])) { 9033 $hostname = $_ENV['HTTP_HOST']; 9034 } else if (!empty($_SERVER['SERVER_NAME'])) { 9035 $hostname = $_SERVER['SERVER_NAME']; 9036 } else if (!empty($_ENV['SERVER_NAME'])) { 9037 $hostname = $_ENV['SERVER_NAME']; 9038 } 9039 9040 $date = gmdate("ymdHis"); 9041 9042 $random = random_string(6); 9043 9044 if ($extra) { 9045 return $hostname .'+'. $date .'+'. $random .'+'. $extra; 9046 } else { 9047 return $hostname .'+'. $date .'+'. $random; 9048 } 9049 } 9050 9051 9052 /** 9053 * Function to check the passed address is within the passed subnet 9054 * 9055 * The parameter is a comma separated string of subnet definitions. 9056 * Subnet strings can be in one of three formats: 9057 * 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn (number of bits in net mask) 9058 * 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) 9059 * 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. (incomplete address, a bit non-technical ;-) 9060 * Code for type 1 modified from user posted comments by mediator at 9061 * {@link http://au.php.net/manual/en/function.ip2long.php} 9062 * 9063 * @param string $addr The address you are checking 9064 * @param string $subnetstr The string of subnet addresses 9065 * @param bool $checkallzeros The state to whether check for 0.0.0.0 9066 * @return bool 9067 */ 9068 function address_in_subnet($addr, $subnetstr, $checkallzeros = false) { 9069 9070 if ($addr == '0.0.0.0' && !$checkallzeros) { 9071 return false; 9072 } 9073 $subnets = explode(',', $subnetstr); 9074 $found = false; 9075 $addr = trim($addr); 9076 $addr = cleanremoteaddr($addr, false); // Normalise. 9077 if ($addr === null) { 9078 return false; 9079 } 9080 $addrparts = explode(':', $addr); 9081 9082 $ipv6 = strpos($addr, ':'); 9083 9084 foreach ($subnets as $subnet) { 9085 $subnet = trim($subnet); 9086 if ($subnet === '') { 9087 continue; 9088 } 9089 9090 if (strpos($subnet, '/') !== false) { 9091 // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn. 9092 list($ip, $mask) = explode('/', $subnet); 9093 $mask = trim($mask); 9094 if (!is_number($mask)) { 9095 continue; // Incorect mask number, eh? 9096 } 9097 $ip = cleanremoteaddr($ip, false); // Normalise. 9098 if ($ip === null) { 9099 continue; 9100 } 9101 if (strpos($ip, ':') !== false) { 9102 // IPv6. 9103 if (!$ipv6) { 9104 continue; 9105 } 9106 if ($mask > 128 or $mask < 0) { 9107 continue; // Nonsense. 9108 } 9109 if ($mask == 0) { 9110 return true; // Any address. 9111 } 9112 if ($mask == 128) { 9113 if ($ip === $addr) { 9114 return true; 9115 } 9116 continue; 9117 } 9118 $ipparts = explode(':', $ip); 9119 $modulo = $mask % 16; 9120 $ipnet = array_slice($ipparts, 0, ($mask-$modulo)/16); 9121 $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16); 9122 if (implode(':', $ipnet) === implode(':', $addrnet)) { 9123 if ($modulo == 0) { 9124 return true; 9125 } 9126 $pos = ($mask-$modulo)/16; 9127 $ipnet = hexdec($ipparts[$pos]); 9128 $addrnet = hexdec($addrparts[$pos]); 9129 $mask = 0xffff << (16 - $modulo); 9130 if (($addrnet & $mask) == ($ipnet & $mask)) { 9131 return true; 9132 } 9133 } 9134 9135 } else { 9136 // IPv4. 9137 if ($ipv6) { 9138 continue; 9139 } 9140 if ($mask > 32 or $mask < 0) { 9141 continue; // Nonsense. 9142 } 9143 if ($mask == 0) { 9144 return true; 9145 } 9146 if ($mask == 32) { 9147 if ($ip === $addr) { 9148 return true; 9149 } 9150 continue; 9151 } 9152 $mask = 0xffffffff << (32 - $mask); 9153 if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) { 9154 return true; 9155 } 9156 } 9157 9158 } else if (strpos($subnet, '-') !== false) { 9159 // 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. 9160 $parts = explode('-', $subnet); 9161 if (count($parts) != 2) { 9162 continue; 9163 } 9164 9165 if (strpos($subnet, ':') !== false) { 9166 // IPv6. 9167 if (!$ipv6) { 9168 continue; 9169 } 9170 $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise. 9171 if ($ipstart === null) { 9172 continue; 9173 } 9174 $ipparts = explode(':', $ipstart); 9175 $start = hexdec(array_pop($ipparts)); 9176 $ipparts[] = trim($parts[1]); 9177 $ipend = cleanremoteaddr(implode(':', $ipparts), false); // Normalise. 9178 if ($ipend === null) { 9179 continue; 9180 } 9181 $ipparts[7] = ''; 9182 $ipnet = implode(':', $ipparts); 9183 if (strpos($addr, $ipnet) !== 0) { 9184 continue; 9185 } 9186 $ipparts = explode(':', $ipend); 9187 $end = hexdec($ipparts[7]); 9188 9189 $addrend = hexdec($addrparts[7]); 9190 9191 if (($addrend >= $start) and ($addrend <= $end)) { 9192 return true; 9193 } 9194 9195 } else { 9196 // IPv4. 9197 if ($ipv6) { 9198 continue; 9199 } 9200 $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise. 9201 if ($ipstart === null) { 9202 continue; 9203 } 9204 $ipparts = explode('.', $ipstart); 9205 $ipparts[3] = trim($parts[1]); 9206 $ipend = cleanremoteaddr(implode('.', $ipparts), false); // Normalise. 9207 if ($ipend === null) { 9208 continue; 9209 } 9210 9211 if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) { 9212 return true; 9213 } 9214 } 9215 9216 } else { 9217 // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. 9218 if (strpos($subnet, ':') !== false) { 9219 // IPv6. 9220 if (!$ipv6) { 9221 continue; 9222 } 9223 $parts = explode(':', $subnet); 9224 $count = count($parts); 9225 if ($parts[$count-1] === '') { 9226 unset($parts[$count-1]); // Trim trailing :'s. 9227 $count--; 9228 $subnet = implode('.', $parts); 9229 } 9230 $isip = cleanremoteaddr($subnet, false); // Normalise. 9231 if ($isip !== null) { 9232 if ($isip === $addr) { 9233 return true; 9234 } 9235 continue; 9236 } else if ($count > 8) { 9237 continue; 9238 } 9239 $zeros = array_fill(0, 8-$count, '0'); 9240 $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16); 9241 if (address_in_subnet($addr, $subnet)) { 9242 return true; 9243 } 9244 9245 } else { 9246 // IPv4. 9247 if ($ipv6) { 9248 continue; 9249 } 9250 $parts = explode('.', $subnet); 9251 $count = count($parts); 9252 if ($parts[$count-1] === '') { 9253 unset($parts[$count-1]); // Trim trailing . 9254 $count--; 9255 $subnet = implode('.', $parts); 9256 } 9257 if ($count == 4) { 9258 $subnet = cleanremoteaddr($subnet, false); // Normalise. 9259 if ($subnet === $addr) { 9260 return true; 9261 } 9262 continue; 9263 } else if ($count > 4) { 9264 continue; 9265 } 9266 $zeros = array_fill(0, 4-$count, '0'); 9267 $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8); 9268 if (address_in_subnet($addr, $subnet)) { 9269 return true; 9270 } 9271 } 9272 } 9273 } 9274 9275 return false; 9276 } 9277 9278 /** 9279 * For outputting debugging info 9280 * 9281 * @param string $string The string to write 9282 * @param string $eol The end of line char(s) to use 9283 * @param string $sleep Period to make the application sleep 9284 * This ensures any messages have time to display before redirect 9285 */ 9286 function mtrace($string, $eol="\n", $sleep=0) { 9287 global $CFG; 9288 9289 if (isset($CFG->mtrace_wrapper) && function_exists($CFG->mtrace_wrapper)) { 9290 $fn = $CFG->mtrace_wrapper; 9291 $fn($string, $eol); 9292 return; 9293 } else if (defined('STDOUT') && !PHPUNIT_TEST && !defined('BEHAT_TEST')) { 9294 // We must explicitly call the add_line function here. 9295 // Uses of fwrite to STDOUT are not picked up by ob_start. 9296 if ($output = \core\task\logmanager::add_line("{$string}{$eol}")) { 9297 fwrite(STDOUT, $output); 9298 } 9299 } else { 9300 echo $string . $eol; 9301 } 9302 9303 // Flush again. 9304 flush(); 9305 9306 // Delay to keep message on user's screen in case of subsequent redirect. 9307 if ($sleep) { 9308 sleep($sleep); 9309 } 9310 } 9311 9312 /** 9313 * Helper to {@see mtrace()} an exception or throwable, including all relevant information. 9314 * 9315 * @param Throwable $e the error to ouptput. 9316 */ 9317 function mtrace_exception(Throwable $e): void { 9318 $info = get_exception_info($e); 9319 9320 $message = $info->message; 9321 if ($info->debuginfo) { 9322 $message .= "\n\n" . $info->debuginfo; 9323 } 9324 if ($info->backtrace) { 9325 $message .= "\n\n" . format_backtrace($info->backtrace, true); 9326 } 9327 9328 mtrace($message); 9329 } 9330 9331 /** 9332 * Replace 1 or more slashes or backslashes to 1 slash 9333 * 9334 * @param string $path The path to strip 9335 * @return string the path with double slashes removed 9336 */ 9337 function cleardoubleslashes ($path) { 9338 return preg_replace('/(\/|\\\){1,}/', '/', $path); 9339 } 9340 9341 /** 9342 * Is the current ip in a given list? 9343 * 9344 * @param string $list 9345 * @return bool 9346 */ 9347 function remoteip_in_list($list) { 9348 $clientip = getremoteaddr(null); 9349 9350 if (!$clientip) { 9351 // Ensure access on cli. 9352 return true; 9353 } 9354 return \core\ip_utils::is_ip_in_subnet_list($clientip, $list); 9355 } 9356 9357 /** 9358 * Returns most reliable client address 9359 * 9360 * @param string $default If an address can't be determined, then return this 9361 * @return string The remote IP address 9362 */ 9363 function getremoteaddr($default='0.0.0.0') { 9364 global $CFG; 9365 9366 if (!isset($CFG->getremoteaddrconf)) { 9367 // This will happen, for example, before just after the upgrade, as the 9368 // user is redirected to the admin screen. 9369 $variablestoskip = GETREMOTEADDR_SKIP_DEFAULT; 9370 } else { 9371 $variablestoskip = $CFG->getremoteaddrconf; 9372 } 9373 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) { 9374 if (!empty($_SERVER['HTTP_CLIENT_IP'])) { 9375 $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']); 9376 return $address ? $address : $default; 9377 } 9378 } 9379 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) { 9380 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { 9381 $forwardedaddresses = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']); 9382 9383 $forwardedaddresses = array_filter($forwardedaddresses, function($ip) { 9384 global $CFG; 9385 return !\core\ip_utils::is_ip_in_subnet_list($ip, $CFG->reverseproxyignore ?? '', ','); 9386 }); 9387 9388 // Multiple proxies can append values to this header including an 9389 // untrusted original request header so we must only trust the last ip. 9390 $address = end($forwardedaddresses); 9391 9392 if (substr_count($address, ":") > 1) { 9393 // Remove port and brackets from IPv6. 9394 if (preg_match("/\[(.*)\]:/", $address, $matches)) { 9395 $address = $matches[1]; 9396 } 9397 } else { 9398 // Remove port from IPv4. 9399 if (substr_count($address, ":") == 1) { 9400 $parts = explode(":", $address); 9401 $address = $parts[0]; 9402 } 9403 } 9404 9405 $address = cleanremoteaddr($address); 9406 return $address ? $address : $default; 9407 } 9408 } 9409 if (!empty($_SERVER['REMOTE_ADDR'])) { 9410 $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']); 9411 return $address ? $address : $default; 9412 } else { 9413 return $default; 9414 } 9415 } 9416 9417 /** 9418 * Cleans an ip address. Internal addresses are now allowed. 9419 * (Originally local addresses were not allowed.) 9420 * 9421 * @param string $addr IPv4 or IPv6 address 9422 * @param bool $compress use IPv6 address compression 9423 * @return string normalised ip address string, null if error 9424 */ 9425 function cleanremoteaddr($addr, $compress=false) { 9426 $addr = trim($addr); 9427 9428 if (strpos($addr, ':') !== false) { 9429 // Can be only IPv6. 9430 $parts = explode(':', $addr); 9431 $count = count($parts); 9432 9433 if (strpos($parts[$count-1], '.') !== false) { 9434 // Legacy ipv4 notation. 9435 $last = array_pop($parts); 9436 $ipv4 = cleanremoteaddr($last, true); 9437 if ($ipv4 === null) { 9438 return null; 9439 } 9440 $bits = explode('.', $ipv4); 9441 $parts[] = dechex($bits[0]).dechex($bits[1]); 9442 $parts[] = dechex($bits[2]).dechex($bits[3]); 9443 $count = count($parts); 9444 $addr = implode(':', $parts); 9445 } 9446 9447 if ($count < 3 or $count > 8) { 9448 return null; // Severly malformed. 9449 } 9450 9451 if ($count != 8) { 9452 if (strpos($addr, '::') === false) { 9453 return null; // Malformed. 9454 } 9455 // Uncompress. 9456 $insertat = array_search('', $parts, true); 9457 $missing = array_fill(0, 1 + 8 - $count, '0'); 9458 array_splice($parts, $insertat, 1, $missing); 9459 foreach ($parts as $key => $part) { 9460 if ($part === '') { 9461 $parts[$key] = '0'; 9462 } 9463 } 9464 } 9465 9466 $adr = implode(':', $parts); 9467 if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) { 9468 return null; // Incorrect format - sorry. 9469 } 9470 9471 // Normalise 0s and case. 9472 $parts = array_map('hexdec', $parts); 9473 $parts = array_map('dechex', $parts); 9474 9475 $result = implode(':', $parts); 9476 9477 if (!$compress) { 9478 return $result; 9479 } 9480 9481 if ($result === '0:0:0:0:0:0:0:0') { 9482 return '::'; // All addresses. 9483 } 9484 9485 $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1); 9486 if ($compressed !== $result) { 9487 return $compressed; 9488 } 9489 9490 $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1); 9491 if ($compressed !== $result) { 9492 return $compressed; 9493 } 9494 9495 $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1); 9496 if ($compressed !== $result) { 9497 return $compressed; 9498 } 9499 9500 return $result; 9501 } 9502 9503 // First get all things that look like IPv4 addresses. 9504 $parts = array(); 9505 if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) { 9506 return null; 9507 } 9508 unset($parts[0]); 9509 9510 foreach ($parts as $key => $match) { 9511 if ($match > 255) { 9512 return null; 9513 } 9514 $parts[$key] = (int)$match; // Normalise 0s. 9515 } 9516 9517 return implode('.', $parts); 9518 } 9519 9520 9521 /** 9522 * Is IP address a public address? 9523 * 9524 * @param string $ip The ip to check 9525 * @return bool true if the ip is public 9526 */ 9527 function ip_is_public($ip) { 9528 return (bool) filter_var($ip, FILTER_VALIDATE_IP, (FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)); 9529 } 9530 9531 /** 9532 * This function will make a complete copy of anything it's given, 9533 * regardless of whether it's an object or not. 9534 * 9535 * @param mixed $thing Something you want cloned 9536 * @return mixed What ever it is you passed it 9537 */ 9538 function fullclone($thing) { 9539 return unserialize(serialize($thing)); 9540 } 9541 9542 /** 9543 * Used to make sure that $min <= $value <= $max 9544 * 9545 * Make sure that value is between min, and max 9546 * 9547 * @param int $min The minimum value 9548 * @param int $value The value to check 9549 * @param int $max The maximum value 9550 * @return int 9551 */ 9552 function bounded_number($min, $value, $max) { 9553 if ($value < $min) { 9554 return $min; 9555 } 9556 if ($value > $max) { 9557 return $max; 9558 } 9559 return $value; 9560 } 9561 9562 /** 9563 * Check if there is a nested array within the passed array 9564 * 9565 * @param array $array 9566 * @return bool true if there is a nested array false otherwise 9567 */ 9568 function array_is_nested($array) { 9569 foreach ($array as $value) { 9570 if (is_array($value)) { 9571 return true; 9572 } 9573 } 9574 return false; 9575 } 9576 9577 /** 9578 * get_performance_info() pairs up with init_performance_info() 9579 * loaded in setup.php. Returns an array with 'html' and 'txt' 9580 * values ready for use, and each of the individual stats provided 9581 * separately as well. 9582 * 9583 * @return array 9584 */ 9585 function get_performance_info() { 9586 global $CFG, $PERF, $DB, $PAGE; 9587 9588 $info = array(); 9589 $info['txt'] = me() . ' '; // Holds log-friendly representation. 9590 9591 $info['html'] = ''; 9592 if (!empty($CFG->themedesignermode)) { 9593 // Attempt to avoid devs debugging peformance issues, when its caused by css building and so on. 9594 $info['html'] .= '<p><strong>Warning: Theme designer mode is enabled.</strong></p>'; 9595 } 9596 $info['html'] .= '<ul class="list-unstyled row mx-md-0">'; // Holds userfriendly HTML representation. 9597 9598 $info['realtime'] = microtime_diff($PERF->starttime, microtime()); 9599 9600 $info['html'] .= '<li class="timeused col-sm-4">'.$info['realtime'].' secs</li> '; 9601 $info['txt'] .= 'time: '.$info['realtime'].'s '; 9602 9603 // GET/POST (or NULL if $_SERVER['REQUEST_METHOD'] is undefined) is useful for txt logged information. 9604 $info['txt'] .= 'method: ' . ($_SERVER['REQUEST_METHOD'] ?? "NULL") . ' '; 9605 9606 if (function_exists('memory_get_usage')) { 9607 $info['memory_total'] = memory_get_usage(); 9608 $info['memory_growth'] = memory_get_usage() - $PERF->startmemory; 9609 $info['html'] .= '<li class="memoryused col-sm-4">RAM: '.display_size($info['memory_total']).'</li> '; 9610 $info['txt'] .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '. 9611 $info['memory_growth'].'B ('.display_size($info['memory_growth']).') '; 9612 } 9613 9614 if (function_exists('memory_get_peak_usage')) { 9615 $info['memory_peak'] = memory_get_peak_usage(); 9616 $info['html'] .= '<li class="memoryused col-sm-4">RAM peak: '.display_size($info['memory_peak']).'</li> '; 9617 $info['txt'] .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') '; 9618 } 9619 9620 $info['html'] .= '</ul><ul class="list-unstyled row mx-md-0">'; 9621 $inc = get_included_files(); 9622 $info['includecount'] = count($inc); 9623 $info['html'] .= '<li class="included col-sm-4">Included '.$info['includecount'].' files</li> '; 9624 $info['txt'] .= 'includecount: '.$info['includecount'].' '; 9625 9626 if (!empty($CFG->early_install_lang) or empty($PAGE)) { 9627 // We can not track more performance before installation or before PAGE init, sorry. 9628 return $info; 9629 } 9630 9631 $filtermanager = filter_manager::instance(); 9632 if (method_exists($filtermanager, 'get_performance_summary')) { 9633 list($filterinfo, $nicenames) = $filtermanager->get_performance_summary(); 9634 $info = array_merge($filterinfo, $info); 9635 foreach ($filterinfo as $key => $value) { 9636 $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> "; 9637 $info['txt'] .= "$key: $value "; 9638 } 9639 } 9640 9641 $stringmanager = get_string_manager(); 9642 if (method_exists($stringmanager, 'get_performance_summary')) { 9643 list($filterinfo, $nicenames) = $stringmanager->get_performance_summary(); 9644 $info = array_merge($filterinfo, $info); 9645 foreach ($filterinfo as $key => $value) { 9646 $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> "; 9647 $info['txt'] .= "$key: $value "; 9648 } 9649 } 9650 9651 if (!empty($PERF->logwrites)) { 9652 $info['logwrites'] = $PERF->logwrites; 9653 $info['html'] .= '<li class="logwrites col-sm-4">Log DB writes '.$info['logwrites'].'</li> '; 9654 $info['txt'] .= 'logwrites: '.$info['logwrites'].' '; 9655 } 9656 9657 $info['dbqueries'] = $DB->perf_get_reads().'/'.($DB->perf_get_writes() - $PERF->logwrites); 9658 $info['html'] .= '<li class="dbqueries col-sm-4">DB reads/writes: '.$info['dbqueries'].'</li> '; 9659 $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' '; 9660 9661 if ($DB->want_read_slave()) { 9662 $info['dbreads_slave'] = $DB->perf_get_reads_slave(); 9663 $info['html'] .= '<li class="dbqueries col-sm-4">DB reads from slave: '.$info['dbreads_slave'].'</li> '; 9664 $info['txt'] .= 'db reads from slave: '.$info['dbreads_slave'].' '; 9665 } 9666 9667 $info['dbtime'] = round($DB->perf_get_queries_time(), 5); 9668 $info['html'] .= '<li class="dbtime col-sm-4">DB queries time: '.$info['dbtime'].' secs</li> '; 9669 $info['txt'] .= 'db queries time: ' . $info['dbtime'] . 's '; 9670 9671 if (function_exists('posix_times')) { 9672 $ptimes = posix_times(); 9673 if (is_array($ptimes)) { 9674 foreach ($ptimes as $key => $val) { 9675 $info[$key] = $ptimes[$key] - $PERF->startposixtimes[$key]; 9676 } 9677 $info['html'] .= "<li class=\"posixtimes col-sm-4\">ticks: $info[ticks] user: $info[utime]"; 9678 $info['html'] .= "sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</li> "; 9679 $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] "; 9680 } 9681 } 9682 9683 // Grab the load average for the last minute. 9684 // /proc will only work under some linux configurations 9685 // while uptime is there under MacOSX/Darwin and other unices. 9686 if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) { 9687 list($serverload) = explode(' ', $loadavg[0]); 9688 unset($loadavg); 9689 } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) { 9690 if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) { 9691 $serverload = $matches[1]; 9692 } else { 9693 trigger_error('Could not parse uptime output!'); 9694 } 9695 } 9696 if (!empty($serverload)) { 9697 $info['serverload'] = $serverload; 9698 $info['html'] .= '<li class="serverload col-sm-4">Load average: '.$info['serverload'].'</li> '; 9699 $info['txt'] .= "serverload: {$info['serverload']} "; 9700 } 9701 9702 // Display size of session if session started. 9703 if ($si = \core\session\manager::get_performance_info()) { 9704 $info['sessionsize'] = $si['size']; 9705 $info['html'] .= "<li class=\"serverload col-sm-4\">" . $si['html'] . "</li>"; 9706 $info['txt'] .= $si['txt']; 9707 } 9708 9709 // Display time waiting for session if applicable. 9710 if (!empty($PERF->sessionlock['wait'])) { 9711 $sessionwait = number_format($PERF->sessionlock['wait'], 3) . ' secs'; 9712 $info['html'] .= html_writer::tag('li', 'Session wait: ' . $sessionwait, [ 9713 'class' => 'sessionwait col-sm-4' 9714 ]); 9715 $info['txt'] .= 'sessionwait: ' . $sessionwait . ' '; 9716 } 9717 9718 $info['html'] .= '</ul>'; 9719 $html = ''; 9720 if ($stats = cache_helper::get_stats()) { 9721 9722 $table = new html_table(); 9723 $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered'; 9724 $table->head = ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S', 'I/O']; 9725 $table->data = []; 9726 $table->align = ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right', 'right']; 9727 9728 $text = 'Caches used (hits/misses/sets): '; 9729 $hits = 0; 9730 $misses = 0; 9731 $sets = 0; 9732 $maxstores = 0; 9733 9734 // We want to align static caches into their own column. 9735 $hasstatic = false; 9736 foreach ($stats as $definition => $details) { 9737 $numstores = count($details['stores']); 9738 $first = key($details['stores']); 9739 if ($first !== cache_store::STATIC_ACCEL) { 9740 $numstores++; // Add a blank space for the missing static store. 9741 } 9742 $maxstores = max($maxstores, $numstores); 9743 } 9744 9745 $storec = 0; 9746 9747 while ($storec++ < ($maxstores - 2)) { 9748 if ($storec == ($maxstores - 2)) { 9749 $table->head[] = get_string('mappingfinal', 'cache'); 9750 } else { 9751 $table->head[] = "Store $storec"; 9752 } 9753 $table->align[] = 'left'; 9754 $table->align[] = 'right'; 9755 $table->align[] = 'right'; 9756 $table->align[] = 'right'; 9757 $table->align[] = 'right'; 9758 $table->head[] = 'H'; 9759 $table->head[] = 'M'; 9760 $table->head[] = 'S'; 9761 $table->head[] = 'I/O'; 9762 } 9763 9764 ksort($stats); 9765 9766 foreach ($stats as $definition => $details) { 9767 switch ($details['mode']) { 9768 case cache_store::MODE_APPLICATION: 9769 $modeclass = 'application'; 9770 $mode = ' <span title="application cache">App</span>'; 9771 break; 9772 case cache_store::MODE_SESSION: 9773 $modeclass = 'session'; 9774 $mode = ' <span title="session cache">Ses</span>'; 9775 break; 9776 case cache_store::MODE_REQUEST: 9777 $modeclass = 'request'; 9778 $mode = ' <span title="request cache">Req</span>'; 9779 break; 9780 } 9781 $row = [$mode, $definition]; 9782 9783 $text .= "$definition {"; 9784 9785 $storec = 0; 9786 foreach ($details['stores'] as $store => $data) { 9787 9788 if ($storec == 0 && $store !== cache_store::STATIC_ACCEL) { 9789 $row[] = ''; 9790 $row[] = ''; 9791 $row[] = ''; 9792 $storec++; 9793 } 9794 9795 $hits += $data['hits']; 9796 $misses += $data['misses']; 9797 $sets += $data['sets']; 9798 if ($data['hits'] == 0 and $data['misses'] > 0) { 9799 $cachestoreclass = 'nohits bg-danger'; 9800 } else if ($data['hits'] < $data['misses']) { 9801 $cachestoreclass = 'lowhits bg-warning text-dark'; 9802 } else { 9803 $cachestoreclass = 'hihits'; 9804 } 9805 $text .= "$store($data[hits]/$data[misses]/$data[sets]) "; 9806 $cell = new html_table_cell($store); 9807 $cell->attributes = ['class' => $cachestoreclass]; 9808 $row[] = $cell; 9809 $cell = new html_table_cell($data['hits']); 9810 $cell->attributes = ['class' => $cachestoreclass]; 9811 $row[] = $cell; 9812 $cell = new html_table_cell($data['misses']); 9813 $cell->attributes = ['class' => $cachestoreclass]; 9814 $row[] = $cell; 9815 9816 if ($store !== cache_store::STATIC_ACCEL) { 9817 // The static cache is never set. 9818 $cell = new html_table_cell($data['sets']); 9819 $cell->attributes = ['class' => $cachestoreclass]; 9820 $row[] = $cell; 9821 9822 if ($data['hits'] || $data['sets']) { 9823 if ($data['iobytes'] === cache_store::IO_BYTES_NOT_SUPPORTED) { 9824 $size = '-'; 9825 } else { 9826 $size = display_size($data['iobytes'], 1, 'KB'); 9827 if ($data['iobytes'] >= 10 * 1024) { 9828 $cachestoreclass = ' bg-warning text-dark'; 9829 } 9830 } 9831 } else { 9832 $size = ''; 9833 } 9834 $cell = new html_table_cell($size); 9835 $cell->attributes = ['class' => $cachestoreclass]; 9836 $row[] = $cell; 9837 } 9838 $storec++; 9839 } 9840 while ($storec++ < $maxstores) { 9841 $row[] = ''; 9842 $row[] = ''; 9843 $row[] = ''; 9844 $row[] = ''; 9845 $row[] = ''; 9846 } 9847 $text .= '} '; 9848 9849 $table->data[] = $row; 9850 } 9851 9852 $html .= html_writer::table($table); 9853 9854 // Now lets also show sub totals for each cache store. 9855 $storetotals = []; 9856 $storetotal = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0]; 9857 foreach ($stats as $definition => $details) { 9858 foreach ($details['stores'] as $store => $data) { 9859 if (!array_key_exists($store, $storetotals)) { 9860 $storetotals[$store] = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0]; 9861 } 9862 $storetotals[$store]['class'] = $data['class']; 9863 $storetotals[$store]['hits'] += $data['hits']; 9864 $storetotals[$store]['misses'] += $data['misses']; 9865 $storetotals[$store]['sets'] += $data['sets']; 9866 $storetotal['hits'] += $data['hits']; 9867 $storetotal['misses'] += $data['misses']; 9868 $storetotal['sets'] += $data['sets']; 9869 if ($data['iobytes'] !== cache_store::IO_BYTES_NOT_SUPPORTED) { 9870 $storetotals[$store]['iobytes'] += $data['iobytes']; 9871 $storetotal['iobytes'] += $data['iobytes']; 9872 } 9873 } 9874 } 9875 9876 $table = new html_table(); 9877 $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered'; 9878 $table->head = [get_string('storename', 'cache'), get_string('type_cachestore', 'plugin'), 'H', 'M', 'S', 'I/O']; 9879 $table->data = []; 9880 $table->align = ['left', 'left', 'right', 'right', 'right', 'right']; 9881 9882 ksort($storetotals); 9883 9884 foreach ($storetotals as $store => $data) { 9885 $row = []; 9886 if ($data['hits'] == 0 and $data['misses'] > 0) { 9887 $cachestoreclass = 'nohits bg-danger'; 9888 } else if ($data['hits'] < $data['misses']) { 9889 $cachestoreclass = 'lowhits bg-warning text-dark'; 9890 } else { 9891 $cachestoreclass = 'hihits'; 9892 } 9893 $cell = new html_table_cell($store); 9894 $cell->attributes = ['class' => $cachestoreclass]; 9895 $row[] = $cell; 9896 $cell = new html_table_cell($data['class']); 9897 $cell->attributes = ['class' => $cachestoreclass]; 9898 $row[] = $cell; 9899 $cell = new html_table_cell($data['hits']); 9900 $cell->attributes = ['class' => $cachestoreclass]; 9901 $row[] = $cell; 9902 $cell = new html_table_cell($data['misses']); 9903 $cell->attributes = ['class' => $cachestoreclass]; 9904 $row[] = $cell; 9905 $cell = new html_table_cell($data['sets']); 9906 $cell->attributes = ['class' => $cachestoreclass]; 9907 $row[] = $cell; 9908 if ($data['hits'] || $data['sets']) { 9909 if ($data['iobytes']) { 9910 $size = display_size($data['iobytes'], 1, 'KB'); 9911 } else { 9912 $size = '-'; 9913 } 9914 } else { 9915 $size = ''; 9916 } 9917 $cell = new html_table_cell($size); 9918 $cell->attributes = ['class' => $cachestoreclass]; 9919 $row[] = $cell; 9920 $table->data[] = $row; 9921 } 9922 if (!empty($storetotal['iobytes'])) { 9923 $size = display_size($storetotal['iobytes'], 1, 'KB'); 9924 } else if (!empty($storetotal['hits']) || !empty($storetotal['sets'])) { 9925 $size = '-'; 9926 } else { 9927 $size = ''; 9928 } 9929 $row = [ 9930 get_string('total'), 9931 '', 9932 $storetotal['hits'], 9933 $storetotal['misses'], 9934 $storetotal['sets'], 9935 $size, 9936 ]; 9937 $table->data[] = $row; 9938 9939 $html .= html_writer::table($table); 9940 9941 $info['cachesused'] = "$hits / $misses / $sets"; 9942 $info['html'] .= $html; 9943 $info['txt'] .= $text.'. '; 9944 } else { 9945 $info['cachesused'] = '0 / 0 / 0'; 9946 $info['html'] .= '<div class="cachesused">Caches used (hits/misses/sets): 0/0/0</div>'; 9947 $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 '; 9948 } 9949 9950 // Display lock information if any. 9951 if (!empty($PERF->locks)) { 9952 $table = new html_table(); 9953 $table->attributes['class'] = 'locktimings table table-dark table-sm w-auto table-bordered'; 9954 $table->head = ['Lock', 'Waited (s)', 'Obtained', 'Held for (s)']; 9955 $table->align = ['left', 'right', 'center', 'right']; 9956 $table->data = []; 9957 $text = 'Locks (waited/obtained/held):'; 9958 foreach ($PERF->locks as $locktiming) { 9959 $row = []; 9960 $row[] = s($locktiming->type . '/' . $locktiming->resource); 9961 $text .= ' ' . $locktiming->type . '/' . $locktiming->resource . ' ('; 9962 9963 // The time we had to wait to get the lock. 9964 $roundedtime = number_format($locktiming->wait, 1); 9965 $cell = new html_table_cell($roundedtime); 9966 if ($locktiming->wait > 0.5) { 9967 $cell->attributes = ['class' => 'bg-warning text-dark']; 9968 } 9969 $row[] = $cell; 9970 $text .= $roundedtime . '/'; 9971 9972 // Show a tick or cross for success. 9973 $row[] = $locktiming->success ? '✓' : '❌'; 9974 $text .= ($locktiming->success ? 'y' : 'n') . '/'; 9975 9976 // If applicable, show how long we held the lock before releasing it. 9977 if (property_exists($locktiming, 'held')) { 9978 $roundedtime = number_format($locktiming->held, 1); 9979 $cell = new html_table_cell($roundedtime); 9980 if ($locktiming->held > 0.5) { 9981 $cell->attributes = ['class' => 'bg-warning text-dark']; 9982 } 9983 $row[] = $cell; 9984 $text .= $roundedtime; 9985 } else { 9986 $row[] = '-'; 9987 $text .= '-'; 9988 } 9989 $text .= ')'; 9990 9991 $table->data[] = $row; 9992 } 9993 $info['html'] .= html_writer::table($table); 9994 $info['txt'] .= $text . '. '; 9995 } 9996 9997 $info['html'] = '<div class="performanceinfo siteinfo container-fluid px-md-0 overflow-auto pt-3">'.$info['html'].'</div>'; 9998 return $info; 9999 } 10000 10001 /** 10002 * Renames a file or directory to a unique name within the same directory. 10003 * 10004 * This function is designed to avoid any potential race conditions, and select an unused name. 10005 * 10006 * @param string $filepath Original filepath 10007 * @param string $prefix Prefix to use for the temporary name 10008 * @return string|bool New file path or false if failed 10009 * @since Moodle 3.10 10010 */ 10011 function rename_to_unused_name(string $filepath, string $prefix = '_temp_') { 10012 $dir = dirname($filepath); 10013 $basename = $dir . '/' . $prefix; 10014 $limit = 0; 10015 while ($limit < 100) { 10016 // Select a new name based on a random number. 10017 $newfilepath = $basename . md5(mt_rand()); 10018 10019 // Attempt a rename to that new name. 10020 if (@rename($filepath, $newfilepath)) { 10021 return $newfilepath; 10022 } 10023 10024 // The first time, do some sanity checks, maybe it is failing for a good reason and there 10025 // is no point trying 100 times if so. 10026 if ($limit === 0 && (!file_exists($filepath) || !is_writable($dir))) { 10027 return false; 10028 } 10029 $limit++; 10030 } 10031 return false; 10032 } 10033 10034 /** 10035 * Delete directory or only its content 10036 * 10037 * @param string $dir directory path 10038 * @param bool $contentonly 10039 * @return bool success, true also if dir does not exist 10040 */ 10041 function remove_dir($dir, $contentonly=false) { 10042 if (!is_dir($dir)) { 10043 // Nothing to do. 10044 return true; 10045 } 10046 10047 if (!$contentonly) { 10048 // Start by renaming the directory; this will guarantee that other processes don't write to it 10049 // while it is in the process of being deleted. 10050 $tempdir = rename_to_unused_name($dir); 10051 if ($tempdir) { 10052 // If the rename was successful then delete the $tempdir instead. 10053 $dir = $tempdir; 10054 } 10055 // If the rename fails, we will continue through and attempt to delete the directory 10056 // without renaming it since that is likely to at least delete most of the files. 10057 } 10058 10059 if (!$handle = opendir($dir)) { 10060 return false; 10061 } 10062 $result = true; 10063 while (false!==($item = readdir($handle))) { 10064 if ($item != '.' && $item != '..') { 10065 if (is_dir($dir.'/'.$item)) { 10066 $result = remove_dir($dir.'/'.$item) && $result; 10067 } else { 10068 $result = unlink($dir.'/'.$item) && $result; 10069 } 10070 } 10071 } 10072 closedir($handle); 10073 if ($contentonly) { 10074 clearstatcache(); // Make sure file stat cache is properly invalidated. 10075 return $result; 10076 } 10077 $result = rmdir($dir); // If anything left the result will be false, no need for && $result. 10078 clearstatcache(); // Make sure file stat cache is properly invalidated. 10079 return $result; 10080 } 10081 10082 /** 10083 * Detect if an object or a class contains a given property 10084 * will take an actual object or the name of a class 10085 * 10086 * @param mix $obj Name of class or real object to test 10087 * @param string $property name of property to find 10088 * @return bool true if property exists 10089 */ 10090 function object_property_exists( $obj, $property ) { 10091 if (is_string( $obj )) { 10092 $properties = get_class_vars( $obj ); 10093 } else { 10094 $properties = get_object_vars( $obj ); 10095 } 10096 return array_key_exists( $property, $properties ); 10097 } 10098 10099 /** 10100 * Converts an object into an associative array 10101 * 10102 * This function converts an object into an associative array by iterating 10103 * over its public properties. Because this function uses the foreach 10104 * construct, Iterators are respected. It works recursively on arrays of objects. 10105 * Arrays and simple values are returned as is. 10106 * 10107 * If class has magic properties, it can implement IteratorAggregate 10108 * and return all available properties in getIterator() 10109 * 10110 * @param mixed $var 10111 * @return array 10112 */ 10113 function convert_to_array($var) { 10114 $result = array(); 10115 10116 // Loop over elements/properties. 10117 foreach ($var as $key => $value) { 10118 // Recursively convert objects. 10119 if (is_object($value) || is_array($value)) { 10120 $result[$key] = convert_to_array($value); 10121 } else { 10122 // Simple values are untouched. 10123 $result[$key] = $value; 10124 } 10125 } 10126 return $result; 10127 } 10128 10129 /** 10130 * Detect a custom script replacement in the data directory that will 10131 * replace an existing moodle script 10132 * 10133 * @return string|bool full path name if a custom script exists, false if no custom script exists 10134 */ 10135 function custom_script_path() { 10136 global $CFG, $SCRIPT; 10137 10138 if ($SCRIPT === null) { 10139 // Probably some weird external script. 10140 return false; 10141 } 10142 10143 $scriptpath = $CFG->customscripts . $SCRIPT; 10144 10145 // Check the custom script exists. 10146 if (file_exists($scriptpath) and is_file($scriptpath)) { 10147 return $scriptpath; 10148 } else { 10149 return false; 10150 } 10151 } 10152 10153 /** 10154 * Returns whether or not the user object is a remote MNET user. This function 10155 * is in moodlelib because it does not rely on loading any of the MNET code. 10156 * 10157 * @param object $user A valid user object 10158 * @return bool True if the user is from a remote Moodle. 10159 */ 10160 function is_mnet_remote_user($user) { 10161 global $CFG; 10162 10163 if (!isset($CFG->mnet_localhost_id)) { 10164 include_once($CFG->dirroot . '/mnet/lib.php'); 10165 $env = new mnet_environment(); 10166 $env->init(); 10167 unset($env); 10168 } 10169 10170 return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id); 10171 } 10172 10173 /** 10174 * This function will search for browser prefereed languages, setting Moodle 10175 * to use the best one available if $SESSION->lang is undefined 10176 */ 10177 function setup_lang_from_browser() { 10178 global $CFG, $SESSION, $USER; 10179 10180 if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) { 10181 // Lang is defined in session or user profile, nothing to do. 10182 return; 10183 } 10184 10185 if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do. 10186 return; 10187 } 10188 10189 // Extract and clean langs from headers. 10190 $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE']; 10191 $rawlangs = str_replace('-', '_', $rawlangs); // We are using underscores. 10192 $rawlangs = explode(',', $rawlangs); // Convert to array. 10193 $langs = array(); 10194 10195 $order = 1.0; 10196 foreach ($rawlangs as $lang) { 10197 if (strpos($lang, ';') === false) { 10198 $langs[(string)$order] = $lang; 10199 $order = $order-0.01; 10200 } else { 10201 $parts = explode(';', $lang); 10202 $pos = strpos($parts[1], '='); 10203 $langs[substr($parts[1], $pos+1)] = $parts[0]; 10204 } 10205 } 10206 krsort($langs, SORT_NUMERIC); 10207 10208 // Look for such langs under standard locations. 10209 foreach ($langs as $lang) { 10210 // Clean it properly for include. 10211 $lang = strtolower(clean_param($lang, PARAM_SAFEDIR)); 10212 if (get_string_manager()->translation_exists($lang, false)) { 10213 // If the translation for this language exists then try to set it 10214 // for the rest of the session, if this is a read only session then 10215 // we can only set it temporarily in $CFG. 10216 if (defined('READ_ONLY_SESSION') && !empty($CFG->enable_read_only_sessions)) { 10217 $CFG->lang = $lang; 10218 } else { 10219 $SESSION->lang = $lang; 10220 } 10221 // We have finished. Go out. 10222 break; 10223 } 10224 } 10225 return; 10226 } 10227 10228 /** 10229 * Check if $url matches anything in proxybypass list 10230 * 10231 * Any errors just result in the proxy being used (least bad) 10232 * 10233 * @param string $url url to check 10234 * @return boolean true if we should bypass the proxy 10235 */ 10236 function is_proxybypass( $url ) { 10237 global $CFG; 10238 10239 // Sanity check. 10240 if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) { 10241 return false; 10242 } 10243 10244 // Get the host part out of the url. 10245 if (!$host = parse_url( $url, PHP_URL_HOST )) { 10246 return false; 10247 } 10248 10249 // Get the possible bypass hosts into an array. 10250 $matches = explode( ',', $CFG->proxybypass ); 10251 10252 // Check for a exact match on the IP or in the domains. 10253 $isdomaininallowedlist = \core\ip_utils::is_domain_in_allowed_list($host, $matches); 10254 $isipinsubnetlist = \core\ip_utils::is_ip_in_subnet_list($host, $CFG->proxybypass, ','); 10255 10256 if ($isdomaininallowedlist || $isipinsubnetlist) { 10257 return true; 10258 } 10259 10260 // Nothing matched. 10261 return false; 10262 } 10263 10264 /** 10265 * Check if the passed navigation is of the new style 10266 * 10267 * @param mixed $navigation 10268 * @return bool true for yes false for no 10269 */ 10270 function is_newnav($navigation) { 10271 if (is_array($navigation) && !empty($navigation['newnav'])) { 10272 return true; 10273 } else { 10274 return false; 10275 } 10276 } 10277 10278 /** 10279 * Checks whether the given variable name is defined as a variable within the given object. 10280 * 10281 * This will NOT work with stdClass objects, which have no class variables. 10282 * 10283 * @param string $var The variable name 10284 * @param object $object The object to check 10285 * @return boolean 10286 */ 10287 function in_object_vars($var, $object) { 10288 $classvars = get_class_vars(get_class($object)); 10289 $classvars = array_keys($classvars); 10290 return in_array($var, $classvars); 10291 } 10292 10293 /** 10294 * Returns an array without repeated objects. 10295 * This function is similar to array_unique, but for arrays that have objects as values 10296 * 10297 * @param array $array 10298 * @param bool $keepkeyassoc 10299 * @return array 10300 */ 10301 function object_array_unique($array, $keepkeyassoc = true) { 10302 $duplicatekeys = array(); 10303 $tmp = array(); 10304 10305 foreach ($array as $key => $val) { 10306 // Convert objects to arrays, in_array() does not support objects. 10307 if (is_object($val)) { 10308 $val = (array)$val; 10309 } 10310 10311 if (!in_array($val, $tmp)) { 10312 $tmp[] = $val; 10313 } else { 10314 $duplicatekeys[] = $key; 10315 } 10316 } 10317 10318 foreach ($duplicatekeys as $key) { 10319 unset($array[$key]); 10320 } 10321 10322 return $keepkeyassoc ? $array : array_values($array); 10323 } 10324 10325 /** 10326 * Is a userid the primary administrator? 10327 * 10328 * @param int $userid int id of user to check 10329 * @return boolean 10330 */ 10331 function is_primary_admin($userid) { 10332 $primaryadmin = get_admin(); 10333 10334 if ($userid == $primaryadmin->id) { 10335 return true; 10336 } else { 10337 return false; 10338 } 10339 } 10340 10341 /** 10342 * Returns the site identifier 10343 * 10344 * @return string $CFG->siteidentifier, first making sure it is properly initialised. 10345 */ 10346 function get_site_identifier() { 10347 global $CFG; 10348 // Check to see if it is missing. If so, initialise it. 10349 if (empty($CFG->siteidentifier)) { 10350 set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']); 10351 } 10352 // Return it. 10353 return $CFG->siteidentifier; 10354 } 10355 10356 /** 10357 * Check whether the given password has no more than the specified 10358 * number of consecutive identical characters. 10359 * 10360 * @param string $password password to be checked against the password policy 10361 * @param integer $maxchars maximum number of consecutive identical characters 10362 * @return bool 10363 */ 10364 function check_consecutive_identical_characters($password, $maxchars) { 10365 10366 if ($maxchars < 1) { 10367 return true; // Zero 0 is to disable this check. 10368 } 10369 if (strlen($password) <= $maxchars) { 10370 return true; // Too short to fail this test. 10371 } 10372 10373 $previouschar = ''; 10374 $consecutivecount = 1; 10375 foreach (str_split($password) as $char) { 10376 if ($char != $previouschar) { 10377 $consecutivecount = 1; 10378 } else { 10379 $consecutivecount++; 10380 if ($consecutivecount > $maxchars) { 10381 return false; // Check failed already. 10382 } 10383 } 10384 10385 $previouschar = $char; 10386 } 10387 10388 return true; 10389 } 10390 10391 /** 10392 * Helper function to do partial function binding. 10393 * so we can use it for preg_replace_callback, for example 10394 * this works with php functions, user functions, static methods and class methods 10395 * it returns you a callback that you can pass on like so: 10396 * 10397 * $callback = partial('somefunction', $arg1, $arg2); 10398 * or 10399 * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2); 10400 * or even 10401 * $obj = new someclass(); 10402 * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2); 10403 * 10404 * and then the arguments that are passed through at calltime are appended to the argument list. 10405 * 10406 * @param mixed $function a php callback 10407 * @param mixed $arg1,... $argv arguments to partially bind with 10408 * @return array Array callback 10409 */ 10410 function partial() { 10411 if (!class_exists('partial')) { 10412 /** 10413 * Used to manage function binding. 10414 * @copyright 2009 Penny Leach 10415 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 10416 */ 10417 class partial{ 10418 /** @var array */ 10419 public $values = array(); 10420 /** @var string The function to call as a callback. */ 10421 public $func; 10422 /** 10423 * Constructor 10424 * @param string $func 10425 * @param array $args 10426 */ 10427 public function __construct($func, $args) { 10428 $this->values = $args; 10429 $this->func = $func; 10430 } 10431 /** 10432 * Calls the callback function. 10433 * @return mixed 10434 */ 10435 public function method() { 10436 $args = func_get_args(); 10437 return call_user_func_array($this->func, array_merge($this->values, $args)); 10438 } 10439 } 10440 } 10441 $args = func_get_args(); 10442 $func = array_shift($args); 10443 $p = new partial($func, $args); 10444 return array($p, 'method'); 10445 } 10446 10447 /** 10448 * helper function to load up and initialise the mnet environment 10449 * this must be called before you use mnet functions. 10450 * 10451 * @return mnet_environment the equivalent of old $MNET global 10452 */ 10453 function get_mnet_environment() { 10454 global $CFG; 10455 require_once($CFG->dirroot . '/mnet/lib.php'); 10456 static $instance = null; 10457 if (empty($instance)) { 10458 $instance = new mnet_environment(); 10459 $instance->init(); 10460 } 10461 return $instance; 10462 } 10463 10464 /** 10465 * during xmlrpc server code execution, any code wishing to access 10466 * information about the remote peer must use this to get it. 10467 * 10468 * @return mnet_remote_client the equivalent of old $MNETREMOTE_CLIENT global 10469 */ 10470 function get_mnet_remote_client() { 10471 if (!defined('MNET_SERVER')) { 10472 debugging(get_string('notinxmlrpcserver', 'mnet')); 10473 return false; 10474 } 10475 global $MNET_REMOTE_CLIENT; 10476 if (isset($MNET_REMOTE_CLIENT)) { 10477 return $MNET_REMOTE_CLIENT; 10478 } 10479 return false; 10480 } 10481 10482 /** 10483 * during the xmlrpc server code execution, this will be called 10484 * to setup the object returned by {@link get_mnet_remote_client} 10485 * 10486 * @param mnet_remote_client $client the client to set up 10487 * @throws moodle_exception 10488 */ 10489 function set_mnet_remote_client($client) { 10490 if (!defined('MNET_SERVER')) { 10491 throw new moodle_exception('notinxmlrpcserver', 'mnet'); 10492 } 10493 global $MNET_REMOTE_CLIENT; 10494 $MNET_REMOTE_CLIENT = $client; 10495 } 10496 10497 /** 10498 * return the jump url for a given remote user 10499 * this is used for rewriting forum post links in emails, etc 10500 * 10501 * @param stdclass $user the user to get the idp url for 10502 */ 10503 function mnet_get_idp_jump_url($user) { 10504 global $CFG; 10505 10506 static $mnetjumps = array(); 10507 if (!array_key_exists($user->mnethostid, $mnetjumps)) { 10508 $idp = mnet_get_peer_host($user->mnethostid); 10509 $idpjumppath = mnet_get_app_jumppath($idp->applicationid); 10510 $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl='; 10511 } 10512 return $mnetjumps[$user->mnethostid]; 10513 } 10514 10515 /** 10516 * Gets the homepage to use for the current user 10517 * 10518 * @return int One of HOMEPAGE_* 10519 */ 10520 function get_home_page() { 10521 global $CFG; 10522 10523 if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) { 10524 // If dashboard is disabled, home will be set to default page. 10525 $defaultpage = get_default_home_page(); 10526 if ($CFG->defaulthomepage == HOMEPAGE_MY) { 10527 if (!empty($CFG->enabledashboard)) { 10528 return HOMEPAGE_MY; 10529 } else { 10530 return $defaultpage; 10531 } 10532 } else if ($CFG->defaulthomepage == HOMEPAGE_MYCOURSES) { 10533 return HOMEPAGE_MYCOURSES; 10534 } else { 10535 $userhomepage = (int) get_user_preferences('user_home_page_preference', $defaultpage); 10536 if (empty($CFG->enabledashboard) && $userhomepage == HOMEPAGE_MY) { 10537 // If the user was using the dashboard but it's disabled, return the default home page. 10538 $userhomepage = $defaultpage; 10539 } 10540 return $userhomepage; 10541 } 10542 } 10543 return HOMEPAGE_SITE; 10544 } 10545 10546 /** 10547 * Returns the default home page to display if current one is not defined or can't be applied. 10548 * The default behaviour is to return Dashboard if it's enabled or My courses page if it isn't. 10549 * 10550 * @return int The default home page. 10551 */ 10552 function get_default_home_page(): int { 10553 global $CFG; 10554 10555 return !empty($CFG->enabledashboard) ? HOMEPAGE_MY : HOMEPAGE_MYCOURSES; 10556 } 10557 10558 /** 10559 * Gets the name of a course to be displayed when showing a list of courses. 10560 * By default this is just $course->fullname but user can configure it. The 10561 * result of this function should be passed through print_string. 10562 * @param stdClass|core_course_list_element $course Moodle course object 10563 * @return string Display name of course (either fullname or short + fullname) 10564 */ 10565 function get_course_display_name_for_list($course) { 10566 global $CFG; 10567 if (!empty($CFG->courselistshortnames)) { 10568 if (!($course instanceof stdClass)) { 10569 $course = (object)convert_to_array($course); 10570 } 10571 return get_string('courseextendednamedisplay', '', $course); 10572 } else { 10573 return $course->fullname; 10574 } 10575 } 10576 10577 /** 10578 * Safe analogue of unserialize() that can only parse arrays 10579 * 10580 * Arrays may contain only integers or strings as both keys and values. Nested arrays are allowed. 10581 * 10582 * @param string $expression 10583 * @return array|bool either parsed array or false if parsing was impossible. 10584 */ 10585 function unserialize_array($expression) { 10586 10587 // Check the expression is an array. 10588 if (!preg_match('/^a:(\d+):/', $expression)) { 10589 return false; 10590 } 10591 10592 $values = (array) unserialize_object($expression); 10593 10594 // Callback that returns true if the given value is an unserialized object, executes recursively. 10595 $invalidvaluecallback = static function($value) use (&$invalidvaluecallback): bool { 10596 if (is_array($value)) { 10597 return (bool) array_filter($value, $invalidvaluecallback); 10598 } 10599 return ($value instanceof stdClass) || ($value instanceof __PHP_Incomplete_Class); 10600 }; 10601 10602 // Iterate over the result to ensure there are no stray objects. 10603 if (array_filter($values, $invalidvaluecallback)) { 10604 return false; 10605 } 10606 10607 return $values; 10608 } 10609 10610 /** 10611 * Safe method for unserializing given input that is expected to contain only a serialized instance of an stdClass object 10612 * 10613 * If any class type other than stdClass is included in the input string, it will not be instantiated and will be cast to an 10614 * stdClass object. The initial cast to array, then back to object is to ensure we are always returning the correct type, 10615 * otherwise we would return an instances of {@see __PHP_Incomplete_class} for malformed strings 10616 * 10617 * @param string $input 10618 * @return stdClass 10619 */ 10620 function unserialize_object(string $input): stdClass { 10621 $instance = (array) unserialize($input, ['allowed_classes' => [stdClass::class]]); 10622 return (object) $instance; 10623 } 10624 10625 /** 10626 * The lang_string class 10627 * 10628 * This special class is used to create an object representation of a string request. 10629 * It is special because processing doesn't occur until the object is first used. 10630 * The class was created especially to aid performance in areas where strings were 10631 * required to be generated but were not necessarily used. 10632 * As an example the admin tree when generated uses over 1500 strings, of which 10633 * normally only 1/3 are ever actually printed at any time. 10634 * The performance advantage is achieved by not actually processing strings that 10635 * arn't being used, as such reducing the processing required for the page. 10636 * 10637 * How to use the lang_string class? 10638 * There are two methods of using the lang_string class, first through the 10639 * forth argument of the get_string function, and secondly directly. 10640 * The following are examples of both. 10641 * 1. Through get_string calls e.g. 10642 * $string = get_string($identifier, $component, $a, true); 10643 * $string = get_string('yes', 'moodle', null, true); 10644 * 2. Direct instantiation 10645 * $string = new lang_string($identifier, $component, $a, $lang); 10646 * $string = new lang_string('yes'); 10647 * 10648 * How do I use a lang_string object? 10649 * The lang_string object makes use of a magic __toString method so that you 10650 * are able to use the object exactly as you would use a string in most cases. 10651 * This means you are able to collect it into a variable and then directly 10652 * echo it, or concatenate it into another string, or similar. 10653 * The other thing you can do is manually get the string by calling the 10654 * lang_strings out method e.g. 10655 * $string = new lang_string('yes'); 10656 * $string->out(); 10657 * Also worth noting is that the out method can take one argument, $lang which 10658 * allows the developer to change the language on the fly. 10659 * 10660 * When should I use a lang_string object? 10661 * The lang_string object is designed to be used in any situation where a 10662 * string may not be needed, but needs to be generated. 10663 * The admin tree is a good example of where lang_string objects should be 10664 * used. 10665 * A more practical example would be any class that requries strings that may 10666 * not be printed (after all classes get renderer by renderers and who knows 10667 * what they will do ;)) 10668 * 10669 * When should I not use a lang_string object? 10670 * Don't use lang_strings when you are going to use a string immediately. 10671 * There is no need as it will be processed immediately and there will be no 10672 * advantage, and in fact perhaps a negative hit as a class has to be 10673 * instantiated for a lang_string object, however get_string won't require 10674 * that. 10675 * 10676 * Limitations: 10677 * 1. You cannot use a lang_string object as an array offset. Doing so will 10678 * result in PHP throwing an error. (You can use it as an object property!) 10679 * 10680 * @package core 10681 * @category string 10682 * @copyright 2011 Sam Hemelryk 10683 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 10684 */ 10685 class lang_string { 10686 10687 /** @var string The strings identifier */ 10688 protected $identifier; 10689 /** @var string The strings component. Default '' */ 10690 protected $component = ''; 10691 /** @var array|stdClass Any arguments required for the string. Default null */ 10692 protected $a = null; 10693 /** @var string The language to use when processing the string. Default null */ 10694 protected $lang = null; 10695 10696 /** @var string The processed string (once processed) */ 10697 protected $string = null; 10698 10699 /** 10700 * A special boolean. If set to true then the object has been woken up and 10701 * cannot be regenerated. If this is set then $this->string MUST be used. 10702 * @var bool 10703 */ 10704 protected $forcedstring = false; 10705 10706 /** 10707 * Constructs a lang_string object 10708 * 10709 * This function should do as little processing as possible to ensure the best 10710 * performance for strings that won't be used. 10711 * 10712 * @param string $identifier The strings identifier 10713 * @param string $component The strings component 10714 * @param stdClass|array $a Any arguments the string requires 10715 * @param string $lang The language to use when processing the string. 10716 * @throws coding_exception 10717 */ 10718 public function __construct($identifier, $component = '', $a = null, $lang = null) { 10719 if (empty($component)) { 10720 $component = 'moodle'; 10721 } 10722 10723 $this->identifier = $identifier; 10724 $this->component = $component; 10725 $this->lang = $lang; 10726 10727 // We MUST duplicate $a to ensure that it if it changes by reference those 10728 // changes are not carried across. 10729 // To do this we always ensure $a or its properties/values are strings 10730 // and that any properties/values that arn't convertable are forgotten. 10731 if ($a !== null) { 10732 if (is_scalar($a)) { 10733 $this->a = $a; 10734 } else if ($a instanceof lang_string) { 10735 $this->a = $a->out(); 10736 } else if (is_object($a) or is_array($a)) { 10737 $a = (array)$a; 10738 $this->a = array(); 10739 foreach ($a as $key => $value) { 10740 // Make sure conversion errors don't get displayed (results in ''). 10741 if (is_array($value)) { 10742 $this->a[$key] = ''; 10743 } else if (is_object($value)) { 10744 if (method_exists($value, '__toString')) { 10745 $this->a[$key] = $value->__toString(); 10746 } else { 10747 $this->a[$key] = ''; 10748 } 10749 } else { 10750 $this->a[$key] = (string)$value; 10751 } 10752 } 10753 } 10754 } 10755 10756 if (debugging(false, DEBUG_DEVELOPER)) { 10757 if (clean_param($this->identifier, PARAM_STRINGID) == '') { 10758 throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition'); 10759 } 10760 if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') { 10761 throw new coding_exception('Invalid string compontent. Please check your string definition'); 10762 } 10763 if (!get_string_manager()->string_exists($this->identifier, $this->component)) { 10764 debugging('String does not exist. Please check your string definition for '.$this->identifier.'/'.$this->component, DEBUG_DEVELOPER); 10765 } 10766 } 10767 } 10768 10769 /** 10770 * Processes the string. 10771 * 10772 * This function actually processes the string, stores it in the string property 10773 * and then returns it. 10774 * You will notice that this function is VERY similar to the get_string method. 10775 * That is because it is pretty much doing the same thing. 10776 * However as this function is an upgrade it isn't as tolerant to backwards 10777 * compatibility. 10778 * 10779 * @return string 10780 * @throws coding_exception 10781 */ 10782 protected function get_string() { 10783 global $CFG; 10784 10785 // Check if we need to process the string. 10786 if ($this->string === null) { 10787 // Check the quality of the identifier. 10788 if ($CFG->debugdeveloper && clean_param($this->identifier, PARAM_STRINGID) === '') { 10789 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); 10790 } 10791 10792 // Process the string. 10793 $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang); 10794 // Debugging feature lets you display string identifier and component. 10795 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) { 10796 $this->string .= ' {' . $this->identifier . '/' . $this->component . '}'; 10797 } 10798 } 10799 // Return the string. 10800 return $this->string; 10801 } 10802 10803 /** 10804 * Returns the string 10805 * 10806 * @param string $lang The langauge to use when processing the string 10807 * @return string 10808 */ 10809 public function out($lang = null) { 10810 if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) { 10811 if ($this->forcedstring) { 10812 debugging('lang_string objects that have been used cannot be printed in another language. ('.$this->lang.' used)', DEBUG_DEVELOPER); 10813 return $this->get_string(); 10814 } 10815 $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang); 10816 return $translatedstring->out(); 10817 } 10818 return $this->get_string(); 10819 } 10820 10821 /** 10822 * Magic __toString method for printing a string 10823 * 10824 * @return string 10825 */ 10826 public function __toString() { 10827 return $this->get_string(); 10828 } 10829 10830 /** 10831 * Magic __set_state method used for var_export 10832 * 10833 * @param array $array 10834 * @return self 10835 */ 10836 public static function __set_state(array $array): self { 10837 $tmp = new lang_string($array['identifier'], $array['component'], $array['a'], $array['lang']); 10838 $tmp->string = $array['string']; 10839 $tmp->forcedstring = $array['forcedstring']; 10840 return $tmp; 10841 } 10842 10843 /** 10844 * Prepares the lang_string for sleep and stores only the forcedstring and 10845 * string properties... the string cannot be regenerated so we need to ensure 10846 * it is generated for this. 10847 * 10848 * @return string 10849 */ 10850 public function __sleep() { 10851 $this->get_string(); 10852 $this->forcedstring = true; 10853 return array('forcedstring', 'string', 'lang'); 10854 } 10855 10856 /** 10857 * Returns the identifier. 10858 * 10859 * @return string 10860 */ 10861 public function get_identifier() { 10862 return $this->identifier; 10863 } 10864 10865 /** 10866 * Returns the component. 10867 * 10868 * @return string 10869 */ 10870 public function get_component() { 10871 return $this->component; 10872 } 10873 } 10874 10875 /** 10876 * Get human readable name describing the given callable. 10877 * 10878 * This performs syntax check only to see if the given param looks like a valid function, method or closure. 10879 * It does not check if the callable actually exists. 10880 * 10881 * @param callable|string|array $callable 10882 * @return string|bool Human readable name of callable, or false if not a valid callable. 10883 */ 10884 function get_callable_name($callable) { 10885 10886 if (!is_callable($callable, true, $name)) { 10887 return false; 10888 10889 } else { 10890 return $name; 10891 } 10892 } 10893 10894 /** 10895 * Tries to guess if $CFG->wwwroot is publicly accessible or not. 10896 * Never put your faith on this function and rely on its accuracy as there might be false positives. 10897 * It just performs some simple checks, and mainly is used for places where we want to hide some options 10898 * such as site registration when $CFG->wwwroot is not publicly accessible. 10899 * Good thing is there is no false negative. 10900 * Note that it's possible to force the result of this check by specifying $CFG->site_is_public in config.php 10901 * 10902 * @return bool 10903 */ 10904 function site_is_public() { 10905 global $CFG; 10906 10907 // Return early if site admin has forced this setting. 10908 if (isset($CFG->site_is_public)) { 10909 return (bool)$CFG->site_is_public; 10910 } 10911 10912 $host = parse_url($CFG->wwwroot, PHP_URL_HOST); 10913 10914 if ($host === 'localhost' || preg_match('|^127\.\d+\.\d+\.\d+$|', $host)) { 10915 $ispublic = false; 10916 } else if (\core\ip_utils::is_ip_address($host) && !ip_is_public($host)) { 10917 $ispublic = false; 10918 } else if (($address = \core\ip_utils::get_ip_address($host)) && !ip_is_public($address)) { 10919 $ispublic = false; 10920 } else { 10921 $ispublic = true; 10922 } 10923 10924 return $ispublic; 10925 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body