Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402] [Versions 402 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * moodlelib.php - Moodle main library 19 * 20 * Main library file of miscellaneous general-purpose Moodle functions. 21 * Other main libraries: 22 * - weblib.php - functions that produce web output 23 * - datalib.php - functions that access the database 24 * 25 * @package core 26 * @subpackage lib 27 * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com 28 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 29 */ 30 31 defined('MOODLE_INTERNAL') || die(); 32 33 // CONSTANTS (Encased in phpdoc proper comments). 34 35 // Date and time constants. 36 /** 37 * Time constant - the number of seconds in a year 38 */ 39 define('YEARSECS', 31536000); 40 41 /** 42 * Time constant - the number of seconds in a week 43 */ 44 define('WEEKSECS', 604800); 45 46 /** 47 * Time constant - the number of seconds in a day 48 */ 49 define('DAYSECS', 86400); 50 51 /** 52 * Time constant - the number of seconds in an hour 53 */ 54 define('HOURSECS', 3600); 55 56 /** 57 * Time constant - the number of seconds in a minute 58 */ 59 define('MINSECS', 60); 60 61 /** 62 * Time constant - the number of minutes in a day 63 */ 64 define('DAYMINS', 1440); 65 66 /** 67 * Time constant - the number of minutes in an hour 68 */ 69 define('HOURMINS', 60); 70 71 // Parameter constants - every call to optional_param(), required_param() 72 // or clean_param() should have a specified type of parameter. 73 74 /** 75 * PARAM_ALPHA - contains only English ascii letters [a-zA-Z]. 76 */ 77 define('PARAM_ALPHA', 'alpha'); 78 79 /** 80 * PARAM_ALPHAEXT the same contents as PARAM_ALPHA (English ascii letters [a-zA-Z]) plus the chars in quotes: "_-" allowed 81 * NOTE: originally this allowed "/" too, please use PARAM_SAFEPATH if "/" needed 82 */ 83 define('PARAM_ALPHAEXT', 'alphaext'); 84 85 /** 86 * PARAM_ALPHANUM - expected numbers 0-9 and English ascii letters [a-zA-Z] only. 87 */ 88 define('PARAM_ALPHANUM', 'alphanum'); 89 90 /** 91 * PARAM_ALPHANUMEXT - expected numbers 0-9, letters (English ascii letters [a-zA-Z]) and _- only. 92 */ 93 define('PARAM_ALPHANUMEXT', 'alphanumext'); 94 95 /** 96 * PARAM_AUTH - actually checks to make sure the string is a valid auth plugin 97 */ 98 define('PARAM_AUTH', 'auth'); 99 100 /** 101 * PARAM_BASE64 - Base 64 encoded format 102 */ 103 define('PARAM_BASE64', 'base64'); 104 105 /** 106 * PARAM_BOOL - converts input into 0 or 1, use for switches in forms and urls. 107 */ 108 define('PARAM_BOOL', 'bool'); 109 110 /** 111 * PARAM_CAPABILITY - A capability name, like 'moodle/role:manage'. Actually 112 * checked against the list of capabilities in the database. 113 */ 114 define('PARAM_CAPABILITY', 'capability'); 115 116 /** 117 * PARAM_CLEANHTML - cleans submitted HTML code. Note that you almost never want 118 * to use this. The normal mode of operation is to use PARAM_RAW when receiving 119 * the input (required/optional_param or formslib) and then sanitise the HTML 120 * using format_text on output. This is for the rare cases when you want to 121 * sanitise the HTML on input. This cleaning may also fix xhtml strictness. 122 */ 123 define('PARAM_CLEANHTML', 'cleanhtml'); 124 125 /** 126 * PARAM_EMAIL - an email address following the RFC 127 */ 128 define('PARAM_EMAIL', 'email'); 129 130 /** 131 * PARAM_FILE - safe file name, all dangerous chars are stripped, protects against XSS, SQL injections and directory traversals 132 */ 133 define('PARAM_FILE', 'file'); 134 135 /** 136 * PARAM_FLOAT - a real/floating point number. 137 * 138 * Note that you should not use PARAM_FLOAT for numbers typed in by the user. 139 * It does not work for languages that use , as a decimal separator. 140 * Use PARAM_LOCALISEDFLOAT instead. 141 */ 142 define('PARAM_FLOAT', 'float'); 143 144 /** 145 * PARAM_LOCALISEDFLOAT - a localised real/floating point number. 146 * This is preferred over PARAM_FLOAT for numbers typed in by the user. 147 * Cleans localised numbers to computer readable numbers; false for invalid numbers. 148 */ 149 define('PARAM_LOCALISEDFLOAT', 'localisedfloat'); 150 151 /** 152 * PARAM_HOST - expected fully qualified domain name (FQDN) or an IPv4 dotted quad (IP address) 153 */ 154 define('PARAM_HOST', 'host'); 155 156 /** 157 * PARAM_INT - integers only, use when expecting only numbers. 158 */ 159 define('PARAM_INT', 'int'); 160 161 /** 162 * PARAM_LANG - checks to see if the string is a valid installed language in the current site. 163 */ 164 define('PARAM_LANG', 'lang'); 165 166 /** 167 * PARAM_LOCALURL - expected properly formatted URL as well as one that refers to the local server itself. (NOT orthogonal to the 168 * others! Implies PARAM_URL!) 169 */ 170 define('PARAM_LOCALURL', 'localurl'); 171 172 /** 173 * PARAM_NOTAGS - all html tags are stripped from the text. Do not abuse this type. 174 */ 175 define('PARAM_NOTAGS', 'notags'); 176 177 /** 178 * PARAM_PATH - safe relative path name, all dangerous chars are stripped, protects against XSS, SQL injections and directory 179 * traversals note: the leading slash is not removed, window drive letter is not allowed 180 */ 181 define('PARAM_PATH', 'path'); 182 183 /** 184 * PARAM_PEM - Privacy Enhanced Mail format 185 */ 186 define('PARAM_PEM', 'pem'); 187 188 /** 189 * PARAM_PERMISSION - A permission, one of CAP_INHERIT, CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT. 190 */ 191 define('PARAM_PERMISSION', 'permission'); 192 193 /** 194 * PARAM_RAW specifies a parameter that is not cleaned/processed in any way except the discarding of the invalid utf-8 characters 195 */ 196 define('PARAM_RAW', 'raw'); 197 198 /** 199 * PARAM_RAW_TRIMMED like PARAM_RAW but leading and trailing whitespace is stripped. 200 */ 201 define('PARAM_RAW_TRIMMED', 'raw_trimmed'); 202 203 /** 204 * PARAM_SAFEDIR - safe directory name, suitable for include() and require() 205 */ 206 define('PARAM_SAFEDIR', 'safedir'); 207 208 /** 209 * PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths 210 * and other references to Moodle code files. 211 * 212 * This is NOT intended to be used for absolute paths or any user uploaded files. 213 */ 214 define('PARAM_SAFEPATH', 'safepath'); 215 216 /** 217 * PARAM_SEQUENCE - expects a sequence of numbers like 8 to 1,5,6,4,6,8,9. Numbers and comma only. 218 */ 219 define('PARAM_SEQUENCE', 'sequence'); 220 221 /** 222 * PARAM_TAG - one tag (interests, blogs, etc.) - mostly international characters and space, <> not supported 223 */ 224 define('PARAM_TAG', 'tag'); 225 226 /** 227 * PARAM_TAGLIST - list of tags separated by commas (interests, blogs, etc.) 228 */ 229 define('PARAM_TAGLIST', 'taglist'); 230 231 /** 232 * PARAM_TEXT - general plain text compatible with multilang filter, no other html tags. Please note '<', or '>' are allowed here. 233 */ 234 define('PARAM_TEXT', 'text'); 235 236 /** 237 * PARAM_THEME - Checks to see if the string is a valid theme name in the current site 238 */ 239 define('PARAM_THEME', 'theme'); 240 241 /** 242 * PARAM_URL - expected properly formatted URL. Please note that domain part is required, http://localhost/ is not accepted but 243 * http://localhost.localdomain/ is ok. 244 */ 245 define('PARAM_URL', 'url'); 246 247 /** 248 * PARAM_USERNAME - Clean username to only contains allowed characters. This is to be used ONLY when manually creating user 249 * accounts, do NOT use when syncing with external systems!! 250 */ 251 define('PARAM_USERNAME', 'username'); 252 253 /** 254 * PARAM_STRINGID - used to check if the given string is valid string identifier for get_string() 255 */ 256 define('PARAM_STRINGID', 'stringid'); 257 258 // DEPRECATED PARAM TYPES OR ALIASES - DO NOT USE FOR NEW CODE. 259 /** 260 * PARAM_CLEAN - obsoleted, please use a more specific type of parameter. 261 * It was one of the first types, that is why it is abused so much ;-) 262 * @deprecated since 2.0 263 */ 264 define('PARAM_CLEAN', 'clean'); 265 266 /** 267 * PARAM_INTEGER - deprecated alias for PARAM_INT 268 * @deprecated since 2.0 269 */ 270 define('PARAM_INTEGER', 'int'); 271 272 /** 273 * PARAM_NUMBER - deprecated alias of PARAM_FLOAT 274 * @deprecated since 2.0 275 */ 276 define('PARAM_NUMBER', 'float'); 277 278 /** 279 * PARAM_ACTION - deprecated alias for PARAM_ALPHANUMEXT, use for various actions in forms and urls 280 * NOTE: originally alias for PARAM_APLHA 281 * @deprecated since 2.0 282 */ 283 define('PARAM_ACTION', 'alphanumext'); 284 285 /** 286 * PARAM_FORMAT - deprecated alias for PARAM_ALPHANUMEXT, use for names of plugins, formats, etc. 287 * NOTE: originally alias for PARAM_APLHA 288 * @deprecated since 2.0 289 */ 290 define('PARAM_FORMAT', 'alphanumext'); 291 292 /** 293 * PARAM_MULTILANG - deprecated alias of PARAM_TEXT. 294 * @deprecated since 2.0 295 */ 296 define('PARAM_MULTILANG', 'text'); 297 298 /** 299 * PARAM_TIMEZONE - expected timezone. Timezone can be int +-(0-13) or float +-(0.5-12.5) or 300 * string separated by '/' and can have '-' &/ '_' (eg. America/North_Dakota/New_Salem 301 * America/Port-au-Prince) 302 */ 303 define('PARAM_TIMEZONE', 'timezone'); 304 305 /** 306 * PARAM_CLEANFILE - deprecated alias of PARAM_FILE; originally was removing regional chars too 307 */ 308 define('PARAM_CLEANFILE', 'file'); 309 310 /** 311 * PARAM_COMPONENT is used for full component names (aka frankenstyle) such as 'mod_forum', 'core_rating', 'auth_ldap'. 312 * Short legacy subsystem names and module names are accepted too ex: 'forum', 'rating', 'user'. 313 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter. 314 * NOTE: numbers and underscores are strongly discouraged in plugin names! 315 */ 316 define('PARAM_COMPONENT', 'component'); 317 318 /** 319 * PARAM_AREA is a name of area used when addressing files, comments, ratings, etc. 320 * It is usually used together with context id and component. 321 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter. 322 */ 323 define('PARAM_AREA', 'area'); 324 325 /** 326 * PARAM_PLUGIN is used for plugin names such as 'forum', 'glossary', 'ldap', 'paypal', 'completionstatus'. 327 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter. 328 * NOTE: numbers and underscores are strongly discouraged in plugin names! Underscores are forbidden in module names. 329 */ 330 define('PARAM_PLUGIN', 'plugin'); 331 332 333 // Web Services. 334 335 /** 336 * VALUE_REQUIRED - if the parameter is not supplied, there is an error 337 */ 338 define('VALUE_REQUIRED', 1); 339 340 /** 341 * VALUE_OPTIONAL - if the parameter is not supplied, then the param has no value 342 */ 343 define('VALUE_OPTIONAL', 2); 344 345 /** 346 * VALUE_DEFAULT - if the parameter is not supplied, then the default value is used 347 */ 348 define('VALUE_DEFAULT', 0); 349 350 /** 351 * NULL_NOT_ALLOWED - the parameter can not be set to null in the database 352 */ 353 define('NULL_NOT_ALLOWED', false); 354 355 /** 356 * NULL_ALLOWED - the parameter can be set to null in the database 357 */ 358 define('NULL_ALLOWED', true); 359 360 // Page types. 361 362 /** 363 * PAGE_COURSE_VIEW is a definition of a page type. For more information on the page class see moodle/lib/pagelib.php. 364 */ 365 define('PAGE_COURSE_VIEW', 'course-view'); 366 367 /** Get remote addr constant */ 368 define('GETREMOTEADDR_SKIP_HTTP_CLIENT_IP', '1'); 369 /** Get remote addr constant */ 370 define('GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR', '2'); 371 /** 372 * GETREMOTEADDR_SKIP_DEFAULT defines the default behavior remote IP address validation. 373 */ 374 define('GETREMOTEADDR_SKIP_DEFAULT', GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR|GETREMOTEADDR_SKIP_HTTP_CLIENT_IP); 375 376 // Blog access level constant declaration. 377 define ('BLOG_USER_LEVEL', 1); 378 define ('BLOG_GROUP_LEVEL', 2); 379 define ('BLOG_COURSE_LEVEL', 3); 380 define ('BLOG_SITE_LEVEL', 4); 381 define ('BLOG_GLOBAL_LEVEL', 5); 382 383 384 // Tag constants. 385 /** 386 * To prevent problems with multibytes strings,Flag updating in nav not working on the review page. this should not exceed the 387 * length of "varchar(255) / 3 (bytes / utf-8 character) = 85". 388 * TODO: this is not correct, varchar(255) are 255 unicode chars ;-) 389 * 390 * @todo define(TAG_MAX_LENGTH) this is not correct, varchar(255) are 255 unicode chars ;-) 391 */ 392 define('TAG_MAX_LENGTH', 50); 393 394 // Password policy constants. 395 define ('PASSWORD_LOWER', 'abcdefghijklmnopqrstuvwxyz'); 396 define ('PASSWORD_UPPER', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'); 397 define ('PASSWORD_DIGITS', '0123456789'); 398 define ('PASSWORD_NONALPHANUM', '.,;:!?_-+/*@#&$'); 399 400 // 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}/i", $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|int $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 array|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 // Clean the filename as cache_helper::hash_key only allows a-zA-Z0-9_. 7850 $pluginfunctions = false; 7851 if (!empty($CFG->allversionshash)) { 7852 $key = $CFG->allversionshash . '_' . $function . '_' . clean_param($file, PARAM_ALPHA); 7853 $pluginfunctions = $cache->get($key); 7854 } 7855 $dirty = false; 7856 7857 // Use the plugin manager to check that plugins are currently installed. 7858 $pluginmanager = \core_plugin_manager::instance(); 7859 7860 if ($pluginfunctions !== false) { 7861 7862 // Checking that the files are still available. 7863 foreach ($pluginfunctions as $plugintype => $plugins) { 7864 7865 $allplugins = \core_component::get_plugin_list($plugintype); 7866 $installedplugins = $pluginmanager->get_installed_plugins($plugintype); 7867 foreach ($plugins as $plugin => $function) { 7868 if (!isset($installedplugins[$plugin])) { 7869 // Plugin code is still present on disk but it is not installed. 7870 $dirty = true; 7871 break 2; 7872 } 7873 7874 // Cache might be out of sync with the codebase, skip the plugin if it is not available. 7875 if (empty($allplugins[$plugin])) { 7876 $dirty = true; 7877 break 2; 7878 } 7879 7880 $fileexists = file_exists($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file); 7881 if ($include && $fileexists) { 7882 // Include the files if it was requested. 7883 include_once($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file); 7884 } else if (!$fileexists) { 7885 // If the file is not available any more it should not be returned. 7886 $dirty = true; 7887 break 2; 7888 } 7889 7890 // Check if the function still exists in the file. 7891 if ($include && !function_exists($function)) { 7892 $dirty = true; 7893 break 2; 7894 } 7895 } 7896 } 7897 7898 // If the cache is dirty, we should fall through and let it rebuild. 7899 if (!$dirty) { 7900 return $pluginfunctions; 7901 } 7902 } 7903 7904 $pluginfunctions = array(); 7905 7906 // To fill the cached. Also, everything should continue working with cache disabled. 7907 $plugintypes = \core_component::get_plugin_types(); 7908 foreach ($plugintypes as $plugintype => $unused) { 7909 7910 // We need to include files here. 7911 $pluginswithfile = \core_component::get_plugin_list_with_file($plugintype, $file, true); 7912 $installedplugins = $pluginmanager->get_installed_plugins($plugintype); 7913 foreach ($pluginswithfile as $plugin => $notused) { 7914 7915 if (!isset($installedplugins[$plugin])) { 7916 continue; 7917 } 7918 7919 $fullfunction = $plugintype . '_' . $plugin . '_' . $function; 7920 7921 $pluginfunction = false; 7922 if (function_exists($fullfunction)) { 7923 // Function exists with standard name. Store, indexed by frankenstyle name of plugin. 7924 $pluginfunction = $fullfunction; 7925 7926 } else if ($plugintype === 'mod') { 7927 // For modules, we also allow plugin without full frankenstyle but just starting with the module name. 7928 $shortfunction = $plugin . '_' . $function; 7929 if (function_exists($shortfunction)) { 7930 $pluginfunction = $shortfunction; 7931 } 7932 } 7933 7934 if ($pluginfunction) { 7935 if (empty($pluginfunctions[$plugintype])) { 7936 $pluginfunctions[$plugintype] = array(); 7937 } 7938 $pluginfunctions[$plugintype][$plugin] = $pluginfunction; 7939 } 7940 7941 } 7942 } 7943 if (!empty($CFG->allversionshash)) { 7944 $cache->set($key, $pluginfunctions); 7945 } 7946 7947 return $pluginfunctions; 7948 7949 } 7950 7951 /** 7952 * Lists plugin-like directories within specified directory 7953 * 7954 * This function was originally used for standard Moodle plugins, please use 7955 * new core_component::get_plugin_list() now. 7956 * 7957 * This function is used for general directory listing and backwards compatility. 7958 * 7959 * @param string $directory relative directory from root 7960 * @param string $exclude dir name to exclude from the list (defaults to none) 7961 * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot) 7962 * @return array Sorted array of directory names found under the requested parameters 7963 */ 7964 function get_list_of_plugins($directory='mod', $exclude='', $basedir='') { 7965 global $CFG; 7966 7967 $plugins = array(); 7968 7969 if (empty($basedir)) { 7970 $basedir = $CFG->dirroot .'/'. $directory; 7971 7972 } else { 7973 $basedir = $basedir .'/'. $directory; 7974 } 7975 7976 if ($CFG->debugdeveloper and empty($exclude)) { 7977 // Make sure devs do not use this to list normal plugins, 7978 // this is intended for general directories that are not plugins! 7979 7980 $subtypes = core_component::get_plugin_types(); 7981 if (in_array($basedir, $subtypes)) { 7982 debugging('get_list_of_plugins() should not be used to list real plugins, use core_component::get_plugin_list() instead!', DEBUG_DEVELOPER); 7983 } 7984 unset($subtypes); 7985 } 7986 7987 $ignorelist = array_flip(array_filter([ 7988 'CVS', 7989 '_vti_cnf', 7990 'amd', 7991 'classes', 7992 'simpletest', 7993 'tests', 7994 'templates', 7995 'yui', 7996 $exclude, 7997 ])); 7998 7999 if (file_exists($basedir) && filetype($basedir) == 'dir') { 8000 if (!$dirhandle = opendir($basedir)) { 8001 debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER); 8002 return array(); 8003 } 8004 while (false !== ($dir = readdir($dirhandle))) { 8005 if (strpos($dir, '.') === 0) { 8006 // Ignore directories starting with . 8007 // These are treated as hidden directories. 8008 continue; 8009 } 8010 if (array_key_exists($dir, $ignorelist)) { 8011 // This directory features on the ignore list. 8012 continue; 8013 } 8014 if (filetype($basedir .'/'. $dir) != 'dir') { 8015 continue; 8016 } 8017 $plugins[] = $dir; 8018 } 8019 closedir($dirhandle); 8020 } 8021 if ($plugins) { 8022 asort($plugins); 8023 } 8024 return $plugins; 8025 } 8026 8027 /** 8028 * Invoke plugin's callback functions 8029 * 8030 * @param string $type plugin type e.g. 'mod' 8031 * @param string $name plugin name 8032 * @param string $feature feature name 8033 * @param string $action feature's action 8034 * @param array $params parameters of callback function, should be an array 8035 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null. 8036 * @return mixed 8037 * 8038 * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743 8039 */ 8040 function plugin_callback($type, $name, $feature, $action, $params = null, $default = null) { 8041 return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default); 8042 } 8043 8044 /** 8045 * Invoke component's callback functions 8046 * 8047 * @param string $component frankenstyle component name, e.g. 'mod_quiz' 8048 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron' 8049 * @param array $params parameters of callback function 8050 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null. 8051 * @return mixed 8052 */ 8053 function component_callback($component, $function, array $params = array(), $default = null) { 8054 8055 $functionname = component_callback_exists($component, $function); 8056 8057 if ($params && (array_keys($params) !== range(0, count($params) - 1))) { 8058 // PHP 8 allows to have associative arrays in the call_user_func_array() parameters but 8059 // PHP 7 does not. Using associative arrays can result in different behavior in different PHP versions. 8060 // See https://php.watch/versions/8.0/named-parameters#named-params-call_user_func_array 8061 // This check can be removed when minimum PHP version for Moodle is raised to 8. 8062 debugging('Parameters array can not be an associative array while Moodle supports both PHP 7 and PHP 8.', 8063 DEBUG_DEVELOPER); 8064 $params = array_values($params); 8065 } 8066 8067 if ($functionname) { 8068 // Function exists, so just return function result. 8069 $ret = call_user_func_array($functionname, $params); 8070 if (is_null($ret)) { 8071 return $default; 8072 } else { 8073 return $ret; 8074 } 8075 } 8076 return $default; 8077 } 8078 8079 /** 8080 * Determine if a component callback exists and return the function name to call. Note that this 8081 * function will include the required library files so that the functioname returned can be 8082 * called directly. 8083 * 8084 * @param string $component frankenstyle component name, e.g. 'mod_quiz' 8085 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron' 8086 * @return mixed Complete function name to call if the callback exists or false if it doesn't. 8087 * @throws coding_exception if invalid component specfied 8088 */ 8089 function component_callback_exists($component, $function) { 8090 global $CFG; // This is needed for the inclusions. 8091 8092 $cleancomponent = clean_param($component, PARAM_COMPONENT); 8093 if (empty($cleancomponent)) { 8094 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component); 8095 } 8096 $component = $cleancomponent; 8097 8098 list($type, $name) = core_component::normalize_component($component); 8099 $component = $type . '_' . $name; 8100 8101 $oldfunction = $name.'_'.$function; 8102 $function = $component.'_'.$function; 8103 8104 $dir = core_component::get_component_directory($component); 8105 if (empty($dir)) { 8106 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component); 8107 } 8108 8109 // Load library and look for function. 8110 if (file_exists($dir.'/lib.php')) { 8111 require_once($dir.'/lib.php'); 8112 } 8113 8114 if (!function_exists($function) and function_exists($oldfunction)) { 8115 if ($type !== 'mod' and $type !== 'core') { 8116 debugging("Please use new function name $function instead of legacy $oldfunction", DEBUG_DEVELOPER); 8117 } 8118 $function = $oldfunction; 8119 } 8120 8121 if (function_exists($function)) { 8122 return $function; 8123 } 8124 return false; 8125 } 8126 8127 /** 8128 * Call the specified callback method on the provided class. 8129 * 8130 * If the callback returns null, then the default value is returned instead. 8131 * If the class does not exist, then the default value is returned. 8132 * 8133 * @param string $classname The name of the class to call upon. 8134 * @param string $methodname The name of the staticically defined method on the class. 8135 * @param array $params The arguments to pass into the method. 8136 * @param mixed $default The default value. 8137 * @return mixed The return value. 8138 */ 8139 function component_class_callback($classname, $methodname, array $params, $default = null) { 8140 if (!class_exists($classname)) { 8141 return $default; 8142 } 8143 8144 if (!method_exists($classname, $methodname)) { 8145 return $default; 8146 } 8147 8148 $fullfunction = $classname . '::' . $methodname; 8149 $result = call_user_func_array($fullfunction, $params); 8150 8151 if (null === $result) { 8152 return $default; 8153 } else { 8154 return $result; 8155 } 8156 } 8157 8158 /** 8159 * Checks whether a plugin supports a specified feature. 8160 * 8161 * @param string $type Plugin type e.g. 'mod' 8162 * @param string $name Plugin name e.g. 'forum' 8163 * @param string $feature Feature code (FEATURE_xx constant) 8164 * @param mixed $default default value if feature support unknown 8165 * @return mixed Feature result (false if not supported, null if feature is unknown, 8166 * otherwise usually true but may have other feature-specific value such as array) 8167 * @throws coding_exception 8168 */ 8169 function plugin_supports($type, $name, $feature, $default = null) { 8170 global $CFG; 8171 8172 if ($type === 'mod' and $name === 'NEWMODULE') { 8173 // Somebody forgot to rename the module template. 8174 return false; 8175 } 8176 8177 $component = clean_param($type . '_' . $name, PARAM_COMPONENT); 8178 if (empty($component)) { 8179 throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name); 8180 } 8181 8182 $function = null; 8183 8184 if ($type === 'mod') { 8185 // We need this special case because we support subplugins in modules, 8186 // otherwise it would end up in infinite loop. 8187 if (file_exists("$CFG->dirroot/mod/$name/lib.php")) { 8188 include_once("$CFG->dirroot/mod/$name/lib.php"); 8189 $function = $component.'_supports'; 8190 if (!function_exists($function)) { 8191 // Legacy non-frankenstyle function name. 8192 $function = $name.'_supports'; 8193 } 8194 } 8195 8196 } else { 8197 if (!$path = core_component::get_plugin_directory($type, $name)) { 8198 // Non existent plugin type. 8199 return false; 8200 } 8201 if (file_exists("$path/lib.php")) { 8202 include_once("$path/lib.php"); 8203 $function = $component.'_supports'; 8204 } 8205 } 8206 8207 if ($function and function_exists($function)) { 8208 $supports = $function($feature); 8209 if (is_null($supports)) { 8210 // Plugin does not know - use default. 8211 return $default; 8212 } else { 8213 return $supports; 8214 } 8215 } 8216 8217 // Plugin does not care, so use default. 8218 return $default; 8219 } 8220 8221 /** 8222 * Returns true if the current version of PHP is greater that the specified one. 8223 * 8224 * @todo Check PHP version being required here is it too low? 8225 * 8226 * @param string $version The version of php being tested. 8227 * @return bool 8228 */ 8229 function check_php_version($version='5.2.4') { 8230 return (version_compare(phpversion(), $version) >= 0); 8231 } 8232 8233 /** 8234 * Determine if moodle installation requires update. 8235 * 8236 * Checks version numbers of main code and all plugins to see 8237 * if there are any mismatches. 8238 * 8239 * @param bool $checkupgradeflag check the outagelessupgrade flag to see if an upgrade is running. 8240 * @return bool 8241 */ 8242 function moodle_needs_upgrading($checkupgradeflag = true) { 8243 global $CFG, $DB; 8244 8245 // Say no if there is already an upgrade running. 8246 if ($checkupgradeflag) { 8247 $lock = $DB->get_field('config', 'value', ['name' => 'outagelessupgrade']); 8248 $currentprocessrunningupgrade = (defined('CLI_UPGRADE_RUNNING') && CLI_UPGRADE_RUNNING); 8249 // If we ARE locked, but this PHP process is NOT the process running the upgrade, 8250 // We should always return false. 8251 // This means the upgrade is running from CLI somewhere, or about to. 8252 if (!empty($lock) && !$currentprocessrunningupgrade) { 8253 return false; 8254 } 8255 } 8256 8257 if (empty($CFG->version)) { 8258 return true; 8259 } 8260 8261 // There is no need to purge plugininfo caches here because 8262 // these caches are not used during upgrade and they are purged after 8263 // every upgrade. 8264 8265 if (empty($CFG->allversionshash)) { 8266 return true; 8267 } 8268 8269 $hash = core_component::get_all_versions_hash(); 8270 8271 return ($hash !== $CFG->allversionshash); 8272 } 8273 8274 /** 8275 * Returns the major version of this site 8276 * 8277 * Moodle version numbers consist of three numbers separated by a dot, for 8278 * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so 8279 * called major version. This function extracts the major version from either 8280 * $CFG->release (default) or eventually from the $release variable defined in 8281 * the main version.php. 8282 * 8283 * @param bool $fromdisk should the version if source code files be used 8284 * @return string|false the major version like '2.3', false if could not be determined 8285 */ 8286 function moodle_major_version($fromdisk = false) { 8287 global $CFG; 8288 8289 if ($fromdisk) { 8290 $release = null; 8291 require($CFG->dirroot.'/version.php'); 8292 if (empty($release)) { 8293 return false; 8294 } 8295 8296 } else { 8297 if (empty($CFG->release)) { 8298 return false; 8299 } 8300 $release = $CFG->release; 8301 } 8302 8303 if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) { 8304 return $matches[0]; 8305 } else { 8306 return false; 8307 } 8308 } 8309 8310 // MISCELLANEOUS. 8311 8312 /** 8313 * Gets the system locale 8314 * 8315 * @return string Retuns the current locale. 8316 */ 8317 function moodle_getlocale() { 8318 global $CFG; 8319 8320 // Fetch the correct locale based on ostype. 8321 if ($CFG->ostype == 'WINDOWS') { 8322 $stringtofetch = 'localewin'; 8323 } else { 8324 $stringtofetch = 'locale'; 8325 } 8326 8327 if (!empty($CFG->locale)) { // Override locale for all language packs. 8328 return $CFG->locale; 8329 } 8330 8331 return get_string($stringtofetch, 'langconfig'); 8332 } 8333 8334 /** 8335 * Sets the system locale 8336 * 8337 * @category string 8338 * @param string $locale Can be used to force a locale 8339 */ 8340 function moodle_setlocale($locale='') { 8341 global $CFG; 8342 8343 static $currentlocale = ''; // Last locale caching. 8344 8345 $oldlocale = $currentlocale; 8346 8347 // The priority is the same as in get_string() - parameter, config, course, session, user, global language. 8348 if (!empty($locale)) { 8349 $currentlocale = $locale; 8350 } else { 8351 $currentlocale = moodle_getlocale(); 8352 } 8353 8354 // Do nothing if locale already set up. 8355 if ($oldlocale == $currentlocale) { 8356 return; 8357 } 8358 8359 // Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values, 8360 // set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7 8361 // Some day, numeric, monetary and other categories should be set too, I think. :-/. 8362 8363 // Get current values. 8364 $monetary= setlocale (LC_MONETARY, 0); 8365 $numeric = setlocale (LC_NUMERIC, 0); 8366 $ctype = setlocale (LC_CTYPE, 0); 8367 if ($CFG->ostype != 'WINDOWS') { 8368 $messages= setlocale (LC_MESSAGES, 0); 8369 } 8370 // Set locale to all. 8371 $result = setlocale (LC_ALL, $currentlocale); 8372 // If setting of locale fails try the other utf8 or utf-8 variant, 8373 // some operating systems support both (Debian), others just one (OSX). 8374 if ($result === false) { 8375 if (stripos($currentlocale, '.UTF-8') !== false) { 8376 $newlocale = str_ireplace('.UTF-8', '.UTF8', $currentlocale); 8377 setlocale (LC_ALL, $newlocale); 8378 } else if (stripos($currentlocale, '.UTF8') !== false) { 8379 $newlocale = str_ireplace('.UTF8', '.UTF-8', $currentlocale); 8380 setlocale (LC_ALL, $newlocale); 8381 } 8382 } 8383 // Set old values. 8384 setlocale (LC_MONETARY, $monetary); 8385 setlocale (LC_NUMERIC, $numeric); 8386 if ($CFG->ostype != 'WINDOWS') { 8387 setlocale (LC_MESSAGES, $messages); 8388 } 8389 if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') { 8390 // To workaround a well-known PHP problem with Turkish letter Ii. 8391 setlocale (LC_CTYPE, $ctype); 8392 } 8393 } 8394 8395 /** 8396 * Count words in a string. 8397 * 8398 * Words are defined as things between whitespace. 8399 * 8400 * @category string 8401 * @param string $string The text to be searched for words. May be HTML. 8402 * @param int|null $format 8403 * @return int The count of words in the specified string 8404 */ 8405 function count_words($string, $format = null) { 8406 // Before stripping tags, add a space after the close tag of anything that is not obviously inline. 8407 // Also, br is a special case because it definitely delimits a word, but has no close tag. 8408 $string = preg_replace('~ 8409 ( # Capture the tag we match. 8410 </ # Start of close tag. 8411 (?! # Do not match any of these specific close tag names. 8412 a> | b> | del> | em> | i> | 8413 ins> | s> | small> | span> | 8414 strong> | sub> | sup> | u> 8415 ) 8416 \w+ # But, apart from those execptions, match any tag name. 8417 > # End of close tag. 8418 | 8419 <br> | <br\s*/> # Special cases that are not close tags. 8420 ) 8421 ~x', '$1 ', $string); // Add a space after the close tag. 8422 if ($format !== null && $format != FORMAT_PLAIN) { 8423 // Match the usual text cleaning before display. 8424 // Ideally we should apply multilang filter only here, other filters might add extra text. 8425 $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]); 8426 } 8427 // Now remove HTML tags. 8428 $string = strip_tags($string); 8429 // Decode HTML entities. 8430 $string = html_entity_decode($string, ENT_COMPAT); 8431 8432 // Now, the word count is the number of blocks of characters separated 8433 // by any sort of space. That seems to be the definition used by all other systems. 8434 // To be precise about what is considered to separate words: 8435 // * Anything that Unicode considers a 'Separator' 8436 // * Anything that Unicode considers a 'Control character' 8437 // * An em- or en- dash. 8438 return count(preg_split('~[\p{Z}\p{Cc}—–]+~u', $string, -1, PREG_SPLIT_NO_EMPTY)); 8439 } 8440 8441 /** 8442 * Count letters in a string. 8443 * 8444 * Letters are defined as chars not in tags and different from whitespace. 8445 * 8446 * @category string 8447 * @param string $string The text to be searched for letters. May be HTML. 8448 * @param int|null $format 8449 * @return int The count of letters in the specified text. 8450 */ 8451 function count_letters($string, $format = null) { 8452 if ($format !== null && $format != FORMAT_PLAIN) { 8453 // Match the usual text cleaning before display. 8454 // Ideally we should apply multilang filter only here, other filters might add extra text. 8455 $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]); 8456 } 8457 $string = strip_tags($string); // Tags are out now. 8458 $string = html_entity_decode($string, ENT_COMPAT); 8459 $string = preg_replace('/[[:space:]]*/', '', $string); // Whitespace are out now. 8460 8461 return core_text::strlen($string); 8462 } 8463 8464 /** 8465 * Generate and return a random string of the specified length. 8466 * 8467 * @param int $length The length of the string to be created. 8468 * @return string 8469 */ 8470 function random_string($length=15) { 8471 $randombytes = random_bytes_emulate($length); 8472 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 8473 $pool .= 'abcdefghijklmnopqrstuvwxyz'; 8474 $pool .= '0123456789'; 8475 $poollen = strlen($pool); 8476 $string = ''; 8477 for ($i = 0; $i < $length; $i++) { 8478 $rand = ord($randombytes[$i]); 8479 $string .= substr($pool, ($rand%($poollen)), 1); 8480 } 8481 return $string; 8482 } 8483 8484 /** 8485 * Generate a complex random string (useful for md5 salts) 8486 * 8487 * This function is based on the above {@link random_string()} however it uses a 8488 * larger pool of characters and generates a string between 24 and 32 characters 8489 * 8490 * @param int $length Optional if set generates a string to exactly this length 8491 * @return string 8492 */ 8493 function complex_random_string($length=null) { 8494 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 8495 $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} '; 8496 $poollen = strlen($pool); 8497 if ($length===null) { 8498 $length = floor(rand(24, 32)); 8499 } 8500 $randombytes = random_bytes_emulate($length); 8501 $string = ''; 8502 for ($i = 0; $i < $length; $i++) { 8503 $rand = ord($randombytes[$i]); 8504 $string .= $pool[($rand%$poollen)]; 8505 } 8506 return $string; 8507 } 8508 8509 /** 8510 * Try to generates cryptographically secure pseudo-random bytes. 8511 * 8512 * Note this is achieved by fallbacking between: 8513 * - PHP 7 random_bytes(). 8514 * - OpenSSL openssl_random_pseudo_bytes(). 8515 * - In house random generator getting its entropy from various, hard to guess, pseudo-random sources. 8516 * 8517 * @param int $length requested length in bytes 8518 * @return string binary data 8519 */ 8520 function random_bytes_emulate($length) { 8521 global $CFG; 8522 if ($length <= 0) { 8523 debugging('Invalid random bytes length', DEBUG_DEVELOPER); 8524 return ''; 8525 } 8526 if (function_exists('random_bytes')) { 8527 // Use PHP 7 goodness. 8528 $hash = @random_bytes($length); 8529 if ($hash !== false) { 8530 return $hash; 8531 } 8532 } 8533 if (function_exists('openssl_random_pseudo_bytes')) { 8534 // If you have the openssl extension enabled. 8535 $hash = openssl_random_pseudo_bytes($length); 8536 if ($hash !== false) { 8537 return $hash; 8538 } 8539 } 8540 8541 // Bad luck, there is no reliable random generator, let's just slowly hash some unique stuff that is hard to guess. 8542 $staticdata = serialize($CFG) . serialize($_SERVER); 8543 $hash = ''; 8544 do { 8545 $hash .= sha1($staticdata . microtime(true) . uniqid('', true), true); 8546 } while (strlen($hash) < $length); 8547 8548 return substr($hash, 0, $length); 8549 } 8550 8551 /** 8552 * Given some text (which may contain HTML) and an ideal length, 8553 * this function truncates the text neatly on a word boundary if possible 8554 * 8555 * @category string 8556 * @param string $text text to be shortened 8557 * @param int $ideal ideal string length 8558 * @param boolean $exact if false, $text will not be cut mid-word 8559 * @param string $ending The string to append if the passed string is truncated 8560 * @return string $truncate shortened string 8561 */ 8562 function shorten_text($text, $ideal=30, $exact = false, $ending='...') { 8563 // If the plain text is shorter than the maximum length, return the whole text. 8564 if (core_text::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) { 8565 return $text; 8566 } 8567 8568 // Splits on HTML tags. Each open/close/empty tag will be the first thing 8569 // and only tag in its 'line'. 8570 preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER); 8571 8572 $totallength = core_text::strlen($ending); 8573 $truncate = ''; 8574 8575 // This array stores information about open and close tags and their position 8576 // in the truncated string. Each item in the array is an object with fields 8577 // ->open (true if open), ->tag (tag name in lower case), and ->pos 8578 // (byte position in truncated text). 8579 $tagdetails = array(); 8580 8581 foreach ($lines as $linematchings) { 8582 // If there is any html-tag in this line, handle it and add it (uncounted) to the output. 8583 if (!empty($linematchings[1])) { 8584 // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>). 8585 if (!preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $linematchings[1])) { 8586 if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $linematchings[1], $tagmatchings)) { 8587 // Record closing tag. 8588 $tagdetails[] = (object) array( 8589 'open' => false, 8590 'tag' => core_text::strtolower($tagmatchings[1]), 8591 'pos' => core_text::strlen($truncate), 8592 ); 8593 8594 } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $linematchings[1], $tagmatchings)) { 8595 // Record opening tag. 8596 $tagdetails[] = (object) array( 8597 'open' => true, 8598 'tag' => core_text::strtolower($tagmatchings[1]), 8599 'pos' => core_text::strlen($truncate), 8600 ); 8601 } else if (preg_match('/^<!--\[if\s.*?\]>$/s', $linematchings[1], $tagmatchings)) { 8602 $tagdetails[] = (object) array( 8603 'open' => true, 8604 'tag' => core_text::strtolower('if'), 8605 'pos' => core_text::strlen($truncate), 8606 ); 8607 } else if (preg_match('/^<!--<!\[endif\]-->$/s', $linematchings[1], $tagmatchings)) { 8608 $tagdetails[] = (object) array( 8609 'open' => false, 8610 'tag' => core_text::strtolower('if'), 8611 'pos' => core_text::strlen($truncate), 8612 ); 8613 } 8614 } 8615 // Add html-tag to $truncate'd text. 8616 $truncate .= $linematchings[1]; 8617 } 8618 8619 // Calculate the length of the plain text part of the line; handle entities as one character. 8620 $contentlength = core_text::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $linematchings[2])); 8621 if ($totallength + $contentlength > $ideal) { 8622 // The number of characters which are left. 8623 $left = $ideal - $totallength; 8624 $entitieslength = 0; 8625 // Search for html entities. 8626 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)) { 8627 // Calculate the real length of all entities in the legal range. 8628 foreach ($entities[0] as $entity) { 8629 if ($entity[1]+1-$entitieslength <= $left) { 8630 $left--; 8631 $entitieslength += core_text::strlen($entity[0]); 8632 } else { 8633 // No more characters left. 8634 break; 8635 } 8636 } 8637 } 8638 $breakpos = $left + $entitieslength; 8639 8640 // If the words shouldn't be cut in the middle... 8641 if (!$exact) { 8642 // Search the last occurence of a space. 8643 for (; $breakpos > 0; $breakpos--) { 8644 if ($char = core_text::substr($linematchings[2], $breakpos, 1)) { 8645 if ($char === '.' or $char === ' ') { 8646 $breakpos += 1; 8647 break; 8648 } else if (strlen($char) > 2) { 8649 // Chinese/Japanese/Korean text can be truncated at any UTF-8 character boundary. 8650 $breakpos += 1; 8651 break; 8652 } 8653 } 8654 } 8655 } 8656 if ($breakpos == 0) { 8657 // This deals with the test_shorten_text_no_spaces case. 8658 $breakpos = $left + $entitieslength; 8659 } else if ($breakpos > $left + $entitieslength) { 8660 // This deals with the previous for loop breaking on the first char. 8661 $breakpos = $left + $entitieslength; 8662 } 8663 8664 $truncate .= core_text::substr($linematchings[2], 0, $breakpos); 8665 // Maximum length is reached, so get off the loop. 8666 break; 8667 } else { 8668 $truncate .= $linematchings[2]; 8669 $totallength += $contentlength; 8670 } 8671 8672 // If the maximum length is reached, get off the loop. 8673 if ($totallength >= $ideal) { 8674 break; 8675 } 8676 } 8677 8678 // Add the defined ending to the text. 8679 $truncate .= $ending; 8680 8681 // Now calculate the list of open html tags based on the truncate position. 8682 $opentags = array(); 8683 foreach ($tagdetails as $taginfo) { 8684 if ($taginfo->open) { 8685 // Add tag to the beginning of $opentags list. 8686 array_unshift($opentags, $taginfo->tag); 8687 } else { 8688 // Can have multiple exact same open tags, close the last one. 8689 $pos = array_search($taginfo->tag, array_reverse($opentags, true)); 8690 if ($pos !== false) { 8691 unset($opentags[$pos]); 8692 } 8693 } 8694 } 8695 8696 // Close all unclosed html-tags. 8697 foreach ($opentags as $tag) { 8698 if ($tag === 'if') { 8699 $truncate .= '<!--<![endif]-->'; 8700 } else { 8701 $truncate .= '</' . $tag . '>'; 8702 } 8703 } 8704 8705 return $truncate; 8706 } 8707 8708 /** 8709 * Shortens a given filename by removing characters positioned after the ideal string length. 8710 * When the filename is too long, the file cannot be created on the filesystem due to exceeding max byte size. 8711 * Limiting the filename to a certain size (considering multibyte characters) will prevent this. 8712 * 8713 * @param string $filename file name 8714 * @param int $length ideal string length 8715 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness. 8716 * @return string $shortened shortened file name 8717 */ 8718 function shorten_filename($filename, $length = MAX_FILENAME_SIZE, $includehash = false) { 8719 $shortened = $filename; 8720 // Extract a part of the filename if it's char size exceeds the ideal string length. 8721 if (core_text::strlen($filename) > $length) { 8722 // Exclude extension if present in filename. 8723 $mimetypes = get_mimetypes_array(); 8724 $extension = pathinfo($filename, PATHINFO_EXTENSION); 8725 if ($extension && !empty($mimetypes[$extension])) { 8726 $basename = pathinfo($filename, PATHINFO_FILENAME); 8727 $hash = empty($includehash) ? '' : ' - ' . substr(sha1($basename), 0, 10); 8728 $shortened = core_text::substr($basename, 0, $length - strlen($hash)) . $hash; 8729 $shortened .= '.' . $extension; 8730 } else { 8731 $hash = empty($includehash) ? '' : ' - ' . substr(sha1($filename), 0, 10); 8732 $shortened = core_text::substr($filename, 0, $length - strlen($hash)) . $hash; 8733 } 8734 } 8735 return $shortened; 8736 } 8737 8738 /** 8739 * Shortens a given array of filenames by removing characters positioned after the ideal string length. 8740 * 8741 * @param array $path The paths to reduce the length. 8742 * @param int $length Ideal string length 8743 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness. 8744 * @return array $result Shortened paths in array. 8745 */ 8746 function shorten_filenames(array $path, $length = MAX_FILENAME_SIZE, $includehash = false) { 8747 $result = null; 8748 8749 $result = array_reduce($path, function($carry, $singlepath) use ($length, $includehash) { 8750 $carry[] = shorten_filename($singlepath, $length, $includehash); 8751 return $carry; 8752 }, []); 8753 8754 return $result; 8755 } 8756 8757 /** 8758 * Given dates in seconds, how many weeks is the date from startdate 8759 * The first week is 1, the second 2 etc ... 8760 * 8761 * @param int $startdate Timestamp for the start date 8762 * @param int $thedate Timestamp for the end date 8763 * @return string 8764 */ 8765 function getweek ($startdate, $thedate) { 8766 if ($thedate < $startdate) { 8767 return 0; 8768 } 8769 8770 return floor(($thedate - $startdate) / WEEKSECS) + 1; 8771 } 8772 8773 /** 8774 * Returns a randomly generated password of length $maxlen. inspired by 8775 * 8776 * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and 8777 * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254} 8778 * 8779 * @param int $maxlen The maximum size of the password being generated. 8780 * @return string 8781 */ 8782 function generate_password($maxlen=10) { 8783 global $CFG; 8784 8785 if (empty($CFG->passwordpolicy)) { 8786 $fillers = PASSWORD_DIGITS; 8787 $wordlist = file($CFG->wordlist); 8788 $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]); 8789 $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]); 8790 $filler1 = $fillers[rand(0, strlen($fillers) - 1)]; 8791 $password = $word1 . $filler1 . $word2; 8792 } else { 8793 $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0; 8794 $digits = $CFG->minpassworddigits; 8795 $lower = $CFG->minpasswordlower; 8796 $upper = $CFG->minpasswordupper; 8797 $nonalphanum = $CFG->minpasswordnonalphanum; 8798 $total = $lower + $upper + $digits + $nonalphanum; 8799 // Var minlength should be the greater one of the two ( $minlen and $total ). 8800 $minlen = $minlen < $total ? $total : $minlen; 8801 // Var maxlen can never be smaller than minlen. 8802 $maxlen = $minlen > $maxlen ? $minlen : $maxlen; 8803 $additional = $maxlen - $total; 8804 8805 // Make sure we have enough characters to fulfill 8806 // complexity requirements. 8807 $passworddigits = PASSWORD_DIGITS; 8808 while ($digits > strlen($passworddigits)) { 8809 $passworddigits .= PASSWORD_DIGITS; 8810 } 8811 $passwordlower = PASSWORD_LOWER; 8812 while ($lower > strlen($passwordlower)) { 8813 $passwordlower .= PASSWORD_LOWER; 8814 } 8815 $passwordupper = PASSWORD_UPPER; 8816 while ($upper > strlen($passwordupper)) { 8817 $passwordupper .= PASSWORD_UPPER; 8818 } 8819 $passwordnonalphanum = PASSWORD_NONALPHANUM; 8820 while ($nonalphanum > strlen($passwordnonalphanum)) { 8821 $passwordnonalphanum .= PASSWORD_NONALPHANUM; 8822 } 8823 8824 // Now mix and shuffle it all. 8825 $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) . 8826 substr(str_shuffle ($passwordupper), 0, $upper) . 8827 substr(str_shuffle ($passworddigits), 0, $digits) . 8828 substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) . 8829 substr(str_shuffle ($passwordlower . 8830 $passwordupper . 8831 $passworddigits . 8832 $passwordnonalphanum), 0 , $additional)); 8833 } 8834 8835 return substr ($password, 0, $maxlen); 8836 } 8837 8838 /** 8839 * Given a float, prints it nicely. 8840 * Localized floats must not be used in calculations! 8841 * 8842 * The stripzeros feature is intended for making numbers look nicer in small 8843 * areas where it is not necessary to indicate the degree of accuracy by showing 8844 * ending zeros. If you turn it on with $decimalpoints set to 3, for example, 8845 * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'. 8846 * 8847 * @param float $float The float to print 8848 * @param int $decimalpoints The number of decimal places to print. -1 is a special value for auto detect (full precision). 8849 * @param bool $localized use localized decimal separator 8850 * @param bool $stripzeros If true, removes final zeros after decimal point. It will be ignored and the trailing zeros after 8851 * the decimal point are always striped if $decimalpoints is -1. 8852 * @return string locale float 8853 */ 8854 function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=false) { 8855 if (is_null($float)) { 8856 return ''; 8857 } 8858 if ($localized) { 8859 $separator = get_string('decsep', 'langconfig'); 8860 } else { 8861 $separator = '.'; 8862 } 8863 if ($decimalpoints == -1) { 8864 // The following counts the number of decimals. 8865 // It is safe as both floatval() and round() functions have same behaviour when non-numeric values are provided. 8866 $floatval = floatval($float); 8867 for ($decimalpoints = 0; $floatval != round($float, $decimalpoints); $decimalpoints++); 8868 } 8869 8870 $result = number_format($float, $decimalpoints, $separator, ''); 8871 if ($stripzeros && $decimalpoints > 0) { 8872 // Remove zeros and final dot if not needed. 8873 // However, only do this if there is a decimal point! 8874 $result = preg_replace('~(' . preg_quote($separator, '~') . ')?0+$~', '', $result); 8875 } 8876 return $result; 8877 } 8878 8879 /** 8880 * Converts locale specific floating point/comma number back to standard PHP float value 8881 * Do NOT try to do any math operations before this conversion on any user submitted floats! 8882 * 8883 * @param string $localefloat locale aware float representation 8884 * @param bool $strict If true, then check the input and return false if it is not a valid number. 8885 * @return mixed float|bool - false or the parsed float. 8886 */ 8887 function unformat_float($localefloat, $strict = false) { 8888 $localefloat = trim((string)$localefloat); 8889 8890 if ($localefloat == '') { 8891 return null; 8892 } 8893 8894 $localefloat = str_replace(' ', '', $localefloat); // No spaces - those might be used as thousand separators. 8895 $localefloat = str_replace(get_string('decsep', 'langconfig'), '.', $localefloat); 8896 8897 if ($strict && !is_numeric($localefloat)) { 8898 return false; 8899 } 8900 8901 return (float)$localefloat; 8902 } 8903 8904 /** 8905 * Given a simple array, this shuffles it up just like shuffle() 8906 * Unlike PHP's shuffle() this function works on any machine. 8907 * 8908 * @param array $array The array to be rearranged 8909 * @return array 8910 */ 8911 function swapshuffle($array) { 8912 8913 $last = count($array) - 1; 8914 for ($i = 0; $i <= $last; $i++) { 8915 $from = rand(0, $last); 8916 $curr = $array[$i]; 8917 $array[$i] = $array[$from]; 8918 $array[$from] = $curr; 8919 } 8920 return $array; 8921 } 8922 8923 /** 8924 * Like {@link swapshuffle()}, but works on associative arrays 8925 * 8926 * @param array $array The associative array to be rearranged 8927 * @return array 8928 */ 8929 function swapshuffle_assoc($array) { 8930 8931 $newarray = array(); 8932 $newkeys = swapshuffle(array_keys($array)); 8933 8934 foreach ($newkeys as $newkey) { 8935 $newarray[$newkey] = $array[$newkey]; 8936 } 8937 return $newarray; 8938 } 8939 8940 /** 8941 * Given an arbitrary array, and a number of draws, 8942 * this function returns an array with that amount 8943 * of items. The indexes are retained. 8944 * 8945 * @todo Finish documenting this function 8946 * 8947 * @param array $array 8948 * @param int $draws 8949 * @return array 8950 */ 8951 function draw_rand_array($array, $draws) { 8952 8953 $return = array(); 8954 8955 $last = count($array); 8956 8957 if ($draws > $last) { 8958 $draws = $last; 8959 } 8960 8961 while ($draws > 0) { 8962 $last--; 8963 8964 $keys = array_keys($array); 8965 $rand = rand(0, $last); 8966 8967 $return[$keys[$rand]] = $array[$keys[$rand]]; 8968 unset($array[$keys[$rand]]); 8969 8970 $draws--; 8971 } 8972 8973 return $return; 8974 } 8975 8976 /** 8977 * Calculate the difference between two microtimes 8978 * 8979 * @param string $a The first Microtime 8980 * @param string $b The second Microtime 8981 * @return string 8982 */ 8983 function microtime_diff($a, $b) { 8984 list($adec, $asec) = explode(' ', $a); 8985 list($bdec, $bsec) = explode(' ', $b); 8986 return $bsec - $asec + $bdec - $adec; 8987 } 8988 8989 /** 8990 * Given a list (eg a,b,c,d,e) this function returns 8991 * an array of 1->a, 2->b, 3->c etc 8992 * 8993 * @param string $list The string to explode into array bits 8994 * @param string $separator The separator used within the list string 8995 * @return array The now assembled array 8996 */ 8997 function make_menu_from_list($list, $separator=',') { 8998 8999 $array = array_reverse(explode($separator, $list), true); 9000 foreach ($array as $key => $item) { 9001 $outarray[$key+1] = trim($item); 9002 } 9003 return $outarray; 9004 } 9005 9006 /** 9007 * Creates an array that represents all the current grades that 9008 * can be chosen using the given grading type. 9009 * 9010 * Negative numbers 9011 * are scales, zero is no grade, and positive numbers are maximum 9012 * grades. 9013 * 9014 * @todo Finish documenting this function or better deprecated this completely! 9015 * 9016 * @param int $gradingtype 9017 * @return array 9018 */ 9019 function make_grades_menu($gradingtype) { 9020 global $DB; 9021 9022 $grades = array(); 9023 if ($gradingtype < 0) { 9024 if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) { 9025 return make_menu_from_list($scale->scale); 9026 } 9027 } else if ($gradingtype > 0) { 9028 for ($i=$gradingtype; $i>=0; $i--) { 9029 $grades[$i] = $i .' / '. $gradingtype; 9030 } 9031 return $grades; 9032 } 9033 return $grades; 9034 } 9035 9036 /** 9037 * make_unique_id_code 9038 * 9039 * @todo Finish documenting this function 9040 * 9041 * @uses $_SERVER 9042 * @param string $extra Extra string to append to the end of the code 9043 * @return string 9044 */ 9045 function make_unique_id_code($extra = '') { 9046 9047 $hostname = 'unknownhost'; 9048 if (!empty($_SERVER['HTTP_HOST'])) { 9049 $hostname = $_SERVER['HTTP_HOST']; 9050 } else if (!empty($_ENV['HTTP_HOST'])) { 9051 $hostname = $_ENV['HTTP_HOST']; 9052 } else if (!empty($_SERVER['SERVER_NAME'])) { 9053 $hostname = $_SERVER['SERVER_NAME']; 9054 } else if (!empty($_ENV['SERVER_NAME'])) { 9055 $hostname = $_ENV['SERVER_NAME']; 9056 } 9057 9058 $date = gmdate("ymdHis"); 9059 9060 $random = random_string(6); 9061 9062 if ($extra) { 9063 return $hostname .'+'. $date .'+'. $random .'+'. $extra; 9064 } else { 9065 return $hostname .'+'. $date .'+'. $random; 9066 } 9067 } 9068 9069 9070 /** 9071 * Function to check the passed address is within the passed subnet 9072 * 9073 * The parameter is a comma separated string of subnet definitions. 9074 * Subnet strings can be in one of three formats: 9075 * 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn (number of bits in net mask) 9076 * 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) 9077 * 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. (incomplete address, a bit non-technical ;-) 9078 * Code for type 1 modified from user posted comments by mediator at 9079 * {@link http://au.php.net/manual/en/function.ip2long.php} 9080 * 9081 * @param string $addr The address you are checking 9082 * @param string $subnetstr The string of subnet addresses 9083 * @param bool $checkallzeros The state to whether check for 0.0.0.0 9084 * @return bool 9085 */ 9086 function address_in_subnet($addr, $subnetstr, $checkallzeros = false) { 9087 9088 if ($addr == '0.0.0.0' && !$checkallzeros) { 9089 return false; 9090 } 9091 $subnets = explode(',', $subnetstr); 9092 $found = false; 9093 $addr = trim($addr); 9094 $addr = cleanremoteaddr($addr, false); // Normalise. 9095 if ($addr === null) { 9096 return false; 9097 } 9098 $addrparts = explode(':', $addr); 9099 9100 $ipv6 = strpos($addr, ':'); 9101 9102 foreach ($subnets as $subnet) { 9103 $subnet = trim($subnet); 9104 if ($subnet === '') { 9105 continue; 9106 } 9107 9108 if (strpos($subnet, '/') !== false) { 9109 // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn. 9110 list($ip, $mask) = explode('/', $subnet); 9111 $mask = trim($mask); 9112 if (!is_number($mask)) { 9113 continue; // Incorect mask number, eh? 9114 } 9115 $ip = cleanremoteaddr($ip, false); // Normalise. 9116 if ($ip === null) { 9117 continue; 9118 } 9119 if (strpos($ip, ':') !== false) { 9120 // IPv6. 9121 if (!$ipv6) { 9122 continue; 9123 } 9124 if ($mask > 128 or $mask < 0) { 9125 continue; // Nonsense. 9126 } 9127 if ($mask == 0) { 9128 return true; // Any address. 9129 } 9130 if ($mask == 128) { 9131 if ($ip === $addr) { 9132 return true; 9133 } 9134 continue; 9135 } 9136 $ipparts = explode(':', $ip); 9137 $modulo = $mask % 16; 9138 $ipnet = array_slice($ipparts, 0, ($mask-$modulo)/16); 9139 $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16); 9140 if (implode(':', $ipnet) === implode(':', $addrnet)) { 9141 if ($modulo == 0) { 9142 return true; 9143 } 9144 $pos = ($mask-$modulo)/16; 9145 $ipnet = hexdec($ipparts[$pos]); 9146 $addrnet = hexdec($addrparts[$pos]); 9147 $mask = 0xffff << (16 - $modulo); 9148 if (($addrnet & $mask) == ($ipnet & $mask)) { 9149 return true; 9150 } 9151 } 9152 9153 } else { 9154 // IPv4. 9155 if ($ipv6) { 9156 continue; 9157 } 9158 if ($mask > 32 or $mask < 0) { 9159 continue; // Nonsense. 9160 } 9161 if ($mask == 0) { 9162 return true; 9163 } 9164 if ($mask == 32) { 9165 if ($ip === $addr) { 9166 return true; 9167 } 9168 continue; 9169 } 9170 $mask = 0xffffffff << (32 - $mask); 9171 if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) { 9172 return true; 9173 } 9174 } 9175 9176 } else if (strpos($subnet, '-') !== false) { 9177 // 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. 9178 $parts = explode('-', $subnet); 9179 if (count($parts) != 2) { 9180 continue; 9181 } 9182 9183 if (strpos($subnet, ':') !== false) { 9184 // IPv6. 9185 if (!$ipv6) { 9186 continue; 9187 } 9188 $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise. 9189 if ($ipstart === null) { 9190 continue; 9191 } 9192 $ipparts = explode(':', $ipstart); 9193 $start = hexdec(array_pop($ipparts)); 9194 $ipparts[] = trim($parts[1]); 9195 $ipend = cleanremoteaddr(implode(':', $ipparts), false); // Normalise. 9196 if ($ipend === null) { 9197 continue; 9198 } 9199 $ipparts[7] = ''; 9200 $ipnet = implode(':', $ipparts); 9201 if (strpos($addr, $ipnet) !== 0) { 9202 continue; 9203 } 9204 $ipparts = explode(':', $ipend); 9205 $end = hexdec($ipparts[7]); 9206 9207 $addrend = hexdec($addrparts[7]); 9208 9209 if (($addrend >= $start) and ($addrend <= $end)) { 9210 return true; 9211 } 9212 9213 } else { 9214 // IPv4. 9215 if ($ipv6) { 9216 continue; 9217 } 9218 $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise. 9219 if ($ipstart === null) { 9220 continue; 9221 } 9222 $ipparts = explode('.', $ipstart); 9223 $ipparts[3] = trim($parts[1]); 9224 $ipend = cleanremoteaddr(implode('.', $ipparts), false); // Normalise. 9225 if ($ipend === null) { 9226 continue; 9227 } 9228 9229 if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) { 9230 return true; 9231 } 9232 } 9233 9234 } else { 9235 // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. 9236 if (strpos($subnet, ':') !== false) { 9237 // IPv6. 9238 if (!$ipv6) { 9239 continue; 9240 } 9241 $parts = explode(':', $subnet); 9242 $count = count($parts); 9243 if ($parts[$count-1] === '') { 9244 unset($parts[$count-1]); // Trim trailing :'s. 9245 $count--; 9246 $subnet = implode('.', $parts); 9247 } 9248 $isip = cleanremoteaddr($subnet, false); // Normalise. 9249 if ($isip !== null) { 9250 if ($isip === $addr) { 9251 return true; 9252 } 9253 continue; 9254 } else if ($count > 8) { 9255 continue; 9256 } 9257 $zeros = array_fill(0, 8-$count, '0'); 9258 $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16); 9259 if (address_in_subnet($addr, $subnet)) { 9260 return true; 9261 } 9262 9263 } else { 9264 // IPv4. 9265 if ($ipv6) { 9266 continue; 9267 } 9268 $parts = explode('.', $subnet); 9269 $count = count($parts); 9270 if ($parts[$count-1] === '') { 9271 unset($parts[$count-1]); // Trim trailing . 9272 $count--; 9273 $subnet = implode('.', $parts); 9274 } 9275 if ($count == 4) { 9276 $subnet = cleanremoteaddr($subnet, false); // Normalise. 9277 if ($subnet === $addr) { 9278 return true; 9279 } 9280 continue; 9281 } else if ($count > 4) { 9282 continue; 9283 } 9284 $zeros = array_fill(0, 4-$count, '0'); 9285 $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8); 9286 if (address_in_subnet($addr, $subnet)) { 9287 return true; 9288 } 9289 } 9290 } 9291 } 9292 9293 return false; 9294 } 9295 9296 /** 9297 * For outputting debugging info 9298 * 9299 * @param string $string The string to write 9300 * @param string $eol The end of line char(s) to use 9301 * @param string $sleep Period to make the application sleep 9302 * This ensures any messages have time to display before redirect 9303 */ 9304 function mtrace($string, $eol="\n", $sleep=0) { 9305 global $CFG; 9306 9307 if (isset($CFG->mtrace_wrapper) && function_exists($CFG->mtrace_wrapper)) { 9308 $fn = $CFG->mtrace_wrapper; 9309 $fn($string, $eol); 9310 return; 9311 } else if (defined('STDOUT') && !PHPUNIT_TEST && !defined('BEHAT_TEST')) { 9312 // We must explicitly call the add_line function here. 9313 // Uses of fwrite to STDOUT are not picked up by ob_start. 9314 if ($output = \core\task\logmanager::add_line("{$string}{$eol}")) { 9315 fwrite(STDOUT, $output); 9316 } 9317 } else { 9318 echo $string . $eol; 9319 } 9320 9321 // Flush again. 9322 flush(); 9323 9324 // Delay to keep message on user's screen in case of subsequent redirect. 9325 if ($sleep) { 9326 sleep($sleep); 9327 } 9328 } 9329 9330 /** 9331 * Helper to {@see mtrace()} an exception or throwable, including all relevant information. 9332 * 9333 * @param Throwable $e the error to ouptput. 9334 */ 9335 function mtrace_exception(Throwable $e): void { 9336 $info = get_exception_info($e); 9337 9338 $message = $info->message; 9339 if ($info->debuginfo) { 9340 $message .= "\n\n" . $info->debuginfo; 9341 } 9342 if ($info->backtrace) { 9343 $message .= "\n\n" . format_backtrace($info->backtrace, true); 9344 } 9345 9346 mtrace($message); 9347 } 9348 9349 /** 9350 * Replace 1 or more slashes or backslashes to 1 slash 9351 * 9352 * @param string $path The path to strip 9353 * @return string the path with double slashes removed 9354 */ 9355 function cleardoubleslashes ($path) { 9356 return preg_replace('/(\/|\\\){1,}/', '/', $path); 9357 } 9358 9359 /** 9360 * Is the current ip in a given list? 9361 * 9362 * @param string $list 9363 * @return bool 9364 */ 9365 function remoteip_in_list($list) { 9366 $clientip = getremoteaddr(null); 9367 9368 if (!$clientip) { 9369 // Ensure access on cli. 9370 return true; 9371 } 9372 return \core\ip_utils::is_ip_in_subnet_list($clientip, $list); 9373 } 9374 9375 /** 9376 * Returns most reliable client address 9377 * 9378 * @param string $default If an address can't be determined, then return this 9379 * @return string The remote IP address 9380 */ 9381 function getremoteaddr($default='0.0.0.0') { 9382 global $CFG; 9383 9384 if (!isset($CFG->getremoteaddrconf)) { 9385 // This will happen, for example, before just after the upgrade, as the 9386 // user is redirected to the admin screen. 9387 $variablestoskip = GETREMOTEADDR_SKIP_DEFAULT; 9388 } else { 9389 $variablestoskip = $CFG->getremoteaddrconf; 9390 } 9391 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) { 9392 if (!empty($_SERVER['HTTP_CLIENT_IP'])) { 9393 $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']); 9394 return $address ? $address : $default; 9395 } 9396 } 9397 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) { 9398 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { 9399 $forwardedaddresses = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']); 9400 9401 $forwardedaddresses = array_filter($forwardedaddresses, function($ip) { 9402 global $CFG; 9403 return !\core\ip_utils::is_ip_in_subnet_list($ip, $CFG->reverseproxyignore ?? '', ','); 9404 }); 9405 9406 // Multiple proxies can append values to this header including an 9407 // untrusted original request header so we must only trust the last ip. 9408 $address = end($forwardedaddresses); 9409 9410 if (substr_count($address, ":") > 1) { 9411 // Remove port and brackets from IPv6. 9412 if (preg_match("/\[(.*)\]:/", $address, $matches)) { 9413 $address = $matches[1]; 9414 } 9415 } else { 9416 // Remove port from IPv4. 9417 if (substr_count($address, ":") == 1) { 9418 $parts = explode(":", $address); 9419 $address = $parts[0]; 9420 } 9421 } 9422 9423 $address = cleanremoteaddr($address); 9424 return $address ? $address : $default; 9425 } 9426 } 9427 if (!empty($_SERVER['REMOTE_ADDR'])) { 9428 $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']); 9429 return $address ? $address : $default; 9430 } else { 9431 return $default; 9432 } 9433 } 9434 9435 /** 9436 * Cleans an ip address. Internal addresses are now allowed. 9437 * (Originally local addresses were not allowed.) 9438 * 9439 * @param string $addr IPv4 or IPv6 address 9440 * @param bool $compress use IPv6 address compression 9441 * @return string normalised ip address string, null if error 9442 */ 9443 function cleanremoteaddr($addr, $compress=false) { 9444 $addr = trim($addr); 9445 9446 if (strpos($addr, ':') !== false) { 9447 // Can be only IPv6. 9448 $parts = explode(':', $addr); 9449 $count = count($parts); 9450 9451 if (strpos($parts[$count-1], '.') !== false) { 9452 // Legacy ipv4 notation. 9453 $last = array_pop($parts); 9454 $ipv4 = cleanremoteaddr($last, true); 9455 if ($ipv4 === null) { 9456 return null; 9457 } 9458 $bits = explode('.', $ipv4); 9459 $parts[] = dechex($bits[0]).dechex($bits[1]); 9460 $parts[] = dechex($bits[2]).dechex($bits[3]); 9461 $count = count($parts); 9462 $addr = implode(':', $parts); 9463 } 9464 9465 if ($count < 3 or $count > 8) { 9466 return null; // Severly malformed. 9467 } 9468 9469 if ($count != 8) { 9470 if (strpos($addr, '::') === false) { 9471 return null; // Malformed. 9472 } 9473 // Uncompress. 9474 $insertat = array_search('', $parts, true); 9475 $missing = array_fill(0, 1 + 8 - $count, '0'); 9476 array_splice($parts, $insertat, 1, $missing); 9477 foreach ($parts as $key => $part) { 9478 if ($part === '') { 9479 $parts[$key] = '0'; 9480 } 9481 } 9482 } 9483 9484 $adr = implode(':', $parts); 9485 if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) { 9486 return null; // Incorrect format - sorry. 9487 } 9488 9489 // Normalise 0s and case. 9490 $parts = array_map('hexdec', $parts); 9491 $parts = array_map('dechex', $parts); 9492 9493 $result = implode(':', $parts); 9494 9495 if (!$compress) { 9496 return $result; 9497 } 9498 9499 if ($result === '0:0:0:0:0:0:0:0') { 9500 return '::'; // All addresses. 9501 } 9502 9503 $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1); 9504 if ($compressed !== $result) { 9505 return $compressed; 9506 } 9507 9508 $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1); 9509 if ($compressed !== $result) { 9510 return $compressed; 9511 } 9512 9513 $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1); 9514 if ($compressed !== $result) { 9515 return $compressed; 9516 } 9517 9518 return $result; 9519 } 9520 9521 // First get all things that look like IPv4 addresses. 9522 $parts = array(); 9523 if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) { 9524 return null; 9525 } 9526 unset($parts[0]); 9527 9528 foreach ($parts as $key => $match) { 9529 if ($match > 255) { 9530 return null; 9531 } 9532 $parts[$key] = (int)$match; // Normalise 0s. 9533 } 9534 9535 return implode('.', $parts); 9536 } 9537 9538 9539 /** 9540 * Is IP address a public address? 9541 * 9542 * @param string $ip The ip to check 9543 * @return bool true if the ip is public 9544 */ 9545 function ip_is_public($ip) { 9546 return (bool) filter_var($ip, FILTER_VALIDATE_IP, (FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)); 9547 } 9548 9549 /** 9550 * This function will make a complete copy of anything it's given, 9551 * regardless of whether it's an object or not. 9552 * 9553 * @param mixed $thing Something you want cloned 9554 * @return mixed What ever it is you passed it 9555 */ 9556 function fullclone($thing) { 9557 return unserialize(serialize($thing)); 9558 } 9559 9560 /** 9561 * Used to make sure that $min <= $value <= $max 9562 * 9563 * Make sure that value is between min, and max 9564 * 9565 * @param int $min The minimum value 9566 * @param int $value The value to check 9567 * @param int $max The maximum value 9568 * @return int 9569 */ 9570 function bounded_number($min, $value, $max) { 9571 if ($value < $min) { 9572 return $min; 9573 } 9574 if ($value > $max) { 9575 return $max; 9576 } 9577 return $value; 9578 } 9579 9580 /** 9581 * Check if there is a nested array within the passed array 9582 * 9583 * @param array $array 9584 * @return bool true if there is a nested array false otherwise 9585 */ 9586 function array_is_nested($array) { 9587 foreach ($array as $value) { 9588 if (is_array($value)) { 9589 return true; 9590 } 9591 } 9592 return false; 9593 } 9594 9595 /** 9596 * get_performance_info() pairs up with init_performance_info() 9597 * loaded in setup.php. Returns an array with 'html' and 'txt' 9598 * values ready for use, and each of the individual stats provided 9599 * separately as well. 9600 * 9601 * @return array 9602 */ 9603 function get_performance_info() { 9604 global $CFG, $PERF, $DB, $PAGE; 9605 9606 $info = array(); 9607 $info['txt'] = me() . ' '; // Holds log-friendly representation. 9608 9609 $info['html'] = ''; 9610 if (!empty($CFG->themedesignermode)) { 9611 // Attempt to avoid devs debugging peformance issues, when its caused by css building and so on. 9612 $info['html'] .= '<p><strong>Warning: Theme designer mode is enabled.</strong></p>'; 9613 } 9614 $info['html'] .= '<ul class="list-unstyled row mx-md-0">'; // Holds userfriendly HTML representation. 9615 9616 $info['realtime'] = microtime_diff($PERF->starttime, microtime()); 9617 9618 $info['html'] .= '<li class="timeused col-sm-4">'.$info['realtime'].' secs</li> '; 9619 $info['txt'] .= 'time: '.$info['realtime'].'s '; 9620 9621 // GET/POST (or NULL if $_SERVER['REQUEST_METHOD'] is undefined) is useful for txt logged information. 9622 $info['txt'] .= 'method: ' . ($_SERVER['REQUEST_METHOD'] ?? "NULL") . ' '; 9623 9624 if (function_exists('memory_get_usage')) { 9625 $info['memory_total'] = memory_get_usage(); 9626 $info['memory_growth'] = memory_get_usage() - $PERF->startmemory; 9627 $info['html'] .= '<li class="memoryused col-sm-4">RAM: '.display_size($info['memory_total']).'</li> '; 9628 $info['txt'] .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '. 9629 $info['memory_growth'].'B ('.display_size($info['memory_growth']).') '; 9630 } 9631 9632 if (function_exists('memory_get_peak_usage')) { 9633 $info['memory_peak'] = memory_get_peak_usage(); 9634 $info['html'] .= '<li class="memoryused col-sm-4">RAM peak: '.display_size($info['memory_peak']).'</li> '; 9635 $info['txt'] .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') '; 9636 } 9637 9638 $info['html'] .= '</ul><ul class="list-unstyled row mx-md-0">'; 9639 $inc = get_included_files(); 9640 $info['includecount'] = count($inc); 9641 $info['html'] .= '<li class="included col-sm-4">Included '.$info['includecount'].' files</li> '; 9642 $info['txt'] .= 'includecount: '.$info['includecount'].' '; 9643 9644 if (!empty($CFG->early_install_lang) or empty($PAGE)) { 9645 // We can not track more performance before installation or before PAGE init, sorry. 9646 return $info; 9647 } 9648 9649 $filtermanager = filter_manager::instance(); 9650 if (method_exists($filtermanager, 'get_performance_summary')) { 9651 list($filterinfo, $nicenames) = $filtermanager->get_performance_summary(); 9652 $info = array_merge($filterinfo, $info); 9653 foreach ($filterinfo as $key => $value) { 9654 $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> "; 9655 $info['txt'] .= "$key: $value "; 9656 } 9657 } 9658 9659 $stringmanager = get_string_manager(); 9660 if (method_exists($stringmanager, 'get_performance_summary')) { 9661 list($filterinfo, $nicenames) = $stringmanager->get_performance_summary(); 9662 $info = array_merge($filterinfo, $info); 9663 foreach ($filterinfo as $key => $value) { 9664 $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> "; 9665 $info['txt'] .= "$key: $value "; 9666 } 9667 } 9668 9669 if (!empty($PERF->logwrites)) { 9670 $info['logwrites'] = $PERF->logwrites; 9671 $info['html'] .= '<li class="logwrites col-sm-4">Log DB writes '.$info['logwrites'].'</li> '; 9672 $info['txt'] .= 'logwrites: '.$info['logwrites'].' '; 9673 } 9674 9675 $info['dbqueries'] = $DB->perf_get_reads().'/'.($DB->perf_get_writes() - $PERF->logwrites); 9676 $info['html'] .= '<li class="dbqueries col-sm-4">DB reads/writes: '.$info['dbqueries'].'</li> '; 9677 $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' '; 9678 9679 if ($DB->want_read_slave()) { 9680 $info['dbreads_slave'] = $DB->perf_get_reads_slave(); 9681 $info['html'] .= '<li class="dbqueries col-sm-4">DB reads from slave: '.$info['dbreads_slave'].'</li> '; 9682 $info['txt'] .= 'db reads from slave: '.$info['dbreads_slave'].' '; 9683 } 9684 9685 $info['dbtime'] = round($DB->perf_get_queries_time(), 5); 9686 $info['html'] .= '<li class="dbtime col-sm-4">DB queries time: '.$info['dbtime'].' secs</li> '; 9687 $info['txt'] .= 'db queries time: ' . $info['dbtime'] . 's '; 9688 9689 if (function_exists('posix_times')) { 9690 $ptimes = posix_times(); 9691 if (is_array($ptimes)) { 9692 foreach ($ptimes as $key => $val) { 9693 $info[$key] = $ptimes[$key] - $PERF->startposixtimes[$key]; 9694 } 9695 $info['html'] .= "<li class=\"posixtimes col-sm-4\">ticks: $info[ticks] user: $info[utime]"; 9696 $info['html'] .= "sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</li> "; 9697 $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] "; 9698 } 9699 } 9700 9701 // Grab the load average for the last minute. 9702 // /proc will only work under some linux configurations 9703 // while uptime is there under MacOSX/Darwin and other unices. 9704 if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) { 9705 list($serverload) = explode(' ', $loadavg[0]); 9706 unset($loadavg); 9707 } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) { 9708 if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) { 9709 $serverload = $matches[1]; 9710 } else { 9711 trigger_error('Could not parse uptime output!'); 9712 } 9713 } 9714 if (!empty($serverload)) { 9715 $info['serverload'] = $serverload; 9716 $info['html'] .= '<li class="serverload col-sm-4">Load average: '.$info['serverload'].'</li> '; 9717 $info['txt'] .= "serverload: {$info['serverload']} "; 9718 } 9719 9720 // Display size of session if session started. 9721 if ($si = \core\session\manager::get_performance_info()) { 9722 $info['sessionsize'] = $si['size']; 9723 $info['html'] .= "<li class=\"serverload col-sm-4\">" . $si['html'] . "</li>"; 9724 $info['txt'] .= $si['txt']; 9725 } 9726 9727 // Display time waiting for session if applicable. 9728 if (!empty($PERF->sessionlock['wait'])) { 9729 $sessionwait = number_format($PERF->sessionlock['wait'], 3) . ' secs'; 9730 $info['html'] .= html_writer::tag('li', 'Session wait: ' . $sessionwait, [ 9731 'class' => 'sessionwait col-sm-4' 9732 ]); 9733 $info['txt'] .= 'sessionwait: ' . $sessionwait . ' '; 9734 } 9735 9736 $info['html'] .= '</ul>'; 9737 $html = ''; 9738 if ($stats = cache_helper::get_stats()) { 9739 9740 $table = new html_table(); 9741 $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered'; 9742 $table->head = ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S', 'I/O']; 9743 $table->data = []; 9744 $table->align = ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right', 'right']; 9745 9746 $text = 'Caches used (hits/misses/sets): '; 9747 $hits = 0; 9748 $misses = 0; 9749 $sets = 0; 9750 $maxstores = 0; 9751 9752 // We want to align static caches into their own column. 9753 $hasstatic = false; 9754 foreach ($stats as $definition => $details) { 9755 $numstores = count($details['stores']); 9756 $first = key($details['stores']); 9757 if ($first !== cache_store::STATIC_ACCEL) { 9758 $numstores++; // Add a blank space for the missing static store. 9759 } 9760 $maxstores = max($maxstores, $numstores); 9761 } 9762 9763 $storec = 0; 9764 9765 while ($storec++ < ($maxstores - 2)) { 9766 if ($storec == ($maxstores - 2)) { 9767 $table->head[] = get_string('mappingfinal', 'cache'); 9768 } else { 9769 $table->head[] = "Store $storec"; 9770 } 9771 $table->align[] = 'left'; 9772 $table->align[] = 'right'; 9773 $table->align[] = 'right'; 9774 $table->align[] = 'right'; 9775 $table->align[] = 'right'; 9776 $table->head[] = 'H'; 9777 $table->head[] = 'M'; 9778 $table->head[] = 'S'; 9779 $table->head[] = 'I/O'; 9780 } 9781 9782 ksort($stats); 9783 9784 foreach ($stats as $definition => $details) { 9785 switch ($details['mode']) { 9786 case cache_store::MODE_APPLICATION: 9787 $modeclass = 'application'; 9788 $mode = ' <span title="application cache">App</span>'; 9789 break; 9790 case cache_store::MODE_SESSION: 9791 $modeclass = 'session'; 9792 $mode = ' <span title="session cache">Ses</span>'; 9793 break; 9794 case cache_store::MODE_REQUEST: 9795 $modeclass = 'request'; 9796 $mode = ' <span title="request cache">Req</span>'; 9797 break; 9798 } 9799 $row = [$mode, $definition]; 9800 9801 $text .= "$definition {"; 9802 9803 $storec = 0; 9804 foreach ($details['stores'] as $store => $data) { 9805 9806 if ($storec == 0 && $store !== cache_store::STATIC_ACCEL) { 9807 $row[] = ''; 9808 $row[] = ''; 9809 $row[] = ''; 9810 $storec++; 9811 } 9812 9813 $hits += $data['hits']; 9814 $misses += $data['misses']; 9815 $sets += $data['sets']; 9816 if ($data['hits'] == 0 and $data['misses'] > 0) { 9817 $cachestoreclass = 'nohits bg-danger'; 9818 } else if ($data['hits'] < $data['misses']) { 9819 $cachestoreclass = 'lowhits bg-warning text-dark'; 9820 } else { 9821 $cachestoreclass = 'hihits'; 9822 } 9823 $text .= "$store($data[hits]/$data[misses]/$data[sets]) "; 9824 $cell = new html_table_cell($store); 9825 $cell->attributes = ['class' => $cachestoreclass]; 9826 $row[] = $cell; 9827 $cell = new html_table_cell($data['hits']); 9828 $cell->attributes = ['class' => $cachestoreclass]; 9829 $row[] = $cell; 9830 $cell = new html_table_cell($data['misses']); 9831 $cell->attributes = ['class' => $cachestoreclass]; 9832 $row[] = $cell; 9833 9834 if ($store !== cache_store::STATIC_ACCEL) { 9835 // The static cache is never set. 9836 $cell = new html_table_cell($data['sets']); 9837 $cell->attributes = ['class' => $cachestoreclass]; 9838 $row[] = $cell; 9839 9840 if ($data['hits'] || $data['sets']) { 9841 if ($data['iobytes'] === cache_store::IO_BYTES_NOT_SUPPORTED) { 9842 $size = '-'; 9843 } else { 9844 $size = display_size($data['iobytes'], 1, 'KB'); 9845 if ($data['iobytes'] >= 10 * 1024) { 9846 $cachestoreclass = ' bg-warning text-dark'; 9847 } 9848 } 9849 } else { 9850 $size = ''; 9851 } 9852 $cell = new html_table_cell($size); 9853 $cell->attributes = ['class' => $cachestoreclass]; 9854 $row[] = $cell; 9855 } 9856 $storec++; 9857 } 9858 while ($storec++ < $maxstores) { 9859 $row[] = ''; 9860 $row[] = ''; 9861 $row[] = ''; 9862 $row[] = ''; 9863 $row[] = ''; 9864 } 9865 $text .= '} '; 9866 9867 $table->data[] = $row; 9868 } 9869 9870 $html .= html_writer::table($table); 9871 9872 // Now lets also show sub totals for each cache store. 9873 $storetotals = []; 9874 $storetotal = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0]; 9875 foreach ($stats as $definition => $details) { 9876 foreach ($details['stores'] as $store => $data) { 9877 if (!array_key_exists($store, $storetotals)) { 9878 $storetotals[$store] = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0]; 9879 } 9880 $storetotals[$store]['class'] = $data['class']; 9881 $storetotals[$store]['hits'] += $data['hits']; 9882 $storetotals[$store]['misses'] += $data['misses']; 9883 $storetotals[$store]['sets'] += $data['sets']; 9884 $storetotal['hits'] += $data['hits']; 9885 $storetotal['misses'] += $data['misses']; 9886 $storetotal['sets'] += $data['sets']; 9887 if ($data['iobytes'] !== cache_store::IO_BYTES_NOT_SUPPORTED) { 9888 $storetotals[$store]['iobytes'] += $data['iobytes']; 9889 $storetotal['iobytes'] += $data['iobytes']; 9890 } 9891 } 9892 } 9893 9894 $table = new html_table(); 9895 $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered'; 9896 $table->head = [get_string('storename', 'cache'), get_string('type_cachestore', 'plugin'), 'H', 'M', 'S', 'I/O']; 9897 $table->data = []; 9898 $table->align = ['left', 'left', 'right', 'right', 'right', 'right']; 9899 9900 ksort($storetotals); 9901 9902 foreach ($storetotals as $store => $data) { 9903 $row = []; 9904 if ($data['hits'] == 0 and $data['misses'] > 0) { 9905 $cachestoreclass = 'nohits bg-danger'; 9906 } else if ($data['hits'] < $data['misses']) { 9907 $cachestoreclass = 'lowhits bg-warning text-dark'; 9908 } else { 9909 $cachestoreclass = 'hihits'; 9910 } 9911 $cell = new html_table_cell($store); 9912 $cell->attributes = ['class' => $cachestoreclass]; 9913 $row[] = $cell; 9914 $cell = new html_table_cell($data['class']); 9915 $cell->attributes = ['class' => $cachestoreclass]; 9916 $row[] = $cell; 9917 $cell = new html_table_cell($data['hits']); 9918 $cell->attributes = ['class' => $cachestoreclass]; 9919 $row[] = $cell; 9920 $cell = new html_table_cell($data['misses']); 9921 $cell->attributes = ['class' => $cachestoreclass]; 9922 $row[] = $cell; 9923 $cell = new html_table_cell($data['sets']); 9924 $cell->attributes = ['class' => $cachestoreclass]; 9925 $row[] = $cell; 9926 if ($data['hits'] || $data['sets']) { 9927 if ($data['iobytes']) { 9928 $size = display_size($data['iobytes'], 1, 'KB'); 9929 } else { 9930 $size = '-'; 9931 } 9932 } else { 9933 $size = ''; 9934 } 9935 $cell = new html_table_cell($size); 9936 $cell->attributes = ['class' => $cachestoreclass]; 9937 $row[] = $cell; 9938 $table->data[] = $row; 9939 } 9940 if (!empty($storetotal['iobytes'])) { 9941 $size = display_size($storetotal['iobytes'], 1, 'KB'); 9942 } else if (!empty($storetotal['hits']) || !empty($storetotal['sets'])) { 9943 $size = '-'; 9944 } else { 9945 $size = ''; 9946 } 9947 $row = [ 9948 get_string('total'), 9949 '', 9950 $storetotal['hits'], 9951 $storetotal['misses'], 9952 $storetotal['sets'], 9953 $size, 9954 ]; 9955 $table->data[] = $row; 9956 9957 $html .= html_writer::table($table); 9958 9959 $info['cachesused'] = "$hits / $misses / $sets"; 9960 $info['html'] .= $html; 9961 $info['txt'] .= $text.'. '; 9962 } else { 9963 $info['cachesused'] = '0 / 0 / 0'; 9964 $info['html'] .= '<div class="cachesused">Caches used (hits/misses/sets): 0/0/0</div>'; 9965 $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 '; 9966 } 9967 9968 // Display lock information if any. 9969 if (!empty($PERF->locks)) { 9970 $table = new html_table(); 9971 $table->attributes['class'] = 'locktimings table table-dark table-sm w-auto table-bordered'; 9972 $table->head = ['Lock', 'Waited (s)', 'Obtained', 'Held for (s)']; 9973 $table->align = ['left', 'right', 'center', 'right']; 9974 $table->data = []; 9975 $text = 'Locks (waited/obtained/held):'; 9976 foreach ($PERF->locks as $locktiming) { 9977 $row = []; 9978 $row[] = s($locktiming->type . '/' . $locktiming->resource); 9979 $text .= ' ' . $locktiming->type . '/' . $locktiming->resource . ' ('; 9980 9981 // The time we had to wait to get the lock. 9982 $roundedtime = number_format($locktiming->wait, 1); 9983 $cell = new html_table_cell($roundedtime); 9984 if ($locktiming->wait > 0.5) { 9985 $cell->attributes = ['class' => 'bg-warning text-dark']; 9986 } 9987 $row[] = $cell; 9988 $text .= $roundedtime . '/'; 9989 9990 // Show a tick or cross for success. 9991 $row[] = $locktiming->success ? '✓' : '❌'; 9992 $text .= ($locktiming->success ? 'y' : 'n') . '/'; 9993 9994 // If applicable, show how long we held the lock before releasing it. 9995 if (property_exists($locktiming, 'held')) { 9996 $roundedtime = number_format($locktiming->held, 1); 9997 $cell = new html_table_cell($roundedtime); 9998 if ($locktiming->held > 0.5) { 9999 $cell->attributes = ['class' => 'bg-warning text-dark']; 10000 } 10001 $row[] = $cell; 10002 $text .= $roundedtime; 10003 } else { 10004 $row[] = '-'; 10005 $text .= '-'; 10006 } 10007 $text .= ')'; 10008 10009 $table->data[] = $row; 10010 } 10011 $info['html'] .= html_writer::table($table); 10012 $info['txt'] .= $text . '. '; 10013 } 10014 10015 $info['html'] = '<div class="performanceinfo siteinfo container-fluid px-md-0 overflow-auto pt-3">'.$info['html'].'</div>'; 10016 return $info; 10017 } 10018 10019 /** 10020 * Renames a file or directory to a unique name within the same directory. 10021 * 10022 * This function is designed to avoid any potential race conditions, and select an unused name. 10023 * 10024 * @param string $filepath Original filepath 10025 * @param string $prefix Prefix to use for the temporary name 10026 * @return string|bool New file path or false if failed 10027 * @since Moodle 3.10 10028 */ 10029 function rename_to_unused_name(string $filepath, string $prefix = '_temp_') { 10030 $dir = dirname($filepath); 10031 $basename = $dir . '/' . $prefix; 10032 $limit = 0; 10033 while ($limit < 100) { 10034 // Select a new name based on a random number. 10035 $newfilepath = $basename . md5(mt_rand()); 10036 10037 // Attempt a rename to that new name. 10038 if (@rename($filepath, $newfilepath)) { 10039 return $newfilepath; 10040 } 10041 10042 // The first time, do some sanity checks, maybe it is failing for a good reason and there 10043 // is no point trying 100 times if so. 10044 if ($limit === 0 && (!file_exists($filepath) || !is_writable($dir))) { 10045 return false; 10046 } 10047 $limit++; 10048 } 10049 return false; 10050 } 10051 10052 /** 10053 * Delete directory or only its content 10054 * 10055 * @param string $dir directory path 10056 * @param bool $contentonly 10057 * @return bool success, true also if dir does not exist 10058 */ 10059 function remove_dir($dir, $contentonly=false) { 10060 if (!is_dir($dir)) { 10061 // Nothing to do. 10062 return true; 10063 } 10064 10065 if (!$contentonly) { 10066 // Start by renaming the directory; this will guarantee that other processes don't write to it 10067 // while it is in the process of being deleted. 10068 $tempdir = rename_to_unused_name($dir); 10069 if ($tempdir) { 10070 // If the rename was successful then delete the $tempdir instead. 10071 $dir = $tempdir; 10072 } 10073 // If the rename fails, we will continue through and attempt to delete the directory 10074 // without renaming it since that is likely to at least delete most of the files. 10075 } 10076 10077 if (!$handle = opendir($dir)) { 10078 return false; 10079 } 10080 $result = true; 10081 while (false!==($item = readdir($handle))) { 10082 if ($item != '.' && $item != '..') { 10083 if (is_dir($dir.'/'.$item)) { 10084 $result = remove_dir($dir.'/'.$item) && $result; 10085 } else { 10086 $result = unlink($dir.'/'.$item) && $result; 10087 } 10088 } 10089 } 10090 closedir($handle); 10091 if ($contentonly) { 10092 clearstatcache(); // Make sure file stat cache is properly invalidated. 10093 return $result; 10094 } 10095 $result = rmdir($dir); // If anything left the result will be false, no need for && $result. 10096 clearstatcache(); // Make sure file stat cache is properly invalidated. 10097 return $result; 10098 } 10099 10100 /** 10101 * Detect if an object or a class contains a given property 10102 * will take an actual object or the name of a class 10103 * 10104 * @param mixed $obj Name of class or real object to test 10105 * @param string $property name of property to find 10106 * @return bool true if property exists 10107 */ 10108 function object_property_exists( $obj, $property ) { 10109 if (is_string( $obj )) { 10110 $properties = get_class_vars( $obj ); 10111 } else { 10112 $properties = get_object_vars( $obj ); 10113 } 10114 return array_key_exists( $property, $properties ); 10115 } 10116 10117 /** 10118 * Converts an object into an associative array 10119 * 10120 * This function converts an object into an associative array by iterating 10121 * over its public properties. Because this function uses the foreach 10122 * construct, Iterators are respected. It works recursively on arrays of objects. 10123 * Arrays and simple values are returned as is. 10124 * 10125 * If class has magic properties, it can implement IteratorAggregate 10126 * and return all available properties in getIterator() 10127 * 10128 * @param mixed $var 10129 * @return array 10130 */ 10131 function convert_to_array($var) { 10132 $result = array(); 10133 10134 // Loop over elements/properties. 10135 foreach ($var as $key => $value) { 10136 // Recursively convert objects. 10137 if (is_object($value) || is_array($value)) { 10138 $result[$key] = convert_to_array($value); 10139 } else { 10140 // Simple values are untouched. 10141 $result[$key] = $value; 10142 } 10143 } 10144 return $result; 10145 } 10146 10147 /** 10148 * Detect a custom script replacement in the data directory that will 10149 * replace an existing moodle script 10150 * 10151 * @return string|bool full path name if a custom script exists, false if no custom script exists 10152 */ 10153 function custom_script_path() { 10154 global $CFG, $SCRIPT; 10155 10156 if ($SCRIPT === null) { 10157 // Probably some weird external script. 10158 return false; 10159 } 10160 10161 $scriptpath = $CFG->customscripts . $SCRIPT; 10162 10163 // Check the custom script exists. 10164 if (file_exists($scriptpath) and is_file($scriptpath)) { 10165 return $scriptpath; 10166 } else { 10167 return false; 10168 } 10169 } 10170 10171 /** 10172 * Returns whether or not the user object is a remote MNET user. This function 10173 * is in moodlelib because it does not rely on loading any of the MNET code. 10174 * 10175 * @param object $user A valid user object 10176 * @return bool True if the user is from a remote Moodle. 10177 */ 10178 function is_mnet_remote_user($user) { 10179 global $CFG; 10180 10181 if (!isset($CFG->mnet_localhost_id)) { 10182 include_once($CFG->dirroot . '/mnet/lib.php'); 10183 $env = new mnet_environment(); 10184 $env->init(); 10185 unset($env); 10186 } 10187 10188 return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id); 10189 } 10190 10191 /** 10192 * This function will search for browser prefereed languages, setting Moodle 10193 * to use the best one available if $SESSION->lang is undefined 10194 */ 10195 function setup_lang_from_browser() { 10196 global $CFG, $SESSION, $USER; 10197 10198 if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) { 10199 // Lang is defined in session or user profile, nothing to do. 10200 return; 10201 } 10202 10203 if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do. 10204 return; 10205 } 10206 10207 // Extract and clean langs from headers. 10208 $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE']; 10209 $rawlangs = str_replace('-', '_', $rawlangs); // We are using underscores. 10210 $rawlangs = explode(',', $rawlangs); // Convert to array. 10211 $langs = array(); 10212 10213 $order = 1.0; 10214 foreach ($rawlangs as $lang) { 10215 if (strpos($lang, ';') === false) { 10216 $langs[(string)$order] = $lang; 10217 $order = $order-0.01; 10218 } else { 10219 $parts = explode(';', $lang); 10220 $pos = strpos($parts[1], '='); 10221 $langs[substr($parts[1], $pos+1)] = $parts[0]; 10222 } 10223 } 10224 krsort($langs, SORT_NUMERIC); 10225 10226 // Look for such langs under standard locations. 10227 foreach ($langs as $lang) { 10228 // Clean it properly for include. 10229 $lang = strtolower(clean_param($lang, PARAM_SAFEDIR)); 10230 if (get_string_manager()->translation_exists($lang, false)) { 10231 // If the translation for this language exists then try to set it 10232 // for the rest of the session, if this is a read only session then 10233 // we can only set it temporarily in $CFG. 10234 if (defined('READ_ONLY_SESSION') && !empty($CFG->enable_read_only_sessions)) { 10235 $CFG->lang = $lang; 10236 } else { 10237 $SESSION->lang = $lang; 10238 } 10239 // We have finished. Go out. 10240 break; 10241 } 10242 } 10243 return; 10244 } 10245 10246 /** 10247 * Check if $url matches anything in proxybypass list 10248 * 10249 * Any errors just result in the proxy being used (least bad) 10250 * 10251 * @param string $url url to check 10252 * @return boolean true if we should bypass the proxy 10253 */ 10254 function is_proxybypass( $url ) { 10255 global $CFG; 10256 10257 // Sanity check. 10258 if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) { 10259 return false; 10260 } 10261 10262 // Get the host part out of the url. 10263 if (!$host = parse_url( $url, PHP_URL_HOST )) { 10264 return false; 10265 } 10266 10267 // Get the possible bypass hosts into an array. 10268 $matches = explode( ',', $CFG->proxybypass ); 10269 10270 // Check for a exact match on the IP or in the domains. 10271 $isdomaininallowedlist = \core\ip_utils::is_domain_in_allowed_list($host, $matches); 10272 $isipinsubnetlist = \core\ip_utils::is_ip_in_subnet_list($host, $CFG->proxybypass, ','); 10273 10274 if ($isdomaininallowedlist || $isipinsubnetlist) { 10275 return true; 10276 } 10277 10278 // Nothing matched. 10279 return false; 10280 } 10281 10282 /** 10283 * Check if the passed navigation is of the new style 10284 * 10285 * @param mixed $navigation 10286 * @return bool true for yes false for no 10287 */ 10288 function is_newnav($navigation) { 10289 if (is_array($navigation) && !empty($navigation['newnav'])) { 10290 return true; 10291 } else { 10292 return false; 10293 } 10294 } 10295 10296 /** 10297 * Checks whether the given variable name is defined as a variable within the given object. 10298 * 10299 * This will NOT work with stdClass objects, which have no class variables. 10300 * 10301 * @param string $var The variable name 10302 * @param object $object The object to check 10303 * @return boolean 10304 */ 10305 function in_object_vars($var, $object) { 10306 $classvars = get_class_vars(get_class($object)); 10307 $classvars = array_keys($classvars); 10308 return in_array($var, $classvars); 10309 } 10310 10311 /** 10312 * Returns an array without repeated objects. 10313 * This function is similar to array_unique, but for arrays that have objects as values 10314 * 10315 * @param array $array 10316 * @param bool $keepkeyassoc 10317 * @return array 10318 */ 10319 function object_array_unique($array, $keepkeyassoc = true) { 10320 $duplicatekeys = array(); 10321 $tmp = array(); 10322 10323 foreach ($array as $key => $val) { 10324 // Convert objects to arrays, in_array() does not support objects. 10325 if (is_object($val)) { 10326 $val = (array)$val; 10327 } 10328 10329 if (!in_array($val, $tmp)) { 10330 $tmp[] = $val; 10331 } else { 10332 $duplicatekeys[] = $key; 10333 } 10334 } 10335 10336 foreach ($duplicatekeys as $key) { 10337 unset($array[$key]); 10338 } 10339 10340 return $keepkeyassoc ? $array : array_values($array); 10341 } 10342 10343 /** 10344 * Is a userid the primary administrator? 10345 * 10346 * @param int $userid int id of user to check 10347 * @return boolean 10348 */ 10349 function is_primary_admin($userid) { 10350 $primaryadmin = get_admin(); 10351 10352 if ($userid == $primaryadmin->id) { 10353 return true; 10354 } else { 10355 return false; 10356 } 10357 } 10358 10359 /** 10360 * Returns the site identifier 10361 * 10362 * @return string $CFG->siteidentifier, first making sure it is properly initialised. 10363 */ 10364 function get_site_identifier() { 10365 global $CFG; 10366 // Check to see if it is missing. If so, initialise it. 10367 if (empty($CFG->siteidentifier)) { 10368 set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']); 10369 } 10370 // Return it. 10371 return $CFG->siteidentifier; 10372 } 10373 10374 /** 10375 * Check whether the given password has no more than the specified 10376 * number of consecutive identical characters. 10377 * 10378 * @param string $password password to be checked against the password policy 10379 * @param integer $maxchars maximum number of consecutive identical characters 10380 * @return bool 10381 */ 10382 function check_consecutive_identical_characters($password, $maxchars) { 10383 10384 if ($maxchars < 1) { 10385 return true; // Zero 0 is to disable this check. 10386 } 10387 if (strlen($password) <= $maxchars) { 10388 return true; // Too short to fail this test. 10389 } 10390 10391 $previouschar = ''; 10392 $consecutivecount = 1; 10393 foreach (str_split($password) as $char) { 10394 if ($char != $previouschar) { 10395 $consecutivecount = 1; 10396 } else { 10397 $consecutivecount++; 10398 if ($consecutivecount > $maxchars) { 10399 return false; // Check failed already. 10400 } 10401 } 10402 10403 $previouschar = $char; 10404 } 10405 10406 return true; 10407 } 10408 10409 /** 10410 * Helper function to do partial function binding. 10411 * so we can use it for preg_replace_callback, for example 10412 * this works with php functions, user functions, static methods and class methods 10413 * it returns you a callback that you can pass on like so: 10414 * 10415 * $callback = partial('somefunction', $arg1, $arg2); 10416 * or 10417 * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2); 10418 * or even 10419 * $obj = new someclass(); 10420 * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2); 10421 * 10422 * and then the arguments that are passed through at calltime are appended to the argument list. 10423 * 10424 * @param mixed $function a php callback 10425 * @param mixed $arg1,... $argv arguments to partially bind with 10426 * @return array Array callback 10427 */ 10428 function partial() { 10429 if (!class_exists('partial')) { 10430 /** 10431 * Used to manage function binding. 10432 * @copyright 2009 Penny Leach 10433 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 10434 */ 10435 class partial{ 10436 /** @var array */ 10437 public $values = array(); 10438 /** @var string The function to call as a callback. */ 10439 public $func; 10440 /** 10441 * Constructor 10442 * @param string $func 10443 * @param array $args 10444 */ 10445 public function __construct($func, $args) { 10446 $this->values = $args; 10447 $this->func = $func; 10448 } 10449 /** 10450 * Calls the callback function. 10451 * @return mixed 10452 */ 10453 public function method() { 10454 $args = func_get_args(); 10455 return call_user_func_array($this->func, array_merge($this->values, $args)); 10456 } 10457 } 10458 } 10459 $args = func_get_args(); 10460 $func = array_shift($args); 10461 $p = new partial($func, $args); 10462 return array($p, 'method'); 10463 } 10464 10465 /** 10466 * helper function to load up and initialise the mnet environment 10467 * this must be called before you use mnet functions. 10468 * 10469 * @return mnet_environment the equivalent of old $MNET global 10470 */ 10471 function get_mnet_environment() { 10472 global $CFG; 10473 require_once($CFG->dirroot . '/mnet/lib.php'); 10474 static $instance = null; 10475 if (empty($instance)) { 10476 $instance = new mnet_environment(); 10477 $instance->init(); 10478 } 10479 return $instance; 10480 } 10481 10482 /** 10483 * during xmlrpc server code execution, any code wishing to access 10484 * information about the remote peer must use this to get it. 10485 * 10486 * @return mnet_remote_client the equivalent of old $MNETREMOTE_CLIENT global 10487 */ 10488 function get_mnet_remote_client() { 10489 if (!defined('MNET_SERVER')) { 10490 debugging(get_string('notinxmlrpcserver', 'mnet')); 10491 return false; 10492 } 10493 global $MNET_REMOTE_CLIENT; 10494 if (isset($MNET_REMOTE_CLIENT)) { 10495 return $MNET_REMOTE_CLIENT; 10496 } 10497 return false; 10498 } 10499 10500 /** 10501 * during the xmlrpc server code execution, this will be called 10502 * to setup the object returned by {@link get_mnet_remote_client} 10503 * 10504 * @param mnet_remote_client $client the client to set up 10505 * @throws moodle_exception 10506 */ 10507 function set_mnet_remote_client($client) { 10508 if (!defined('MNET_SERVER')) { 10509 throw new moodle_exception('notinxmlrpcserver', 'mnet'); 10510 } 10511 global $MNET_REMOTE_CLIENT; 10512 $MNET_REMOTE_CLIENT = $client; 10513 } 10514 10515 /** 10516 * return the jump url for a given remote user 10517 * this is used for rewriting forum post links in emails, etc 10518 * 10519 * @param stdclass $user the user to get the idp url for 10520 */ 10521 function mnet_get_idp_jump_url($user) { 10522 global $CFG; 10523 10524 static $mnetjumps = array(); 10525 if (!array_key_exists($user->mnethostid, $mnetjumps)) { 10526 $idp = mnet_get_peer_host($user->mnethostid); 10527 $idpjumppath = mnet_get_app_jumppath($idp->applicationid); 10528 $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl='; 10529 } 10530 return $mnetjumps[$user->mnethostid]; 10531 } 10532 10533 /** 10534 * Gets the homepage to use for the current user 10535 * 10536 * @return int One of HOMEPAGE_* 10537 */ 10538 function get_home_page() { 10539 global $CFG; 10540 10541 if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) { 10542 // If dashboard is disabled, home will be set to default page. 10543 $defaultpage = get_default_home_page(); 10544 if ($CFG->defaulthomepage == HOMEPAGE_MY) { 10545 if (!empty($CFG->enabledashboard)) { 10546 return HOMEPAGE_MY; 10547 } else { 10548 return $defaultpage; 10549 } 10550 } else if ($CFG->defaulthomepage == HOMEPAGE_MYCOURSES) { 10551 return HOMEPAGE_MYCOURSES; 10552 } else { 10553 $userhomepage = (int) get_user_preferences('user_home_page_preference', $defaultpage); 10554 if (empty($CFG->enabledashboard) && $userhomepage == HOMEPAGE_MY) { 10555 // If the user was using the dashboard but it's disabled, return the default home page. 10556 $userhomepage = $defaultpage; 10557 } 10558 return $userhomepage; 10559 } 10560 } 10561 return HOMEPAGE_SITE; 10562 } 10563 10564 /** 10565 * Returns the default home page to display if current one is not defined or can't be applied. 10566 * The default behaviour is to return Dashboard if it's enabled or My courses page if it isn't. 10567 * 10568 * @return int The default home page. 10569 */ 10570 function get_default_home_page(): int { 10571 global $CFG; 10572 10573 return !empty($CFG->enabledashboard) ? HOMEPAGE_MY : HOMEPAGE_MYCOURSES; 10574 } 10575 10576 /** 10577 * Gets the name of a course to be displayed when showing a list of courses. 10578 * By default this is just $course->fullname but user can configure it. The 10579 * result of this function should be passed through print_string. 10580 * @param stdClass|core_course_list_element $course Moodle course object 10581 * @return string Display name of course (either fullname or short + fullname) 10582 */ 10583 function get_course_display_name_for_list($course) { 10584 global $CFG; 10585 if (!empty($CFG->courselistshortnames)) { 10586 if (!($course instanceof stdClass)) { 10587 $course = (object)convert_to_array($course); 10588 } 10589 return get_string('courseextendednamedisplay', '', $course); 10590 } else { 10591 return $course->fullname; 10592 } 10593 } 10594 10595 /** 10596 * Safe analogue of unserialize() that can only parse arrays 10597 * 10598 * Arrays may contain only integers or strings as both keys and values. Nested arrays are allowed. 10599 * 10600 * @param string $expression 10601 * @return array|bool either parsed array or false if parsing was impossible. 10602 */ 10603 function unserialize_array($expression) { 10604 10605 // Check the expression is an array. 10606 if (!preg_match('/^a:(\d+):/', $expression)) { 10607 return false; 10608 } 10609 10610 $values = (array) unserialize_object($expression); 10611 10612 // Callback that returns true if the given value is an unserialized object, executes recursively. 10613 $invalidvaluecallback = static function($value) use (&$invalidvaluecallback): bool { 10614 if (is_array($value)) { 10615 return (bool) array_filter($value, $invalidvaluecallback); 10616 } 10617 return ($value instanceof stdClass) || ($value instanceof __PHP_Incomplete_Class); 10618 }; 10619 10620 // Iterate over the result to ensure there are no stray objects. 10621 if (array_filter($values, $invalidvaluecallback)) { 10622 return false; 10623 } 10624 10625 return $values; 10626 } 10627 10628 /** 10629 * Safe method for unserializing given input that is expected to contain only a serialized instance of an stdClass object 10630 * 10631 * If any class type other than stdClass is included in the input string, it will not be instantiated and will be cast to an 10632 * stdClass object. The initial cast to array, then back to object is to ensure we are always returning the correct type, 10633 * otherwise we would return an instances of {@see __PHP_Incomplete_class} for malformed strings 10634 * 10635 * @param string $input 10636 * @return stdClass 10637 */ 10638 function unserialize_object(string $input): stdClass { 10639 $instance = (array) unserialize($input, ['allowed_classes' => [stdClass::class]]); 10640 return (object) $instance; 10641 } 10642 10643 /** 10644 * The lang_string class 10645 * 10646 * This special class is used to create an object representation of a string request. 10647 * It is special because processing doesn't occur until the object is first used. 10648 * The class was created especially to aid performance in areas where strings were 10649 * required to be generated but were not necessarily used. 10650 * As an example the admin tree when generated uses over 1500 strings, of which 10651 * normally only 1/3 are ever actually printed at any time. 10652 * The performance advantage is achieved by not actually processing strings that 10653 * arn't being used, as such reducing the processing required for the page. 10654 * 10655 * How to use the lang_string class? 10656 * There are two methods of using the lang_string class, first through the 10657 * forth argument of the get_string function, and secondly directly. 10658 * The following are examples of both. 10659 * 1. Through get_string calls e.g. 10660 * $string = get_string($identifier, $component, $a, true); 10661 * $string = get_string('yes', 'moodle', null, true); 10662 * 2. Direct instantiation 10663 * $string = new lang_string($identifier, $component, $a, $lang); 10664 * $string = new lang_string('yes'); 10665 * 10666 * How do I use a lang_string object? 10667 * The lang_string object makes use of a magic __toString method so that you 10668 * are able to use the object exactly as you would use a string in most cases. 10669 * This means you are able to collect it into a variable and then directly 10670 * echo it, or concatenate it into another string, or similar. 10671 * The other thing you can do is manually get the string by calling the 10672 * lang_strings out method e.g. 10673 * $string = new lang_string('yes'); 10674 * $string->out(); 10675 * Also worth noting is that the out method can take one argument, $lang which 10676 * allows the developer to change the language on the fly. 10677 * 10678 * When should I use a lang_string object? 10679 * The lang_string object is designed to be used in any situation where a 10680 * string may not be needed, but needs to be generated. 10681 * The admin tree is a good example of where lang_string objects should be 10682 * used. 10683 * A more practical example would be any class that requries strings that may 10684 * not be printed (after all classes get renderer by renderers and who knows 10685 * what they will do ;)) 10686 * 10687 * When should I not use a lang_string object? 10688 * Don't use lang_strings when you are going to use a string immediately. 10689 * There is no need as it will be processed immediately and there will be no 10690 * advantage, and in fact perhaps a negative hit as a class has to be 10691 * instantiated for a lang_string object, however get_string won't require 10692 * that. 10693 * 10694 * Limitations: 10695 * 1. You cannot use a lang_string object as an array offset. Doing so will 10696 * result in PHP throwing an error. (You can use it as an object property!) 10697 * 10698 * @package core 10699 * @category string 10700 * @copyright 2011 Sam Hemelryk 10701 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 10702 */ 10703 class lang_string { 10704 10705 /** @var string The strings identifier */ 10706 protected $identifier; 10707 /** @var string The strings component. Default '' */ 10708 protected $component = ''; 10709 /** @var array|stdClass Any arguments required for the string. Default null */ 10710 protected $a = null; 10711 /** @var string The language to use when processing the string. Default null */ 10712 protected $lang = null; 10713 10714 /** @var string The processed string (once processed) */ 10715 protected $string = null; 10716 10717 /** 10718 * A special boolean. If set to true then the object has been woken up and 10719 * cannot be regenerated. If this is set then $this->string MUST be used. 10720 * @var bool 10721 */ 10722 protected $forcedstring = false; 10723 10724 /** 10725 * Constructs a lang_string object 10726 * 10727 * This function should do as little processing as possible to ensure the best 10728 * performance for strings that won't be used. 10729 * 10730 * @param string $identifier The strings identifier 10731 * @param string $component The strings component 10732 * @param stdClass|array|mixed $a Any arguments the string requires 10733 * @param string $lang The language to use when processing the string. 10734 * @throws coding_exception 10735 */ 10736 public function __construct($identifier, $component = '', $a = null, $lang = null) { 10737 if (empty($component)) { 10738 $component = 'moodle'; 10739 } 10740 10741 $this->identifier = $identifier; 10742 $this->component = $component; 10743 $this->lang = $lang; 10744 10745 // We MUST duplicate $a to ensure that it if it changes by reference those 10746 // changes are not carried across. 10747 // To do this we always ensure $a or its properties/values are strings 10748 // and that any properties/values that arn't convertable are forgotten. 10749 if ($a !== null) { 10750 if (is_scalar($a)) { 10751 $this->a = $a; 10752 } else if ($a instanceof lang_string) { 10753 $this->a = $a->out(); 10754 } else if (is_object($a) or is_array($a)) { 10755 $a = (array)$a; 10756 $this->a = array(); 10757 foreach ($a as $key => $value) { 10758 // Make sure conversion errors don't get displayed (results in ''). 10759 if (is_array($value)) { 10760 $this->a[$key] = ''; 10761 } else if (is_object($value)) { 10762 if (method_exists($value, '__toString')) { 10763 $this->a[$key] = $value->__toString(); 10764 } else { 10765 $this->a[$key] = ''; 10766 } 10767 } else { 10768 $this->a[$key] = (string)$value; 10769 } 10770 } 10771 } 10772 } 10773 10774 if (debugging(false, DEBUG_DEVELOPER)) { 10775 if (clean_param($this->identifier, PARAM_STRINGID) == '') { 10776 throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition'); 10777 } 10778 if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') { 10779 throw new coding_exception('Invalid string compontent. Please check your string definition'); 10780 } 10781 if (!get_string_manager()->string_exists($this->identifier, $this->component)) { 10782 debugging('String does not exist. Please check your string definition for '.$this->identifier.'/'.$this->component, DEBUG_DEVELOPER); 10783 } 10784 } 10785 } 10786 10787 /** 10788 * Processes the string. 10789 * 10790 * This function actually processes the string, stores it in the string property 10791 * and then returns it. 10792 * You will notice that this function is VERY similar to the get_string method. 10793 * That is because it is pretty much doing the same thing. 10794 * However as this function is an upgrade it isn't as tolerant to backwards 10795 * compatibility. 10796 * 10797 * @return string 10798 * @throws coding_exception 10799 */ 10800 protected function get_string() { 10801 global $CFG; 10802 10803 // Check if we need to process the string. 10804 if ($this->string === null) { 10805 // Check the quality of the identifier. 10806 if ($CFG->debugdeveloper && clean_param($this->identifier, PARAM_STRINGID) === '') { 10807 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); 10808 } 10809 10810 // Process the string. 10811 $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang); 10812 // Debugging feature lets you display string identifier and component. 10813 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) { 10814 $this->string .= ' {' . $this->identifier . '/' . $this->component . '}'; 10815 } 10816 } 10817 // Return the string. 10818 return $this->string; 10819 } 10820 10821 /** 10822 * Returns the string 10823 * 10824 * @param string $lang The langauge to use when processing the string 10825 * @return string 10826 */ 10827 public function out($lang = null) { 10828 if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) { 10829 if ($this->forcedstring) { 10830 debugging('lang_string objects that have been used cannot be printed in another language. ('.$this->lang.' used)', DEBUG_DEVELOPER); 10831 return $this->get_string(); 10832 } 10833 $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang); 10834 return $translatedstring->out(); 10835 } 10836 return $this->get_string(); 10837 } 10838 10839 /** 10840 * Magic __toString method for printing a string 10841 * 10842 * @return string 10843 */ 10844 public function __toString() { 10845 return $this->get_string(); 10846 } 10847 10848 /** 10849 * Magic __set_state method used for var_export 10850 * 10851 * @param array $array 10852 * @return self 10853 */ 10854 public static function __set_state(array $array): self { 10855 $tmp = new lang_string($array['identifier'], $array['component'], $array['a'], $array['lang']); 10856 $tmp->string = $array['string']; 10857 $tmp->forcedstring = $array['forcedstring']; 10858 return $tmp; 10859 } 10860 10861 /** 10862 * Prepares the lang_string for sleep and stores only the forcedstring and 10863 * string properties... the string cannot be regenerated so we need to ensure 10864 * it is generated for this. 10865 * 10866 * @return string 10867 */ 10868 public function __sleep() { 10869 $this->get_string(); 10870 $this->forcedstring = true; 10871 return array('forcedstring', 'string', 'lang'); 10872 } 10873 10874 /** 10875 * Returns the identifier. 10876 * 10877 * @return string 10878 */ 10879 public function get_identifier() { 10880 return $this->identifier; 10881 } 10882 10883 /** 10884 * Returns the component. 10885 * 10886 * @return string 10887 */ 10888 public function get_component() { 10889 return $this->component; 10890 } 10891 } 10892 10893 /** 10894 * Get human readable name describing the given callable. 10895 * 10896 * This performs syntax check only to see if the given param looks like a valid function, method or closure. 10897 * It does not check if the callable actually exists. 10898 * 10899 * @param callable|string|array $callable 10900 * @return string|bool Human readable name of callable, or false if not a valid callable. 10901 */ 10902 function get_callable_name($callable) { 10903 10904 if (!is_callable($callable, true, $name)) { 10905 return false; 10906 10907 } else { 10908 return $name; 10909 } 10910 } 10911 10912 /** 10913 * Tries to guess if $CFG->wwwroot is publicly accessible or not. 10914 * Never put your faith on this function and rely on its accuracy as there might be false positives. 10915 * It just performs some simple checks, and mainly is used for places where we want to hide some options 10916 * such as site registration when $CFG->wwwroot is not publicly accessible. 10917 * Good thing is there is no false negative. 10918 * Note that it's possible to force the result of this check by specifying $CFG->site_is_public in config.php 10919 * 10920 * @return bool 10921 */ 10922 function site_is_public() { 10923 global $CFG; 10924 10925 // Return early if site admin has forced this setting. 10926 if (isset($CFG->site_is_public)) { 10927 return (bool)$CFG->site_is_public; 10928 } 10929 10930 $host = parse_url($CFG->wwwroot, PHP_URL_HOST); 10931 10932 if ($host === 'localhost' || preg_match('|^127\.\d+\.\d+\.\d+$|', $host)) { 10933 $ispublic = false; 10934 } else if (\core\ip_utils::is_ip_address($host) && !ip_is_public($host)) { 10935 $ispublic = false; 10936 } else if (($address = \core\ip_utils::get_ip_address($host)) && !ip_is_public($address)) { 10937 $ispublic = false; 10938 } else { 10939 $ispublic = true; 10940 } 10941 10942 return $ispublic; 10943 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body