Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
/lib/ -> moodlelib.php (source)

Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * moodlelib.php - Moodle main library
  19   *
  20   * Main library file of miscellaneous general-purpose Moodle functions.
  21   * Other main libraries:
  22   *  - weblib.php      - functions that produce web output
  23   *  - datalib.php     - functions that access the database
  24   *
  25   * @package    core
  26   * @subpackage lib
  27   * @copyright  1999 onwards Martin Dougiamas  http://dougiamas.com
  28   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  29   */
  30  
  31  defined('MOODLE_INTERNAL') || die();
  32  
  33  // CONSTANTS (Encased in phpdoc proper comments).
  34  
  35  // Date and time constants.
  36  /**
  37   * Time constant - the number of seconds in a year
  38   */
  39  define('YEARSECS', 31536000);
  40  
  41  /**
  42   * Time constant - the number of seconds in a week
  43   */
  44  define('WEEKSECS', 604800);
  45  
  46  /**
  47   * Time constant - the number of seconds in a day
  48   */
  49  define('DAYSECS', 86400);
  50  
  51  /**
  52   * Time constant - the number of seconds in an hour
  53   */
  54  define('HOURSECS', 3600);
  55  
  56  /**
  57   * Time constant - the number of seconds in a minute
  58   */
  59  define('MINSECS', 60);
  60  
  61  /**
  62   * Time constant - the number of minutes in a day
  63   */
  64  define('DAYMINS', 1440);
  65  
  66  /**
  67   * Time constant - the number of minutes in an hour
  68   */
  69  define('HOURMINS', 60);
  70  
  71  // Parameter constants - every call to optional_param(), required_param()
  72  // or clean_param() should have a specified type of parameter.
  73  
  74  /**
  75   * PARAM_ALPHA - contains only English ascii letters [a-zA-Z].
  76   */
  77  define('PARAM_ALPHA',    'alpha');
  78  
  79  /**
  80   * PARAM_ALPHAEXT the same contents as PARAM_ALPHA (English ascii letters [a-zA-Z]) plus the chars in quotes: "_-" allowed
  81   * NOTE: originally this allowed "/" too, please use PARAM_SAFEPATH if "/" needed
  82   */
  83  define('PARAM_ALPHAEXT', 'alphaext');
  84  
  85  /**
  86   * PARAM_ALPHANUM - expected numbers 0-9 and English ascii letters [a-zA-Z] only.
  87   */
  88  define('PARAM_ALPHANUM', 'alphanum');
  89  
  90  /**
  91   * PARAM_ALPHANUMEXT - expected numbers 0-9, letters (English ascii letters [a-zA-Z]) and _- only.
  92   */
  93  define('PARAM_ALPHANUMEXT', 'alphanumext');
  94  
  95  /**
  96   * PARAM_AUTH - actually checks to make sure the string is a valid auth plugin
  97   */
  98  define('PARAM_AUTH',  'auth');
  99  
 100  /**
 101   * PARAM_BASE64 - Base 64 encoded format
 102   */
 103  define('PARAM_BASE64',   'base64');
 104  
 105  /**
 106   * PARAM_BOOL - converts input into 0 or 1, use for switches in forms and urls.
 107   */
 108  define('PARAM_BOOL',     'bool');
 109  
 110  /**
 111   * PARAM_CAPABILITY - A capability name, like 'moodle/role:manage'. Actually
 112   * checked against the list of capabilities in the database.
 113   */
 114  define('PARAM_CAPABILITY',   'capability');
 115  
 116  /**
 117   * PARAM_CLEANHTML - cleans submitted HTML code. Note that you almost never want
 118   * to use this. The normal mode of operation is to use PARAM_RAW when receiving
 119   * the input (required/optional_param or formslib) and then sanitise the HTML
 120   * using format_text on output. This is for the rare cases when you want to
 121   * sanitise the HTML on input. This cleaning may also fix xhtml strictness.
 122   */
 123  define('PARAM_CLEANHTML', 'cleanhtml');
 124  
 125  /**
 126   * PARAM_EMAIL - an email address following the RFC
 127   */
 128  define('PARAM_EMAIL',   'email');
 129  
 130  /**
 131   * PARAM_FILE - safe file name, all dangerous chars are stripped, protects against XSS, SQL injections and directory traversals
 132   */
 133  define('PARAM_FILE',   'file');
 134  
 135  /**
 136   * PARAM_FLOAT - a real/floating point number.
 137   *
 138   * Note that you should not use PARAM_FLOAT for numbers typed in by the user.
 139   * It does not work for languages that use , as a decimal separator.
 140   * Use PARAM_LOCALISEDFLOAT instead.
 141   */
 142  define('PARAM_FLOAT',  'float');
 143  
 144  /**
 145   * PARAM_LOCALISEDFLOAT - a localised real/floating point number.
 146   * This is preferred over PARAM_FLOAT for numbers typed in by the user.
 147   * Cleans localised numbers to computer readable numbers; false for invalid numbers.
 148   */
 149  define('PARAM_LOCALISEDFLOAT',  'localisedfloat');
 150  
 151  /**
 152   * PARAM_HOST - expected fully qualified domain name (FQDN) or an IPv4 dotted quad (IP address)
 153   */
 154  define('PARAM_HOST',     'host');
 155  
 156  /**
 157   * PARAM_INT - integers only, use when expecting only numbers.
 158   */
 159  define('PARAM_INT',      'int');
 160  
 161  /**
 162   * PARAM_LANG - checks to see if the string is a valid installed language in the current site.
 163   */
 164  define('PARAM_LANG',  'lang');
 165  
 166  /**
 167   * PARAM_LOCALURL - expected properly formatted URL as well as one that refers to the local server itself. (NOT orthogonal to the
 168   * others! Implies PARAM_URL!)
 169   */
 170  define('PARAM_LOCALURL', 'localurl');
 171  
 172  /**
 173   * PARAM_NOTAGS - all html tags are stripped from the text. Do not abuse this type.
 174   */
 175  define('PARAM_NOTAGS',   'notags');
 176  
 177  /**
 178   * PARAM_PATH - safe relative path name, all dangerous chars are stripped, protects against XSS, SQL injections and directory
 179   * traversals note: the leading slash is not removed, window drive letter is not allowed
 180   */
 181  define('PARAM_PATH',     'path');
 182  
 183  /**
 184   * PARAM_PEM - Privacy Enhanced Mail format
 185   */
 186  define('PARAM_PEM',      'pem');
 187  
 188  /**
 189   * PARAM_PERMISSION - A permission, one of CAP_INHERIT, CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT.
 190   */
 191  define('PARAM_PERMISSION',   'permission');
 192  
 193  /**
 194   * PARAM_RAW specifies a parameter that is not cleaned/processed in any way except the discarding of the invalid utf-8 characters
 195   */
 196  define('PARAM_RAW', 'raw');
 197  
 198  /**
 199   * PARAM_RAW_TRIMMED like PARAM_RAW but leading and trailing whitespace is stripped.
 200   */
 201  define('PARAM_RAW_TRIMMED', 'raw_trimmed');
 202  
 203  /**
 204   * PARAM_SAFEDIR - safe directory name, suitable for include() and require()
 205   */
 206  define('PARAM_SAFEDIR',  'safedir');
 207  
 208  /**
 209   * PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths
 210   * and other references to Moodle code files.
 211   *
 212   * This is NOT intended to be used for absolute paths or any user uploaded files.
 213   */
 214  define('PARAM_SAFEPATH',  'safepath');
 215  
 216  /**
 217   * PARAM_SEQUENCE - expects a sequence of numbers like 8 to 1,5,6,4,6,8,9.  Numbers and comma only.
 218   */
 219  define('PARAM_SEQUENCE',  'sequence');
 220  
 221  /**
 222   * PARAM_TAG - one tag (interests, blogs, etc.) - mostly international characters and space, <> not supported
 223   */
 224  define('PARAM_TAG',   'tag');
 225  
 226  /**
 227   * PARAM_TAGLIST - list of tags separated by commas (interests, blogs, etc.)
 228   */
 229  define('PARAM_TAGLIST',   'taglist');
 230  
 231  /**
 232   * PARAM_TEXT - general plain text compatible with multilang filter, no other html tags. Please note '<', or '>' are allowed here.
 233   */
 234  define('PARAM_TEXT',  'text');
 235  
 236  /**
 237   * PARAM_THEME - Checks to see if the string is a valid theme name in the current site
 238   */
 239  define('PARAM_THEME',  'theme');
 240  
 241  /**
 242   * PARAM_URL - expected properly formatted URL. Please note that domain part is required, http://localhost/ is not accepted but
 243   * http://localhost.localdomain/ is ok.
 244   */
 245  define('PARAM_URL',      'url');
 246  
 247  /**
 248   * PARAM_USERNAME - Clean username to only contains allowed characters. This is to be used ONLY when manually creating user
 249   * accounts, do NOT use when syncing with external systems!!
 250   */
 251  define('PARAM_USERNAME',    'username');
 252  
 253  /**
 254   * PARAM_STRINGID - used to check if the given string is valid string identifier for get_string()
 255   */
 256  define('PARAM_STRINGID',    'stringid');
 257  
 258  // DEPRECATED PARAM TYPES OR ALIASES - DO NOT USE FOR NEW CODE.
 259  /**
 260   * PARAM_CLEAN - obsoleted, please use a more specific type of parameter.
 261   * It was one of the first types, that is why it is abused so much ;-)
 262   * @deprecated since 2.0
 263   */
 264  define('PARAM_CLEAN',    'clean');
 265  
 266  /**
 267   * PARAM_INTEGER - deprecated alias for PARAM_INT
 268   * @deprecated since 2.0
 269   */
 270  define('PARAM_INTEGER',  'int');
 271  
 272  /**
 273   * PARAM_NUMBER - deprecated alias of PARAM_FLOAT
 274   * @deprecated since 2.0
 275   */
 276  define('PARAM_NUMBER',  'float');
 277  
 278  /**
 279   * PARAM_ACTION - deprecated alias for PARAM_ALPHANUMEXT, use for various actions in forms and urls
 280   * NOTE: originally alias for PARAM_APLHA
 281   * @deprecated since 2.0
 282   */
 283  define('PARAM_ACTION',   'alphanumext');
 284  
 285  /**
 286   * PARAM_FORMAT - deprecated alias for PARAM_ALPHANUMEXT, use for names of plugins, formats, etc.
 287   * NOTE: originally alias for PARAM_APLHA
 288   * @deprecated since 2.0
 289   */
 290  define('PARAM_FORMAT',   'alphanumext');
 291  
 292  /**
 293   * PARAM_MULTILANG - deprecated alias of PARAM_TEXT.
 294   * @deprecated since 2.0
 295   */
 296  define('PARAM_MULTILANG',  'text');
 297  
 298  /**
 299   * PARAM_TIMEZONE - expected timezone. Timezone can be int +-(0-13) or float +-(0.5-12.5) or
 300   * string separated by '/' and can have '-' &/ '_' (eg. America/North_Dakota/New_Salem
 301   * America/Port-au-Prince)
 302   */
 303  define('PARAM_TIMEZONE', 'timezone');
 304  
 305  /**
 306   * PARAM_CLEANFILE - deprecated alias of PARAM_FILE; originally was removing regional chars too
 307   */
 308  define('PARAM_CLEANFILE', 'file');
 309  
 310  /**
 311   * PARAM_COMPONENT is used for full component names (aka frankenstyle) such as 'mod_forum', 'core_rating', 'auth_ldap'.
 312   * Short legacy subsystem names and module names are accepted too ex: 'forum', 'rating', 'user'.
 313   * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
 314   * NOTE: numbers and underscores are strongly discouraged in plugin names!
 315   */
 316  define('PARAM_COMPONENT', 'component');
 317  
 318  /**
 319   * PARAM_AREA is a name of area used when addressing files, comments, ratings, etc.
 320   * It is usually used together with context id and component.
 321   * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
 322   */
 323  define('PARAM_AREA', 'area');
 324  
 325  /**
 326   * PARAM_PLUGIN is used for plugin names such as 'forum', 'glossary', 'ldap', 'paypal', 'completionstatus'.
 327   * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
 328   * NOTE: numbers and underscores are strongly discouraged in plugin names! Underscores are forbidden in module names.
 329   */
 330  define('PARAM_PLUGIN', 'plugin');
 331  
 332  
 333  // Web Services.
 334  
 335  /**
 336   * VALUE_REQUIRED - if the parameter is not supplied, there is an error
 337   */
 338  define('VALUE_REQUIRED', 1);
 339  
 340  /**
 341   * VALUE_OPTIONAL - if the parameter is not supplied, then the param has no value
 342   */
 343  define('VALUE_OPTIONAL', 2);
 344  
 345  /**
 346   * VALUE_DEFAULT - if the parameter is not supplied, then the default value is used
 347   */
 348  define('VALUE_DEFAULT', 0);
 349  
 350  /**
 351   * NULL_NOT_ALLOWED - the parameter can not be set to null in the database
 352   */
 353  define('NULL_NOT_ALLOWED', false);
 354  
 355  /**
 356   * NULL_ALLOWED - the parameter can be set to null in the database
 357   */
 358  define('NULL_ALLOWED', true);
 359  
 360  // Page types.
 361  
 362  /**
 363   * PAGE_COURSE_VIEW is a definition of a page type. For more information on the page class see moodle/lib/pagelib.php.
 364   */
 365  define('PAGE_COURSE_VIEW', 'course-view');
 366  
 367  /** Get remote addr constant */
 368  define('GETREMOTEADDR_SKIP_HTTP_CLIENT_IP', '1');
 369  /** Get remote addr constant */
 370  define('GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR', '2');
 371  /**
 372   * GETREMOTEADDR_SKIP_DEFAULT defines the default behavior remote IP address validation.
 373   */
 374  define('GETREMOTEADDR_SKIP_DEFAULT', GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR|GETREMOTEADDR_SKIP_HTTP_CLIENT_IP);
 375  
 376  // Blog access level constant declaration.
 377  define ('BLOG_USER_LEVEL', 1);
 378  define ('BLOG_GROUP_LEVEL', 2);
 379  define ('BLOG_COURSE_LEVEL', 3);
 380  define ('BLOG_SITE_LEVEL', 4);
 381  define ('BLOG_GLOBAL_LEVEL', 5);
 382  
 383  
 384  // Tag constants.
 385  /**
 386   * To prevent problems with multibytes strings,Flag updating in nav not working on the review page. this should not exceed the
 387   * length of "varchar(255) / 3 (bytes / utf-8 character) = 85".
 388   * TODO: this is not correct, varchar(255) are 255 unicode chars ;-)
 389   *
 390   * @todo define(TAG_MAX_LENGTH) this is not correct, varchar(255) are 255 unicode chars ;-)
 391   */
 392  define('TAG_MAX_LENGTH', 50);
 393  
 394  // Password policy constants.
 395  define ('PASSWORD_LOWER', 'abcdefghijklmnopqrstuvwxyz');
 396  define ('PASSWORD_UPPER', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
 397  define ('PASSWORD_DIGITS', '0123456789');
 398  define ('PASSWORD_NONALPHANUM', '.,;:!?_-+/*@#&$');
 399  
 400  // Feature constants.
 401  // Used for plugin_supports() to report features that are, or are not, supported by a module.
 402  
 403  /** True if module can provide a grade */
 404  define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade');
 405  /** True if module supports outcomes */
 406  define('FEATURE_GRADE_OUTCOMES', 'outcomes');
 407  /** True if module supports advanced grading methods */
 408  define('FEATURE_ADVANCED_GRADING', 'grade_advanced_grading');
 409  /** True if module controls the grade visibility over the gradebook */
 410  define('FEATURE_CONTROLS_GRADE_VISIBILITY', 'controlsgradevisbility');
 411  /** True if module supports plagiarism plugins */
 412  define('FEATURE_PLAGIARISM', 'plagiarism');
 413  
 414  /** True if module has code to track whether somebody viewed it */
 415  define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views');
 416  /** True if module has custom completion rules */
 417  define('FEATURE_COMPLETION_HAS_RULES', 'completion_has_rules');
 418  
 419  /** True if module has no 'view' page (like label) */
 420  define('FEATURE_NO_VIEW_LINK', 'viewlink');
 421  /** True (which is default) if the module wants support for setting the ID number for grade calculation purposes. */
 422  define('FEATURE_IDNUMBER', 'idnumber');
 423  /** True if module supports groups */
 424  define('FEATURE_GROUPS', 'groups');
 425  /** True if module supports groupings */
 426  define('FEATURE_GROUPINGS', 'groupings');
 427  /**
 428   * True if module supports groupmembersonly (which no longer exists)
 429   * @deprecated Since Moodle 2.8
 430   */
 431  define('FEATURE_GROUPMEMBERSONLY', 'groupmembersonly');
 432  
 433  /** Type of module */
 434  define('FEATURE_MOD_ARCHETYPE', 'mod_archetype');
 435  /** True if module supports intro editor */
 436  define('FEATURE_MOD_INTRO', 'mod_intro');
 437  /** True if module has default completion */
 438  define('FEATURE_MODEDIT_DEFAULT_COMPLETION', 'modedit_default_completion');
 439  
 440  define('FEATURE_COMMENT', 'comment');
 441  
 442  define('FEATURE_RATE', 'rate');
 443  /** True if module supports backup/restore of moodle2 format */
 444  define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2');
 445  
 446  /** True if module can show description on course main page */
 447  define('FEATURE_SHOW_DESCRIPTION', 'showdescription');
 448  
 449  /** True if module uses the question bank */
 450  define('FEATURE_USES_QUESTIONS', 'usesquestions');
 451  
 452  /**
 453   * Maximum filename char size
 454   */
 455  define('MAX_FILENAME_SIZE', 100);
 456  
 457  /** Unspecified module archetype */
 458  define('MOD_ARCHETYPE_OTHER', 0);
 459  /** Resource-like type module */
 460  define('MOD_ARCHETYPE_RESOURCE', 1);
 461  /** Assignment module archetype */
 462  define('MOD_ARCHETYPE_ASSIGNMENT', 2);
 463  /** System (not user-addable) module archetype */
 464  define('MOD_ARCHETYPE_SYSTEM', 3);
 465  
 466  /**
 467   * Security token used for allowing access
 468   * from external application such as web services.
 469   * Scripts do not use any session, performance is relatively
 470   * low because we need to load access info in each request.
 471   * Scripts are executed in parallel.
 472   */
 473  define('EXTERNAL_TOKEN_PERMANENT', 0);
 474  
 475  /**
 476   * Security token used for allowing access
 477   * of embedded applications, the code is executed in the
 478   * active user session. Token is invalidated after user logs out.
 479   * Scripts are executed serially - normal session locking is used.
 480   */
 481  define('EXTERNAL_TOKEN_EMBEDDED', 1);
 482  
 483  /**
 484   * The home page should be the site home
 485   */
 486  define('HOMEPAGE_SITE', 0);
 487  /**
 488   * The home page should be the users my page
 489   */
 490  define('HOMEPAGE_MY', 1);
 491  /**
 492   * The home page can be chosen by the user
 493   */
 494  define('HOMEPAGE_USER', 2);
 495  
 496  /**
 497   * URL of the Moodle sites registration portal.
 498   */
 499  defined('HUB_MOODLEORGHUBURL') || define('HUB_MOODLEORGHUBURL', 'https://stats.moodle.org');
 500  
 501  /**
 502   * Moodle mobile app service name
 503   */
 504  define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app');
 505  
 506  /**
 507   * Indicates the user has the capabilities required to ignore activity and course file size restrictions
 508   */
 509  define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1);
 510  
 511  /**
 512   * Course display settings: display all sections on one page.
 513   */
 514  define('COURSE_DISPLAY_SINGLEPAGE', 0);
 515  /**
 516   * Course display settings: split pages into a page per section.
 517   */
 518  define('COURSE_DISPLAY_MULTIPAGE', 1);
 519  
 520  /**
 521   * Authentication constant: String used in password field when password is not stored.
 522   */
 523  define('AUTH_PASSWORD_NOT_CACHED', 'not cached');
 524  
 525  /**
 526   * Email from header to never include via information.
 527   */
 528  define('EMAIL_VIA_NEVER', 0);
 529  
 530  /**
 531   * Email from header to always include via information.
 532   */
 533  define('EMAIL_VIA_ALWAYS', 1);
 534  
 535  /**
 536   * Email from header to only include via information if the address is no-reply.
 537   */
 538  define('EMAIL_VIA_NO_REPLY_ONLY', 2);
 539  
 540  // PARAMETER HANDLING.
 541  
 542  /**
 543   * Returns a particular value for the named variable, taken from
 544   * POST or GET.  If the parameter doesn't exist then an error is
 545   * thrown because we require this variable.
 546   *
 547   * This function should be used to initialise all required values
 548   * in a script that are based on parameters.  Usually it will be
 549   * used like this:
 550   *    $id = required_param('id', PARAM_INT);
 551   *
 552   * Please note the $type parameter is now required and the value can not be array.
 553   *
 554   * @param string $parname the name of the page parameter we want
 555   * @param string $type expected type of parameter
 556   * @return mixed
 557   * @throws coding_exception
 558   */
 559  function required_param($parname, $type) {
 560      if (func_num_args() != 2 or empty($parname) or empty($type)) {
 561          throw new coding_exception('required_param() requires $parname and $type to be specified (parameter: '.$parname.')');
 562      }
 563      // POST has precedence.
 564      if (isset($_POST[$parname])) {
 565          $param = $_POST[$parname];
 566      } else if (isset($_GET[$parname])) {
 567          $param = $_GET[$parname];
 568      } else {
 569          print_error('missingparam', '', '', $parname);
 570      }
 571  
 572      if (is_array($param)) {
 573          debugging('Invalid array parameter detected in required_param(): '.$parname);
 574          // TODO: switch to fatal error in Moodle 2.3.
 575          return required_param_array($parname, $type);
 576      }
 577  
 578      return clean_param($param, $type);
 579  }
 580  
 581  /**
 582   * Returns a particular array value for the named variable, taken from
 583   * POST or GET.  If the parameter doesn't exist then an error is
 584   * thrown because we require this variable.
 585   *
 586   * This function should be used to initialise all required values
 587   * in a script that are based on parameters.  Usually it will be
 588   * used like this:
 589   *    $ids = required_param_array('ids', PARAM_INT);
 590   *
 591   *  Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
 592   *
 593   * @param string $parname the name of the page parameter we want
 594   * @param string $type expected type of parameter
 595   * @return array
 596   * @throws coding_exception
 597   */
 598  function required_param_array($parname, $type) {
 599      if (func_num_args() != 2 or empty($parname) or empty($type)) {
 600          throw new coding_exception('required_param_array() requires $parname and $type to be specified (parameter: '.$parname.')');
 601      }
 602      // POST has precedence.
 603      if (isset($_POST[$parname])) {
 604          $param = $_POST[$parname];
 605      } else if (isset($_GET[$parname])) {
 606          $param = $_GET[$parname];
 607      } else {
 608          print_error('missingparam', '', '', $parname);
 609      }
 610      if (!is_array($param)) {
 611          print_error('missingparam', '', '', $parname);
 612      }
 613  
 614      $result = array();
 615      foreach ($param as $key => $value) {
 616          if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
 617              debugging('Invalid key name in required_param_array() detected: '.$key.', parameter: '.$parname);
 618              continue;
 619          }
 620          $result[$key] = clean_param($value, $type);
 621      }
 622  
 623      return $result;
 624  }
 625  
 626  /**
 627   * Returns a particular value for the named variable, taken from
 628   * POST or GET, otherwise returning a given default.
 629   *
 630   * This function should be used to initialise all optional values
 631   * in a script that are based on parameters.  Usually it will be
 632   * used like this:
 633   *    $name = optional_param('name', 'Fred', PARAM_TEXT);
 634   *
 635   * Please note the $type parameter is now required and the value can not be array.
 636   *
 637   * @param string $parname the name of the page parameter we want
 638   * @param mixed  $default the default value to return if nothing is found
 639   * @param string $type expected type of parameter
 640   * @return mixed
 641   * @throws coding_exception
 642   */
 643  function optional_param($parname, $default, $type) {
 644      if (func_num_args() != 3 or empty($parname) or empty($type)) {
 645          throw new coding_exception('optional_param requires $parname, $default + $type to be specified (parameter: '.$parname.')');
 646      }
 647  
 648      // POST has precedence.
 649      if (isset($_POST[$parname])) {
 650          $param = $_POST[$parname];
 651      } else if (isset($_GET[$parname])) {
 652          $param = $_GET[$parname];
 653      } else {
 654          return $default;
 655      }
 656  
 657      if (is_array($param)) {
 658          debugging('Invalid array parameter detected in required_param(): '.$parname);
 659          // TODO: switch to $default in Moodle 2.3.
 660          return optional_param_array($parname, $default, $type);
 661      }
 662  
 663      return clean_param($param, $type);
 664  }
 665  
 666  /**
 667   * Returns a particular array value for the named variable, taken from
 668   * POST or GET, otherwise returning a given default.
 669   *
 670   * This function should be used to initialise all optional values
 671   * in a script that are based on parameters.  Usually it will be
 672   * used like this:
 673   *    $ids = optional_param('id', array(), PARAM_INT);
 674   *
 675   * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
 676   *
 677   * @param string $parname the name of the page parameter we want
 678   * @param mixed $default the default value to return if nothing is found
 679   * @param string $type expected type of parameter
 680   * @return array
 681   * @throws coding_exception
 682   */
 683  function optional_param_array($parname, $default, $type) {
 684      if (func_num_args() != 3 or empty($parname) or empty($type)) {
 685          throw new coding_exception('optional_param_array requires $parname, $default + $type to be specified (parameter: '.$parname.')');
 686      }
 687  
 688      // POST has precedence.
 689      if (isset($_POST[$parname])) {
 690          $param = $_POST[$parname];
 691      } else if (isset($_GET[$parname])) {
 692          $param = $_GET[$parname];
 693      } else {
 694          return $default;
 695      }
 696      if (!is_array($param)) {
 697          debugging('optional_param_array() expects array parameters only: '.$parname);
 698          return $default;
 699      }
 700  
 701      $result = array();
 702      foreach ($param as $key => $value) {
 703          if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
 704              debugging('Invalid key name in optional_param_array() detected: '.$key.', parameter: '.$parname);
 705              continue;
 706          }
 707          $result[$key] = clean_param($value, $type);
 708      }
 709  
 710      return $result;
 711  }
 712  
 713  /**
 714   * Strict validation of parameter values, the values are only converted
 715   * to requested PHP type. Internally it is using clean_param, the values
 716   * before and after cleaning must be equal - otherwise
 717   * an invalid_parameter_exception is thrown.
 718   * Objects and classes are not accepted.
 719   *
 720   * @param mixed $param
 721   * @param string $type PARAM_ constant
 722   * @param bool $allownull are nulls valid value?
 723   * @param string $debuginfo optional debug information
 724   * @return mixed the $param value converted to PHP type
 725   * @throws invalid_parameter_exception if $param is not of given type
 726   */
 727  function validate_param($param, $type, $allownull=NULL_NOT_ALLOWED, $debuginfo='') {
 728      if (is_null($param)) {
 729          if ($allownull == NULL_ALLOWED) {
 730              return null;
 731          } else {
 732              throw new invalid_parameter_exception($debuginfo);
 733          }
 734      }
 735      if (is_array($param) or is_object($param)) {
 736          throw new invalid_parameter_exception($debuginfo);
 737      }
 738  
 739      $cleaned = clean_param($param, $type);
 740  
 741      if ($type == PARAM_FLOAT) {
 742          // Do not detect precision loss here.
 743          if (is_float($param) or is_int($param)) {
 744              // These always fit.
 745          } else if (!is_numeric($param) or !preg_match('/^[\+-]?[0-9]*\.?[0-9]*(e[-+]?[0-9]+)?$/i', (string)$param)) {
 746              throw new invalid_parameter_exception($debuginfo);
 747          }
 748      } else if ((string)$param !== (string)$cleaned) {
 749          // Conversion to string is usually lossless.
 750          throw new invalid_parameter_exception($debuginfo);
 751      }
 752  
 753      return $cleaned;
 754  }
 755  
 756  /**
 757   * Makes sure array contains only the allowed types, this function does not validate array key names!
 758   *
 759   * <code>
 760   * $options = clean_param($options, PARAM_INT);
 761   * </code>
 762   *
 763   * @param array $param the variable array we are cleaning
 764   * @param string $type expected format of param after cleaning.
 765   * @param bool $recursive clean recursive arrays
 766   * @return array
 767   * @throws coding_exception
 768   */
 769  function clean_param_array(array $param = null, $type, $recursive = false) {
 770      // Convert null to empty array.
 771      $param = (array)$param;
 772      foreach ($param as $key => $value) {
 773          if (is_array($value)) {
 774              if ($recursive) {
 775                  $param[$key] = clean_param_array($value, $type, true);
 776              } else {
 777                  throw new coding_exception('clean_param_array can not process multidimensional arrays when $recursive is false.');
 778              }
 779          } else {
 780              $param[$key] = clean_param($value, $type);
 781          }
 782      }
 783      return $param;
 784  }
 785  
 786  /**
 787   * Used by {@link optional_param()} and {@link required_param()} to
 788   * clean the variables and/or cast to specific types, based on
 789   * an options field.
 790   * <code>
 791   * $course->format = clean_param($course->format, PARAM_ALPHA);
 792   * $selectedgradeitem = clean_param($selectedgradeitem, PARAM_INT);
 793   * </code>
 794   *
 795   * @param mixed $param the variable we are cleaning
 796   * @param string $type expected format of param after cleaning.
 797   * @return mixed
 798   * @throws coding_exception
 799   */
 800  function clean_param($param, $type) {
 801      global $CFG;
 802  
 803      if (is_array($param)) {
 804          throw new coding_exception('clean_param() can not process arrays, please use clean_param_array() instead.');
 805      } else if (is_object($param)) {
 806          if (method_exists($param, '__toString')) {
 807              $param = $param->__toString();
 808          } else {
 809              throw new coding_exception('clean_param() can not process objects, please use clean_param_array() instead.');
 810          }
 811      }
 812  
 813      switch ($type) {
 814          case PARAM_RAW:
 815              // No cleaning at all.
 816              $param = fix_utf8($param);
 817              return $param;
 818  
 819          case PARAM_RAW_TRIMMED:
 820              // No cleaning, but strip leading and trailing whitespace.
 821              $param = fix_utf8($param);
 822              return trim($param);
 823  
 824          case PARAM_CLEAN:
 825              // General HTML cleaning, try to use more specific type if possible this is deprecated!
 826              // Please use more specific type instead.
 827              if (is_numeric($param)) {
 828                  return $param;
 829              }
 830              $param = fix_utf8($param);
 831              // Sweep for scripts, etc.
 832              return clean_text($param);
 833  
 834          case PARAM_CLEANHTML:
 835              // Clean html fragment.
 836              $param = fix_utf8($param);
 837              // Sweep for scripts, etc.
 838              $param = clean_text($param, FORMAT_HTML);
 839              return trim($param);
 840  
 841          case PARAM_INT:
 842              // Convert to integer.
 843              return (int)$param;
 844  
 845          case PARAM_FLOAT:
 846              // Convert to float.
 847              return (float)$param;
 848  
 849          case PARAM_LOCALISEDFLOAT:
 850              // Convert to float.
 851              return unformat_float($param, true);
 852  
 853          case PARAM_ALPHA:
 854              // Remove everything not `a-z`.
 855              return preg_replace('/[^a-zA-Z]/i', '', $param);
 856  
 857          case PARAM_ALPHAEXT:
 858              // Remove everything not `a-zA-Z_-` (originally allowed "/" too).
 859              return preg_replace('/[^a-zA-Z_-]/i', '', $param);
 860  
 861          case PARAM_ALPHANUM:
 862              // Remove everything not `a-zA-Z0-9`.
 863              return preg_replace('/[^A-Za-z0-9]/i', '', $param);
 864  
 865          case PARAM_ALPHANUMEXT:
 866              // Remove everything not `a-zA-Z0-9_-`.
 867              return preg_replace('/[^A-Za-z0-9_-]/i', '', $param);
 868  
 869          case PARAM_SEQUENCE:
 870              // Remove everything not `0-9,`.
 871              return preg_replace('/[^0-9,]/i', '', $param);
 872  
 873          case PARAM_BOOL:
 874              // Convert to 1 or 0.
 875              $tempstr = strtolower($param);
 876              if ($tempstr === 'on' or $tempstr === 'yes' or $tempstr === 'true') {
 877                  $param = 1;
 878              } else if ($tempstr === 'off' or $tempstr === 'no'  or $tempstr === 'false') {
 879                  $param = 0;
 880              } else {
 881                  $param = empty($param) ? 0 : 1;
 882              }
 883              return $param;
 884  
 885          case PARAM_NOTAGS:
 886              // Strip all tags.
 887              $param = fix_utf8($param);
 888              return strip_tags($param);
 889  
 890          case PARAM_TEXT:
 891              // Leave only tags needed for multilang.
 892              $param = fix_utf8($param);
 893              // If the multilang syntax is not correct we strip all tags because it would break xhtml strict which is required
 894              // for accessibility standards please note this cleaning does not strip unbalanced '>' for BC compatibility reasons.
 895              do {
 896                  if (strpos($param, '</lang>') !== false) {
 897                      // Old and future mutilang syntax.
 898                      $param = strip_tags($param, '<lang>');
 899                      if (!preg_match_all('/<.*>/suU', $param, $matches)) {
 900                          break;
 901                      }
 902                      $open = false;
 903                      foreach ($matches[0] as $match) {
 904                          if ($match === '</lang>') {
 905                              if ($open) {
 906                                  $open = false;
 907                                  continue;
 908                              } else {
 909                                  break 2;
 910                              }
 911                          }
 912                          if (!preg_match('/^<lang lang="[a-zA-Z0-9_-]+"\s*>$/u', $match)) {
 913                              break 2;
 914                          } else {
 915                              $open = true;
 916                          }
 917                      }
 918                      if ($open) {
 919                          break;
 920                      }
 921                      return $param;
 922  
 923                  } else if (strpos($param, '</span>') !== false) {
 924                      // Current problematic multilang syntax.
 925                      $param = strip_tags($param, '<span>');
 926                      if (!preg_match_all('/<.*>/suU', $param, $matches)) {
 927                          break;
 928                      }
 929                      $open = false;
 930                      foreach ($matches[0] as $match) {
 931                          if ($match === '</span>') {
 932                              if ($open) {
 933                                  $open = false;
 934                                  continue;
 935                              } else {
 936                                  break 2;
 937                              }
 938                          }
 939                          if (!preg_match('/^<span(\s+lang="[a-zA-Z0-9_-]+"|\s+class="multilang"){2}\s*>$/u', $match)) {
 940                              break 2;
 941                          } else {
 942                              $open = true;
 943                          }
 944                      }
 945                      if ($open) {
 946                          break;
 947                      }
 948                      return $param;
 949                  }
 950              } while (false);
 951              // Easy, just strip all tags, if we ever want to fix orphaned '&' we have to do that in format_string().
 952              return strip_tags($param);
 953  
 954          case PARAM_COMPONENT:
 955              // We do not want any guessing here, either the name is correct or not
 956              // please note only normalised component names are accepted.
 957              if (!preg_match('/^[a-z][a-z0-9]*(_[a-z][a-z0-9_]*)?[a-z0-9]+$/', $param)) {
 958                  return '';
 959              }
 960              if (strpos($param, '__') !== false) {
 961                  return '';
 962              }
 963              if (strpos($param, 'mod_') === 0) {
 964                  // Module names must not contain underscores because we need to differentiate them from invalid plugin types.
 965                  if (substr_count($param, '_') != 1) {
 966                      return '';
 967                  }
 968              }
 969              return $param;
 970  
 971          case PARAM_PLUGIN:
 972          case PARAM_AREA:
 973              // We do not want any guessing here, either the name is correct or not.
 974              if (!is_valid_plugin_name($param)) {
 975                  return '';
 976              }
 977              return $param;
 978  
 979          case PARAM_SAFEDIR:
 980              // Remove everything not a-zA-Z0-9_- .
 981              return preg_replace('/[^a-zA-Z0-9_-]/i', '', $param);
 982  
 983          case PARAM_SAFEPATH:
 984              // Remove everything not a-zA-Z0-9/_- .
 985              return preg_replace('/[^a-zA-Z0-9\/_-]/i', '', $param);
 986  
 987          case PARAM_FILE:
 988              // Strip all suspicious characters from filename.
 989              $param = fix_utf8($param);
 990              $param = preg_replace('~[[:cntrl:]]|[&<>"`\|\':\\\\/]~u', '', $param);
 991              if ($param === '.' || $param === '..') {
 992                  $param = '';
 993              }
 994              return $param;
 995  
 996          case PARAM_PATH:
 997              // Strip all suspicious characters from file path.
 998              $param = fix_utf8($param);
 999              $param = str_replace('\\', '/', $param);
1000  
1001              // Explode the path and clean each element using the PARAM_FILE rules.
1002              $breadcrumb = explode('/', $param);
1003              foreach ($breadcrumb as $key => $crumb) {
1004                  if ($crumb === '.' && $key === 0) {
1005                      // Special condition to allow for relative current path such as ./currentdirfile.txt.
1006                  } else {
1007                      $crumb = clean_param($crumb, PARAM_FILE);
1008                  }
1009                  $breadcrumb[$key] = $crumb;
1010              }
1011              $param = implode('/', $breadcrumb);
1012  
1013              // Remove multiple current path (./././) and multiple slashes (///).
1014              $param = preg_replace('~//+~', '/', $param);
1015              $param = preg_replace('~/(\./)+~', '/', $param);
1016              return $param;
1017  
1018          case PARAM_HOST:
1019              // Allow FQDN or IPv4 dotted quad.
1020              $param = preg_replace('/[^\.\d\w-]/', '', $param );
1021              // Match ipv4 dotted quad.
1022              if (preg_match('/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/', $param, $match)) {
1023                  // Confirm values are ok.
1024                  if ( $match[0] > 255
1025                       || $match[1] > 255
1026                       || $match[3] > 255
1027                       || $match[4] > 255 ) {
1028                      // Hmmm, what kind of dotted quad is this?
1029                      $param = '';
1030                  }
1031              } else if ( preg_match('/^[\w\d\.-]+$/', $param) // Dots, hyphens, numbers.
1032                         && !preg_match('/^[\.-]/',  $param) // No leading dots/hyphens.
1033                         && !preg_match('/[\.-]$/',  $param) // No trailing dots/hyphens.
1034                         ) {
1035                  // All is ok - $param is respected.
1036              } else {
1037                  // All is not ok...
1038                  $param='';
1039              }
1040              return $param;
1041  
1042          case PARAM_URL:
1043              // Allow safe urls.
1044              $param = fix_utf8($param);
1045              include_once($CFG->dirroot . '/lib/validateurlsyntax.php');
1046              if (!empty($param) && validateUrlSyntax($param, 's?H?S?F?E-u-P-a?I?p?f?q?r?')) {
1047                  // All is ok, param is respected.
1048              } else {
1049                  // Not really ok.
1050                  $param ='';
1051              }
1052              return $param;
1053  
1054          case PARAM_LOCALURL:
1055              // Allow http absolute, root relative and relative URLs within wwwroot.
1056              $param = clean_param($param, PARAM_URL);
1057              if (!empty($param)) {
1058  
1059                  if ($param === $CFG->wwwroot) {
1060                      // Exact match;
1061                  } else if (preg_match(':^/:', $param)) {
1062                      // Root-relative, ok!
1063                  } else if (preg_match('/^' . preg_quote($CFG->wwwroot . '/', '/') . '/i', $param)) {
1064                      // Absolute, and matches our wwwroot.
1065                  } else {
1066  
1067                      // Relative - let's make sure there are no tricks.
1068                      if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?') && !preg_match('/javascript:/i', $param)) {
1069                          // Looks ok.
1070                      } else {
1071                          $param = '';
1072                      }
1073                  }
1074              }
1075              return $param;
1076  
1077          case PARAM_PEM:
1078              $param = trim($param);
1079              // PEM formatted strings may contain letters/numbers and the symbols:
1080              //   forward slash: /
1081              //   plus sign:     +
1082              //   equal sign:    =
1083              //   , surrounded by BEGIN and END CERTIFICATE prefix and suffixes.
1084              if (preg_match('/^-----BEGIN CERTIFICATE-----([\s\w\/\+=]+)-----END CERTIFICATE-----$/', trim($param), $matches)) {
1085                  list($wholething, $body) = $matches;
1086                  unset($wholething, $matches);
1087                  $b64 = clean_param($body, PARAM_BASE64);
1088                  if (!empty($b64)) {
1089                      return "-----BEGIN CERTIFICATE-----\n$b64\n-----END CERTIFICATE-----\n";
1090                  } else {
1091                      return '';
1092                  }
1093              }
1094              return '';
1095  
1096          case PARAM_BASE64:
1097              if (!empty($param)) {
1098                  // PEM formatted strings may contain letters/numbers and the symbols
1099                  //   forward slash: /
1100                  //   plus sign:     +
1101                  //   equal sign:    =.
1102                  if (0 >= preg_match('/^([\s\w\/\+=]+)$/', trim($param))) {
1103                      return '';
1104                  }
1105                  $lines = preg_split('/[\s]+/', $param, -1, PREG_SPLIT_NO_EMPTY);
1106                  // Each line of base64 encoded data must be 64 characters in length, except for the last line which may be less
1107                  // than (or equal to) 64 characters long.
1108                  for ($i=0, $j=count($lines); $i < $j; $i++) {
1109                      if ($i + 1 == $j) {
1110                          if (64 < strlen($lines[$i])) {
1111                              return '';
1112                          }
1113                          continue;
1114                      }
1115  
1116                      if (64 != strlen($lines[$i])) {
1117                          return '';
1118                      }
1119                  }
1120                  return implode("\n", $lines);
1121              } else {
1122                  return '';
1123              }
1124  
1125          case PARAM_TAG:
1126              $param = fix_utf8($param);
1127              // Please note it is not safe to use the tag name directly anywhere,
1128              // it must be processed with s(), urlencode() before embedding anywhere.
1129              // Remove some nasties.
1130              $param = preg_replace('~[[:cntrl:]]|[<>`]~u', '', $param);
1131              // Convert many whitespace chars into one.
1132              $param = preg_replace('/\s+/u', ' ', $param);
1133              $param = core_text::substr(trim($param), 0, TAG_MAX_LENGTH);
1134              return $param;
1135  
1136          case PARAM_TAGLIST:
1137              $param = fix_utf8($param);
1138              $tags = explode(',', $param);
1139              $result = array();
1140              foreach ($tags as $tag) {
1141                  $res = clean_param($tag, PARAM_TAG);
1142                  if ($res !== '') {
1143                      $result[] = $res;
1144                  }
1145              }
1146              if ($result) {
1147                  return implode(',', $result);
1148              } else {
1149                  return '';
1150              }
1151  
1152          case PARAM_CAPABILITY:
1153              if (get_capability_info($param)) {
1154                  return $param;
1155              } else {
1156                  return '';
1157              }
1158  
1159          case PARAM_PERMISSION:
1160              $param = (int)$param;
1161              if (in_array($param, array(CAP_INHERIT, CAP_ALLOW, CAP_PREVENT, CAP_PROHIBIT))) {
1162                  return $param;
1163              } else {
1164                  return CAP_INHERIT;
1165              }
1166  
1167          case PARAM_AUTH:
1168              $param = clean_param($param, PARAM_PLUGIN);
1169              if (empty($param)) {
1170                  return '';
1171              } else if (exists_auth_plugin($param)) {
1172                  return $param;
1173              } else {
1174                  return '';
1175              }
1176  
1177          case PARAM_LANG:
1178              $param = clean_param($param, PARAM_SAFEDIR);
1179              if (get_string_manager()->translation_exists($param)) {
1180                  return $param;
1181              } else {
1182                  // Specified language is not installed or param malformed.
1183                  return '';
1184              }
1185  
1186          case PARAM_THEME:
1187              $param = clean_param($param, PARAM_PLUGIN);
1188              if (empty($param)) {
1189                  return '';
1190              } else if (file_exists("$CFG->dirroot/theme/$param/config.php")) {
1191                  return $param;
1192              } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$param/config.php")) {
1193                  return $param;
1194              } else {
1195                  // Specified theme is not installed.
1196                  return '';
1197              }
1198  
1199          case PARAM_USERNAME:
1200              $param = fix_utf8($param);
1201              $param = trim($param);
1202              // Convert uppercase to lowercase MDL-16919.
1203              $param = core_text::strtolower($param);
1204              if (empty($CFG->extendedusernamechars)) {
1205                  $param = str_replace(" " , "", $param);
1206                  // Regular expression, eliminate all chars EXCEPT:
1207                  // alphanum, dash (-), underscore (_), at sign (@) and period (.) characters.
1208                  $param = preg_replace('/[^-\.@_a-z0-9]/', '', $param);
1209              }
1210              return $param;
1211  
1212          case PARAM_EMAIL:
1213              $param = fix_utf8($param);
1214              if (validate_email($param)) {
1215                  return $param;
1216              } else {
1217                  return '';
1218              }
1219  
1220          case PARAM_STRINGID:
1221              if (preg_match('|^[a-zA-Z][a-zA-Z0-9\.:/_-]*$|', $param)) {
1222                  return $param;
1223              } else {
1224                  return '';
1225              }
1226  
1227          case PARAM_TIMEZONE:
1228              // Can be int, float(with .5 or .0) or string seperated by '/' and can have '-_'.
1229              $param = fix_utf8($param);
1230              $timezonepattern = '/^(([+-]?(0?[0-9](\.[5|0])?|1[0-3](\.0)?|1[0-2]\.5))|(99)|[[:alnum:]]+(\/?[[:alpha:]_-])+)$/';
1231              if (preg_match($timezonepattern, $param)) {
1232                  return $param;
1233              } else {
1234                  return '';
1235              }
1236  
1237          default:
1238              // Doh! throw error, switched parameters in optional_param or another serious problem.
1239              print_error("unknownparamtype", '', '', $type);
1240      }
1241  }
1242  
1243  /**
1244   * Whether the PARAM_* type is compatible in RTL.
1245   *
1246   * Being compatible with RTL means that the data they contain can flow
1247   * from right-to-left or left-to-right without compromising the user experience.
1248   *
1249   * Take URLs for example, they are not RTL compatible as they should always
1250   * flow from the left to the right. This also applies to numbers, email addresses,
1251   * configuration snippets, base64 strings, etc...
1252   *
1253   * This function tries to best guess which parameters can contain localised strings.
1254   *
1255   * @param string $paramtype Constant PARAM_*.
1256   * @return bool
1257   */
1258  function is_rtl_compatible($paramtype) {
1259      return $paramtype == PARAM_TEXT || $paramtype == PARAM_NOTAGS;
1260  }
1261  
1262  /**
1263   * Makes sure the data is using valid utf8, invalid characters are discarded.
1264   *
1265   * Note: this function is not intended for full objects with methods and private properties.
1266   *
1267   * @param mixed $value
1268   * @return mixed with proper utf-8 encoding
1269   */
1270  function fix_utf8($value) {
1271      if (is_null($value) or $value === '') {
1272          return $value;
1273  
1274      } else if (is_string($value)) {
1275          if ((string)(int)$value === $value) {
1276              // Shortcut.
1277              return $value;
1278          }
1279          // No null bytes expected in our data, so let's remove it.
1280          $value = str_replace("\0", '', $value);
1281  
1282          // Note: this duplicates min_fix_utf8() intentionally.
1283          static $buggyiconv = null;
1284          if ($buggyiconv === null) {
1285              $buggyiconv = (!function_exists('iconv') or @iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€');
1286          }
1287  
1288          if ($buggyiconv) {
1289              if (function_exists('mb_convert_encoding')) {
1290                  $subst = mb_substitute_character();
1291                  mb_substitute_character('');
1292                  $result = mb_convert_encoding($value, 'utf-8', 'utf-8');
1293                  mb_substitute_character($subst);
1294  
1295              } else {
1296                  // Warn admins on admin/index.php page.
1297                  $result = $value;
1298              }
1299  
1300          } else {
1301              $result = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
1302          }
1303  
1304          return $result;
1305  
1306      } else if (is_array($value)) {
1307          foreach ($value as $k => $v) {
1308              $value[$k] = fix_utf8($v);
1309          }
1310          return $value;
1311  
1312      } else if (is_object($value)) {
1313          // Do not modify original.
1314          $value = clone($value);
1315          foreach ($value as $k => $v) {
1316              $value->$k = fix_utf8($v);
1317          }
1318          return $value;
1319  
1320      } else {
1321          // This is some other type, no utf-8 here.
1322          return $value;
1323      }
1324  }
1325  
1326  /**
1327   * Return true if given value is integer or string with integer value
1328   *
1329   * @param mixed $value String or Int
1330   * @return bool true if number, false if not
1331   */
1332  function is_number($value) {
1333      if (is_int($value)) {
1334          return true;
1335      } else if (is_string($value)) {
1336          return ((string)(int)$value) === $value;
1337      } else {
1338          return false;
1339      }
1340  }
1341  
1342  /**
1343   * Returns host part from url.
1344   *
1345   * @param string $url full url
1346   * @return string host, null if not found
1347   */
1348  function get_host_from_url($url) {
1349      preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches);
1350      if ($matches) {
1351          return $matches[1];
1352      }
1353      return null;
1354  }
1355  
1356  /**
1357   * Tests whether anything was returned by text editor
1358   *
1359   * This function is useful for testing whether something you got back from
1360   * the HTML editor actually contains anything. Sometimes the HTML editor
1361   * appear to be empty, but actually you get back a <br> tag or something.
1362   *
1363   * @param string $string a string containing HTML.
1364   * @return boolean does the string contain any actual content - that is text,
1365   * images, objects, etc.
1366   */
1367  function html_is_blank($string) {
1368      return trim(strip_tags($string, '<img><object><applet><input><select><textarea><hr>')) == '';
1369  }
1370  
1371  /**
1372   * Set a key in global configuration
1373   *
1374   * Set a key/value pair in both this session's {@link $CFG} global variable
1375   * and in the 'config' database table for future sessions.
1376   *
1377   * Can also be used to update keys for plugin-scoped configs in config_plugin table.
1378   * In that case it doesn't affect $CFG.
1379   *
1380   * A NULL value will delete the entry.
1381   *
1382   * NOTE: this function is called from lib/db/upgrade.php
1383   *
1384   * @param string $name the key to set
1385   * @param string $value the value to set (without magic quotes)
1386   * @param string $plugin (optional) the plugin scope, default null
1387   * @return bool true or exception
1388   */
1389  function set_config($name, $value, $plugin=null) {
1390      global $CFG, $DB;
1391  
1392      if (empty($plugin)) {
1393          if (!array_key_exists($name, $CFG->config_php_settings)) {
1394              // So it's defined for this invocation at least.
1395              if (is_null($value)) {
1396                  unset($CFG->$name);
1397              } else {
1398                  // Settings from db are always strings.
1399                  $CFG->$name = (string)$value;
1400              }
1401          }
1402  
1403          if ($DB->get_field('config', 'name', array('name' => $name))) {
1404              if ($value === null) {
1405                  $DB->delete_records('config', array('name' => $name));
1406              } else {
1407                  $DB->set_field('config', 'value', $value, array('name' => $name));
1408              }
1409          } else {
1410              if ($value !== null) {
1411                  $config = new stdClass();
1412                  $config->name  = $name;
1413                  $config->value = $value;
1414                  $DB->insert_record('config', $config, false);
1415              }
1416              // When setting config during a Behat test (in the CLI script, not in the web browser
1417              // requests), remember which ones are set so that we can clear them later.
1418              if (defined('BEHAT_TEST')) {
1419                  if (!property_exists($CFG, 'behat_cli_added_config')) {
1420                      $CFG->behat_cli_added_config = [];
1421                  }
1422                  $CFG->behat_cli_added_config[$name] = true;
1423              }
1424          }
1425          if ($name === 'siteidentifier') {
1426              cache_helper::update_site_identifier($value);
1427          }
1428          cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1429      } else {
1430          // Plugin scope.
1431          if ($id = $DB->get_field('config_plugins', 'id', array('name' => $name, 'plugin' => $plugin))) {
1432              if ($value===null) {
1433                  $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
1434              } else {
1435                  $DB->set_field('config_plugins', 'value', $value, array('id' => $id));
1436              }
1437          } else {
1438              if ($value !== null) {
1439                  $config = new stdClass();
1440                  $config->plugin = $plugin;
1441                  $config->name   = $name;
1442                  $config->value  = $value;
1443                  $DB->insert_record('config_plugins', $config, false);
1444              }
1445          }
1446          cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1447      }
1448  
1449      return true;
1450  }
1451  
1452  /**
1453   * Get configuration values from the global config table
1454   * or the config_plugins table.
1455   *
1456   * If called with one parameter, it will load all the config
1457   * variables for one plugin, and return them as an object.
1458   *
1459   * If called with 2 parameters it will return a string single
1460   * value or false if the value is not found.
1461   *
1462   * NOTE: this function is called from lib/db/upgrade.php
1463   *
1464   * @static string|false $siteidentifier The site identifier is not cached. We use this static cache so
1465   *     that we need only fetch it once per request.
1466   * @param string $plugin full component name
1467   * @param string $name default null
1468   * @return mixed hash-like object or single value, return false no config found
1469   * @throws dml_exception
1470   */
1471  function get_config($plugin, $name = null) {
1472      global $CFG, $DB;
1473  
1474      static $siteidentifier = null;
1475  
1476      if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) {
1477          $forced =& $CFG->config_php_settings;
1478          $iscore = true;
1479          $plugin = 'core';
1480      } else {
1481          if (array_key_exists($plugin, $CFG->forced_plugin_settings)) {
1482              $forced =& $CFG->forced_plugin_settings[$plugin];
1483          } else {
1484              $forced = array();
1485          }
1486          $iscore = false;
1487      }
1488  
1489      if ($siteidentifier === null) {
1490          try {
1491              // This may fail during installation.
1492              // If you have a look at {@link initialise_cfg()} you will see that this is how we detect the need to
1493              // install the database.
1494              $siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
1495          } catch (dml_exception $ex) {
1496              // Set siteidentifier to false. We don't want to trip this continually.
1497              $siteidentifier = false;
1498              throw $ex;
1499          }
1500      }
1501  
1502      if (!empty($name)) {
1503          if (array_key_exists($name, $forced)) {
1504              return (string)$forced[$name];
1505          } else if ($name === 'siteidentifier' && $plugin == 'core') {
1506              return $siteidentifier;
1507          }
1508      }
1509  
1510      $cache = cache::make('core', 'config');
1511      $result = $cache->get($plugin);
1512      if ($result === false) {
1513          // The user is after a recordset.
1514          if (!$iscore) {
1515              $result = $DB->get_records_menu('config_plugins', array('plugin' => $plugin), '', 'name,value');
1516          } else {
1517              // This part is not really used any more, but anyway...
1518              $result = $DB->get_records_menu('config', array(), '', 'name,value');;
1519          }
1520          $cache->set($plugin, $result);
1521      }
1522  
1523      if (!empty($name)) {
1524          if (array_key_exists($name, $result)) {
1525              return $result[$name];
1526          }
1527          return false;
1528      }
1529  
1530      if ($plugin === 'core') {
1531          $result['siteidentifier'] = $siteidentifier;
1532      }
1533  
1534      foreach ($forced as $key => $value) {
1535          if (is_null($value) or is_array($value) or is_object($value)) {
1536              // We do not want any extra mess here, just real settings that could be saved in db.
1537              unset($result[$key]);
1538          } else {
1539              // Convert to string as if it went through the DB.
1540              $result[$key] = (string)$value;
1541          }
1542      }
1543  
1544      return (object)$result;
1545  }
1546  
1547  /**
1548   * Removes a key from global configuration.
1549   *
1550   * NOTE: this function is called from lib/db/upgrade.php
1551   *
1552   * @param string $name the key to set
1553   * @param string $plugin (optional) the plugin scope
1554   * @return boolean whether the operation succeeded.
1555   */
1556  function unset_config($name, $plugin=null) {
1557      global $CFG, $DB;
1558  
1559      if (empty($plugin)) {
1560          unset($CFG->$name);
1561          $DB->delete_records('config', array('name' => $name));
1562          cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1563      } else {
1564          $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
1565          cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1566      }
1567  
1568      return true;
1569  }
1570  
1571  /**
1572   * Remove all the config variables for a given plugin.
1573   *
1574   * NOTE: this function is called from lib/db/upgrade.php
1575   *
1576   * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice';
1577   * @return boolean whether the operation succeeded.
1578   */
1579  function unset_all_config_for_plugin($plugin) {
1580      global $DB;
1581      // Delete from the obvious config_plugins first.
1582      $DB->delete_records('config_plugins', array('plugin' => $plugin));
1583      // Next delete any suspect settings from config.
1584      $like = $DB->sql_like('name', '?', true, true, false, '|');
1585      $params = array($DB->sql_like_escape($plugin.'_', '|') . '%');
1586      $DB->delete_records_select('config', $like, $params);
1587      // Finally clear both the plugin cache and the core cache (suspect settings now removed from core).
1588      cache_helper::invalidate_by_definition('core', 'config', array(), array('core', $plugin));
1589  
1590      return true;
1591  }
1592  
1593  /**
1594   * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability.
1595   *
1596   * All users are verified if they still have the necessary capability.
1597   *
1598   * @param string $value the value of the config setting.
1599   * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor.
1600   * @param bool $includeadmins include administrators.
1601   * @return array of user objects.
1602   */
1603  function get_users_from_config($value, $capability, $includeadmins = true) {
1604      if (empty($value) or $value === '$@NONE@$') {
1605          return array();
1606      }
1607  
1608      // We have to make sure that users still have the necessary capability,
1609      // it should be faster to fetch them all first and then test if they are present
1610      // instead of validating them one-by-one.
1611      $users = get_users_by_capability(context_system::instance(), $capability);
1612      if ($includeadmins) {
1613          $admins = get_admins();
1614          foreach ($admins as $admin) {
1615              $users[$admin->id] = $admin;
1616          }
1617      }
1618  
1619      if ($value === '$@ALL@$') {
1620          return $users;
1621      }
1622  
1623      $result = array(); // Result in correct order.
1624      $allowed = explode(',', $value);
1625      foreach ($allowed as $uid) {
1626          if (isset($users[$uid])) {
1627              $user = $users[$uid];
1628              $result[$user->id] = $user;
1629          }
1630      }
1631  
1632      return $result;
1633  }
1634  
1635  
1636  /**
1637   * Invalidates browser caches and cached data in temp.
1638   *
1639   * @return void
1640   */
1641  function purge_all_caches() {
1642      purge_caches();
1643  }
1644  
1645  /**
1646   * Selectively invalidate different types of cache.
1647   *
1648   * Purges the cache areas specified.  By default, this will purge all caches but can selectively purge specific
1649   * areas alone or in combination.
1650   *
1651   * @param bool[] $options Specific parts of the cache to purge. Valid options are:
1652   *        'muc'    Purge MUC caches?
1653   *        'theme'  Purge theme cache?
1654   *        'lang'   Purge language string cache?
1655   *        'js'     Purge javascript cache?
1656   *        'filter' Purge text filter cache?
1657   *        'other'  Purge all other caches?
1658   */
1659  function purge_caches($options = []) {
1660      $defaults = array_fill_keys(['muc', 'theme', 'lang', 'js', 'template', 'filter', 'other'], false);
1661      if (empty(array_filter($options))) {
1662          $options = array_fill_keys(array_keys($defaults), true); // Set all options to true.
1663      } else {
1664          $options = array_merge($defaults, array_intersect_key($options, $defaults)); // Override defaults with specified options.
1665      }
1666      if ($options['muc']) {
1667          cache_helper::purge_all();
1668      }
1669      if ($options['theme']) {
1670          theme_reset_all_caches();
1671      }
1672      if ($options['lang']) {
1673          get_string_manager()->reset_caches();
1674      }
1675      if ($options['js']) {
1676          js_reset_all_caches();
1677      }
1678      if ($options['template']) {
1679          template_reset_all_caches();
1680      }
1681      if ($options['filter']) {
1682          reset_text_filters_cache();
1683      }
1684      if ($options['other']) {
1685          purge_other_caches();
1686      }
1687  }
1688  
1689  /**
1690   * Purge all non-MUC caches not otherwise purged in purge_caches.
1691   *
1692   * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
1693   * {@link phpunit_util::reset_dataroot()}
1694   */
1695  function purge_other_caches() {
1696      global $DB, $CFG;
1697      core_text::reset_caches();
1698      if (class_exists('core_plugin_manager')) {
1699          core_plugin_manager::reset_caches();
1700      }
1701  
1702      // Bump up cacherev field for all courses.
1703      try {
1704          increment_revision_number('course', 'cacherev', '');
1705      } catch (moodle_exception $e) {
1706          // Ignore exception since this function is also called before upgrade script when field course.cacherev does not exist yet.
1707      }
1708  
1709      $DB->reset_caches();
1710  
1711      // Purge all other caches: rss, simplepie, etc.
1712      clearstatcache();
1713      remove_dir($CFG->cachedir.'', true);
1714  
1715      // Make sure cache dir is writable, throws exception if not.
1716      make_cache_directory('');
1717  
1718      // This is the only place where we purge local caches, we are only adding files there.
1719      // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes.
1720      remove_dir($CFG->localcachedir, true);
1721      set_config('localcachedirpurged', time());
1722      make_localcache_directory('', true);
1723      \core\task\manager::clear_static_caches();
1724  }
1725  
1726  /**
1727   * Get volatile flags
1728   *
1729   * @param string $type
1730   * @param int $changedsince default null
1731   * @return array records array
1732   */
1733  function get_cache_flags($type, $changedsince = null) {
1734      global $DB;
1735  
1736      $params = array('type' => $type, 'expiry' => time());
1737      $sqlwhere = "flagtype = :type AND expiry >= :expiry";
1738      if ($changedsince !== null) {
1739          $params['changedsince'] = $changedsince;
1740          $sqlwhere .= " AND timemodified > :changedsince";
1741      }
1742      $cf = array();
1743      if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) {
1744          foreach ($flags as $flag) {
1745              $cf[$flag->name] = $flag->value;
1746          }
1747      }
1748      return $cf;
1749  }
1750  
1751  /**
1752   * Get volatile flags
1753   *
1754   * @param string $type
1755   * @param string $name
1756   * @param int $changedsince default null
1757   * @return string|false The cache flag value or false
1758   */
1759  function get_cache_flag($type, $name, $changedsince=null) {
1760      global $DB;
1761  
1762      $params = array('type' => $type, 'name' => $name, 'expiry' => time());
1763  
1764      $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry";
1765      if ($changedsince !== null) {
1766          $params['changedsince'] = $changedsince;
1767          $sqlwhere .= " AND timemodified > :changedsince";
1768      }
1769  
1770      return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params);
1771  }
1772  
1773  /**
1774   * Set a volatile flag
1775   *
1776   * @param string $type the "type" namespace for the key
1777   * @param string $name the key to set
1778   * @param string $value the value to set (without magic quotes) - null will remove the flag
1779   * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs
1780   * @return bool Always returns true
1781   */
1782  function set_cache_flag($type, $name, $value, $expiry = null) {
1783      global $DB;
1784  
1785      $timemodified = time();
1786      if ($expiry === null || $expiry < $timemodified) {
1787          $expiry = $timemodified + 24 * 60 * 60;
1788      } else {
1789          $expiry = (int)$expiry;
1790      }
1791  
1792      if ($value === null) {
1793          unset_cache_flag($type, $name);
1794          return true;
1795      }
1796  
1797      if ($f = $DB->get_record('cache_flags', array('name' => $name, 'flagtype' => $type), '*', IGNORE_MULTIPLE)) {
1798          // This is a potential problem in DEBUG_DEVELOPER.
1799          if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) {
1800              return true; // No need to update.
1801          }
1802          $f->value        = $value;
1803          $f->expiry       = $expiry;
1804          $f->timemodified = $timemodified;
1805          $DB->update_record('cache_flags', $f);
1806      } else {
1807          $f = new stdClass();
1808          $f->flagtype     = $type;
1809          $f->name         = $name;
1810          $f->value        = $value;
1811          $f->expiry       = $expiry;
1812          $f->timemodified = $timemodified;
1813          $DB->insert_record('cache_flags', $f);
1814      }
1815      return true;
1816  }
1817  
1818  /**
1819   * Removes a single volatile flag
1820   *
1821   * @param string $type the "type" namespace for the key
1822   * @param string $name the key to set
1823   * @return bool
1824   */
1825  function unset_cache_flag($type, $name) {
1826      global $DB;
1827      $DB->delete_records('cache_flags', array('name' => $name, 'flagtype' => $type));
1828      return true;
1829  }
1830  
1831  /**
1832   * Garbage-collect volatile flags
1833   *
1834   * @return bool Always returns true
1835   */
1836  function gc_cache_flags() {
1837      global $DB;
1838      $DB->delete_records_select('cache_flags', 'expiry < ?', array(time()));
1839      return true;
1840  }
1841  
1842  // USER PREFERENCE API.
1843  
1844  /**
1845   * Refresh user preference cache. This is used most often for $USER
1846   * object that is stored in session, but it also helps with performance in cron script.
1847   *
1848   * Preferences for each user are loaded on first use on every page, then again after the timeout expires.
1849   *
1850   * @package  core
1851   * @category preference
1852   * @access   public
1853   * @param    stdClass         $user          User object. Preferences are preloaded into 'preference' property
1854   * @param    int              $cachelifetime Cache life time on the current page (in seconds)
1855   * @throws   coding_exception
1856   * @return   null
1857   */
1858  function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120) {
1859      global $DB;
1860      // Static cache, we need to check on each page load, not only every 2 minutes.
1861      static $loadedusers = array();
1862  
1863      if (!isset($user->id)) {
1864          throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field');
1865      }
1866  
1867      if (empty($user->id) or isguestuser($user->id)) {
1868          // No permanent storage for not-logged-in users and guest.
1869          if (!isset($user->preference)) {
1870              $user->preference = array();
1871          }
1872          return;
1873      }
1874  
1875      $timenow = time();
1876  
1877      if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) {
1878          // Already loaded at least once on this page. Are we up to date?
1879          if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) {
1880              // No need to reload - we are on the same page and we loaded prefs just a moment ago.
1881              return;
1882  
1883          } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) {
1884              // No change since the lastcheck on this page.
1885              $user->preference['_lastloaded'] = $timenow;
1886              return;
1887          }
1888      }
1889  
1890      // OK, so we have to reload all preferences.
1891      $loadedusers[$user->id] = true;
1892      $user->preference = $DB->get_records_menu('user_preferences', array('userid' => $user->id), '', 'name,value'); // All values.
1893      $user->preference['_lastloaded'] = $timenow;
1894  }
1895  
1896  /**
1897   * Called from set/unset_user_preferences, so that the prefs can be correctly reloaded in different sessions.
1898   *
1899   * NOTE: internal function, do not call from other code.
1900   *
1901   * @package core
1902   * @access private
1903   * @param integer $userid the user whose prefs were changed.
1904   */
1905  function mark_user_preferences_changed($userid) {
1906      global $CFG;
1907  
1908      if (empty($userid) or isguestuser($userid)) {
1909          // No cache flags for guest and not-logged-in users.
1910          return;
1911      }
1912  
1913      set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout);
1914  }
1915  
1916  /**
1917   * Sets a preference for the specified user.
1918   *
1919   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1920   *
1921   * When additional validation/permission check is needed it is better to use {@see useredit_update_user_preference()}
1922   *
1923   * @package  core
1924   * @category preference
1925   * @access   public
1926   * @param    string            $name  The key to set as preference for the specified user
1927   * @param    string            $value The value to set for the $name key in the specified user's
1928   *                                    record, null means delete current value.
1929   * @param    stdClass|int|null $user  A moodle user object or id, null means current user
1930   * @throws   coding_exception
1931   * @return   bool                     Always true or exception
1932   */
1933  function set_user_preference($name, $value, $user = null) {
1934      global $USER, $DB;
1935  
1936      if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1937          throw new coding_exception('Invalid preference name in set_user_preference() call');
1938      }
1939  
1940      if (is_null($value)) {
1941          // Null means delete current.
1942          return unset_user_preference($name, $user);
1943      } else if (is_object($value)) {
1944          throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed');
1945      } else if (is_array($value)) {
1946          throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed');
1947      }
1948      // Value column maximum length is 1333 characters.
1949      $value = (string)$value;
1950      if (core_text::strlen($value) > 1333) {
1951          throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column');
1952      }
1953  
1954      if (is_null($user)) {
1955          $user = $USER;
1956      } else if (isset($user->id)) {
1957          // It is a valid object.
1958      } else if (is_numeric($user)) {
1959          $user = (object)array('id' => (int)$user);
1960      } else {
1961          throw new coding_exception('Invalid $user parameter in set_user_preference() call');
1962      }
1963  
1964      check_user_preferences_loaded($user);
1965  
1966      if (empty($user->id) or isguestuser($user->id)) {
1967          // No permanent storage for not-logged-in users and guest.
1968          $user->preference[$name] = $value;
1969          return true;
1970      }
1971  
1972      if ($preference = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => $name))) {
1973          if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) {
1974              // Preference already set to this value.
1975              return true;
1976          }
1977          $DB->set_field('user_preferences', 'value', $value, array('id' => $preference->id));
1978  
1979      } else {
1980          $preference = new stdClass();
1981          $preference->userid = $user->id;
1982          $preference->name   = $name;
1983          $preference->value  = $value;
1984          $DB->insert_record('user_preferences', $preference);
1985      }
1986  
1987      // Update value in cache.
1988      $user->preference[$name] = $value;
1989      // Update the $USER in case where we've not a direct reference to $USER.
1990      if ($user !== $USER && $user->id == $USER->id) {
1991          $USER->preference[$name] = $value;
1992      }
1993  
1994      // Set reload flag for other sessions.
1995      mark_user_preferences_changed($user->id);
1996  
1997      return true;
1998  }
1999  
2000  /**
2001   * Sets a whole array of preferences for the current user
2002   *
2003   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
2004   *
2005   * @package  core
2006   * @category preference
2007   * @access   public
2008   * @param    array             $prefarray An array of key/value pairs to be set
2009   * @param    stdClass|int|null $user      A moodle user object or id, null means current user
2010   * @return   bool                         Always true or exception
2011   */
2012  function set_user_preferences(array $prefarray, $user = null) {
2013      foreach ($prefarray as $name => $value) {
2014          set_user_preference($name, $value, $user);
2015      }
2016      return true;
2017  }
2018  
2019  /**
2020   * Unsets a preference completely by deleting it from the database
2021   *
2022   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
2023   *
2024   * @package  core
2025   * @category preference
2026   * @access   public
2027   * @param    string            $name The key to unset as preference for the specified user
2028   * @param    stdClass|int|null $user A moodle user object or id, null means current user
2029   * @throws   coding_exception
2030   * @return   bool                    Always true or exception
2031   */
2032  function unset_user_preference($name, $user = null) {
2033      global $USER, $DB;
2034  
2035      if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
2036          throw new coding_exception('Invalid preference name in unset_user_preference() call');
2037      }
2038  
2039      if (is_null($user)) {
2040          $user = $USER;
2041      } else if (isset($user->id)) {
2042          // It is a valid object.
2043      } else if (is_numeric($user)) {
2044          $user = (object)array('id' => (int)$user);
2045      } else {
2046          throw new coding_exception('Invalid $user parameter in unset_user_preference() call');
2047      }
2048  
2049      check_user_preferences_loaded($user);
2050  
2051      if (empty($user->id) or isguestuser($user->id)) {
2052          // No permanent storage for not-logged-in user and guest.
2053          unset($user->preference[$name]);
2054          return true;
2055      }
2056  
2057      // Delete from DB.
2058      $DB->delete_records('user_preferences', array('userid' => $user->id, 'name' => $name));
2059  
2060      // Delete the preference from cache.
2061      unset($user->preference[$name]);
2062      // Update the $USER in case where we've not a direct reference to $USER.
2063      if ($user !== $USER && $user->id == $USER->id) {
2064          unset($USER->preference[$name]);
2065      }
2066  
2067      // Set reload flag for other sessions.
2068      mark_user_preferences_changed($user->id);
2069  
2070      return true;
2071  }
2072  
2073  /**
2074   * Used to fetch user preference(s)
2075   *
2076   * If no arguments are supplied this function will return
2077   * all of the current user preferences as an array.
2078   *
2079   * If a name is specified then this function
2080   * attempts to return that particular preference value.  If
2081   * none is found, then the optional value $default is returned,
2082   * otherwise null.
2083   *
2084   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
2085   *
2086   * @package  core
2087   * @category preference
2088   * @access   public
2089   * @param    string            $name    Name of the key to use in finding a preference value
2090   * @param    mixed|null        $default Value to be returned if the $name key is not set in the user preferences
2091   * @param    stdClass|int|null $user    A moodle user object or id, null means current user
2092   * @throws   coding_exception
2093   * @return   string|mixed|null          A string containing the value of a single preference. An
2094   *                                      array with all of the preferences or null
2095   */
2096  function get_user_preferences($name = null, $default = null, $user = null) {
2097      global $USER;
2098  
2099      if (is_null($name)) {
2100          // All prefs.
2101      } else if (is_numeric($name) or $name === '_lastloaded') {
2102          throw new coding_exception('Invalid preference name in get_user_preferences() call');
2103      }
2104  
2105      if (is_null($user)) {
2106          $user = $USER;
2107      } else if (isset($user->id)) {
2108          // Is a valid object.
2109      } else if (is_numeric($user)) {
2110          if ($USER->id == $user) {
2111              $user = $USER;
2112          } else {
2113              $user = (object)array('id' => (int)$user);
2114          }
2115      } else {
2116          throw new coding_exception('Invalid $user parameter in get_user_preferences() call');
2117      }
2118  
2119      check_user_preferences_loaded($user);
2120  
2121      if (empty($name)) {
2122          // All values.
2123          return $user->preference;
2124      } else if (isset($user->preference[$name])) {
2125          // The single string value.
2126          return $user->preference[$name];
2127      } else {
2128          // Default value (null if not specified).
2129          return $default;
2130      }
2131  }
2132  
2133  // FUNCTIONS FOR HANDLING TIME.
2134  
2135  /**
2136   * Given Gregorian date parts in user time produce a GMT timestamp.
2137   *
2138   * @package core
2139   * @category time
2140   * @param int $year The year part to create timestamp of
2141   * @param int $month The month part to create timestamp of
2142   * @param int $day The day part to create timestamp of
2143   * @param int $hour The hour part to create timestamp of
2144   * @param int $minute The minute part to create timestamp of
2145   * @param int $second The second part to create timestamp of
2146   * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset.
2147   *             if 99 then default user's timezone is used {@link http://docs.moodle.org/dev/Time_API#Timezone}
2148   * @param bool $applydst Toggle Daylight Saving Time, default true, will be
2149   *             applied only if timezone is 99 or string.
2150   * @return int GMT timestamp
2151   */
2152  function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) {
2153      $date = new DateTime('now', core_date::get_user_timezone_object($timezone));
2154      $date->setDate((int)$year, (int)$month, (int)$day);
2155      $date->setTime((int)$hour, (int)$minute, (int)$second);
2156  
2157      $time = $date->getTimestamp();
2158  
2159      if ($time === false) {
2160          throw new coding_exception('getTimestamp() returned false, please ensure you have passed correct values.'.
2161              ' This can fail if year is more than 2038 and OS is 32 bit windows');
2162      }
2163  
2164      // Moodle BC DST stuff.
2165      if (!$applydst) {
2166          $time += dst_offset_on($time, $timezone);
2167      }
2168  
2169      return $time;
2170  
2171  }
2172  
2173  /**
2174   * Format a date/time (seconds) as weeks, days, hours etc as needed
2175   *
2176   * Given an amount of time in seconds, returns string
2177   * formatted nicely as years, days, hours etc as needed
2178   *
2179   * @package core
2180   * @category time
2181   * @uses MINSECS
2182   * @uses HOURSECS
2183   * @uses DAYSECS
2184   * @uses YEARSECS
2185   * @param int $totalsecs Time in seconds
2186   * @param stdClass $str Should be a time object
2187   * @return string A nicely formatted date/time string
2188   */
2189  function format_time($totalsecs, $str = null) {
2190  
2191      $totalsecs = abs($totalsecs);
2192  
2193      if (!$str) {
2194          // Create the str structure the slow way.
2195          $str = new stdClass();
2196          $str->day   = get_string('day');
2197          $str->days  = get_string('days');
2198          $str->hour  = get_string('hour');
2199          $str->hours = get_string('hours');
2200          $str->min   = get_string('min');
2201          $str->mins  = get_string('mins');
2202          $str->sec   = get_string('sec');
2203          $str->secs  = get_string('secs');
2204          $str->year  = get_string('year');
2205          $str->years = get_string('years');
2206      }
2207  
2208      $years     = floor($totalsecs/YEARSECS);
2209      $remainder = $totalsecs - ($years*YEARSECS);
2210      $days      = floor($remainder/DAYSECS);
2211      $remainder = $totalsecs - ($days*DAYSECS);
2212      $hours     = floor($remainder/HOURSECS);
2213      $remainder = $remainder - ($hours*HOURSECS);
2214      $mins      = floor($remainder/MINSECS);
2215      $secs      = $remainder - ($mins*MINSECS);
2216  
2217      $ss = ($secs == 1)  ? $str->sec  : $str->secs;
2218      $sm = ($mins == 1)  ? $str->min  : $str->mins;
2219      $sh = ($hours == 1) ? $str->hour : $str->hours;
2220      $sd = ($days == 1)  ? $str->day  : $str->days;
2221      $sy = ($years == 1)  ? $str->year  : $str->years;
2222  
2223      $oyears = '';
2224      $odays = '';
2225      $ohours = '';
2226      $omins = '';
2227      $osecs = '';
2228  
2229      if ($years) {
2230          $oyears  = $years .' '. $sy;
2231      }
2232      if ($days) {
2233          $odays  = $days .' '. $sd;
2234      }
2235      if ($hours) {
2236          $ohours = $hours .' '. $sh;
2237      }
2238      if ($mins) {
2239          $omins  = $mins .' '. $sm;
2240      }
2241      if ($secs) {
2242          $osecs  = $secs .' '. $ss;
2243      }
2244  
2245      if ($years) {
2246          return trim($oyears .' '. $odays);
2247      }
2248      if ($days) {
2249          return trim($odays .' '. $ohours);
2250      }
2251      if ($hours) {
2252          return trim($ohours .' '. $omins);
2253      }
2254      if ($mins) {
2255          return trim($omins .' '. $osecs);
2256      }
2257      if ($secs) {
2258          return $osecs;
2259      }
2260      return get_string('now');
2261  }
2262  
2263  /**
2264   * Returns a formatted string that represents a date in user time.
2265   *
2266   * @package core
2267   * @category time
2268   * @param int $date the timestamp in UTC, as obtained from the database.
2269   * @param string $format strftime format. You should probably get this using
2270   *        get_string('strftime...', 'langconfig');
2271   * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
2272   *        not 99 then daylight saving will not be added.
2273   *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
2274   * @param bool $fixday If true (default) then the leading zero from %d is removed.
2275   *        If false then the leading zero is maintained.
2276   * @param bool $fixhour If true (default) then the leading zero from %I is removed.
2277   * @return string the formatted date/time.
2278   */
2279  function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
2280      $calendartype = \core_calendar\type_factory::get_calendar_instance();
2281      return $calendartype->timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour);
2282  }
2283  
2284  /**
2285   * Returns a html "time" tag with both the exact user date with timezone information
2286   * as a datetime attribute in the W3C format, and the user readable date and time as text.
2287   *
2288   * @package core
2289   * @category time
2290   * @param int $date the timestamp in UTC, as obtained from the database.
2291   * @param string $format strftime format. You should probably get this using
2292   *        get_string('strftime...', 'langconfig');
2293   * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
2294   *        not 99 then daylight saving will not be added.
2295   *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
2296   * @param bool $fixday If true (default) then the leading zero from %d is removed.
2297   *        If false then the leading zero is maintained.
2298   * @param bool $fixhour If true (default) then the leading zero from %I is removed.
2299   * @return string the formatted date/time.
2300   */
2301  function userdate_htmltime($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
2302      $userdatestr = userdate($date, $format, $timezone, $fixday, $fixhour);
2303      if (CLI_SCRIPT && !PHPUNIT_TEST) {
2304          return $userdatestr;
2305      }
2306      $machinedate = new DateTime();
2307      $machinedate->setTimestamp(intval($date));
2308      $machinedate->setTimezone(core_date::get_user_timezone_object());
2309  
2310      return html_writer::tag('time', $userdatestr, ['datetime' => $machinedate->format(DateTime::W3C)]);
2311  }
2312  
2313  /**
2314   * Returns a formatted date ensuring it is UTF-8.
2315   *
2316   * If we are running under Windows convert to Windows encoding and then back to UTF-8
2317   * (because it's impossible to specify UTF-8 to fetch locale info in Win32).
2318   *
2319   * @param int $date the timestamp - since Moodle 2.9 this is a real UTC timestamp
2320   * @param string $format strftime format.
2321   * @param int|float|string $tz the user timezone
2322   * @return string the formatted date/time.
2323   * @since Moodle 2.3.3
2324   */
2325  function date_format_string($date, $format, $tz = 99) {
2326      global $CFG;
2327  
2328      $localewincharset = null;
2329      // Get the calendar type user is using.
2330      if ($CFG->ostype == 'WINDOWS') {
2331          $calendartype = \core_calendar\type_factory::get_calendar_instance();
2332          $localewincharset = $calendartype->locale_win_charset();
2333      }
2334  
2335      if ($localewincharset) {
2336          $format = core_text::convert($format, 'utf-8', $localewincharset);
2337      }
2338  
2339      date_default_timezone_set(core_date::get_user_timezone($tz));
2340      $datestring = strftime($format, $date);
2341      core_date::set_default_server_timezone();
2342  
2343      if ($localewincharset) {
2344          $datestring = core_text::convert($datestring, $localewincharset, 'utf-8');
2345      }
2346  
2347      return $datestring;
2348  }
2349  
2350  /**
2351   * Given a $time timestamp in GMT (seconds since epoch),
2352   * returns an array that represents the Gregorian date in user time
2353   *
2354   * @package core
2355   * @category time
2356   * @param int $time Timestamp in GMT
2357   * @param float|int|string $timezone user timezone
2358   * @return array An array that represents the date in user time
2359   */
2360  function usergetdate($time, $timezone=99) {
2361      date_default_timezone_set(core_date::get_user_timezone($timezone));
2362      $result = getdate($time);
2363      core_date::set_default_server_timezone();
2364  
2365      return $result;
2366  }
2367  
2368  /**
2369   * Given a GMT timestamp (seconds since epoch), offsets it by
2370   * the timezone.  eg 3pm in India is 3pm GMT - 7 * 3600 seconds
2371   *
2372   * NOTE: this function does not include DST properly,
2373   *       you should use the PHP date stuff instead!
2374   *
2375   * @package core
2376   * @category time
2377   * @param int $date Timestamp in GMT
2378   * @param float|int|string $timezone user timezone
2379   * @return int
2380   */
2381  function usertime($date, $timezone=99) {
2382      $userdate = new DateTime('@' . $date);
2383      $userdate->setTimezone(core_date::get_user_timezone_object($timezone));
2384      $dst = dst_offset_on($date, $timezone);
2385  
2386      return $date - $userdate->getOffset() + $dst;
2387  }
2388  
2389  /**
2390   * Get a formatted string representation of an interval between two unix timestamps.
2391   *
2392   * E.g.
2393   * $intervalstring = get_time_interval_string(12345600, 12345660);
2394   * Will produce the string:
2395   * '0d 0h 1m'
2396   *
2397   * @param int $time1 unix timestamp
2398   * @param int $time2 unix timestamp
2399   * @param string $format string (can be lang string) containing format chars: https://www.php.net/manual/en/dateinterval.format.php.
2400   * @return string the formatted string describing the time difference, e.g. '10d 11h 45m'.
2401   */
2402  function get_time_interval_string(int $time1, int $time2, string $format = ''): string {
2403      $dtdate = new DateTime();
2404      $dtdate->setTimeStamp($time1);
2405      $dtdate2 = new DateTime();
2406      $dtdate2->setTimeStamp($time2);
2407      $interval = $dtdate2->diff($dtdate);
2408      $format = empty($format) ? get_string('dateintervaldayshoursmins', 'langconfig') : $format;
2409      return $interval->format($format);
2410  }
2411  
2412  /**
2413   * Given a time, return the GMT timestamp of the most recent midnight
2414   * for the current user.
2415   *
2416   * @package core
2417   * @category time
2418   * @param int $date Timestamp in GMT
2419   * @param float|int|string $timezone user timezone
2420   * @return int Returns a GMT timestamp
2421   */
2422  function usergetmidnight($date, $timezone=99) {
2423  
2424      $userdate = usergetdate($date, $timezone);
2425  
2426      // Time of midnight of this user's day, in GMT.
2427      return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone);
2428  
2429  }
2430  
2431  /**
2432   * Returns a string that prints the user's timezone
2433   *
2434   * @package core
2435   * @category time
2436   * @param float|int|string $timezone user timezone
2437   * @return string
2438   */
2439  function usertimezone($timezone=99) {
2440      $tz = core_date::get_user_timezone($timezone);
2441      return core_date::get_localised_timezone($tz);
2442  }
2443  
2444  /**
2445   * Returns a float or a string which denotes the user's timezone
2446   * A float value means that a simple offset from GMT is used, while a string (it will be the name of a timezone in the database)
2447   * means that for this timezone there are also DST rules to be taken into account
2448   * Checks various settings and picks the most dominant of those which have a value
2449   *
2450   * @package core
2451   * @category time
2452   * @param float|int|string $tz timezone to calculate GMT time offset before
2453   *        calculating user timezone, 99 is default user timezone
2454   *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
2455   * @return float|string
2456   */
2457  function get_user_timezone($tz = 99) {
2458      global $USER, $CFG;
2459  
2460      $timezones = array(
2461          $tz,
2462          isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99,
2463          isset($USER->timezone) ? $USER->timezone : 99,
2464          isset($CFG->timezone) ? $CFG->timezone : 99,
2465          );
2466  
2467      $tz = 99;
2468  
2469      // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array.
2470      foreach ($timezones as $nextvalue) {
2471          if ((empty($tz) && !is_numeric($tz)) || $tz == 99) {
2472              $tz = $nextvalue;
2473          }
2474      }
2475      return is_numeric($tz) ? (float) $tz : $tz;
2476  }
2477  
2478  /**
2479   * Calculates the Daylight Saving Offset for a given date/time (timestamp)
2480   * - Note: Daylight saving only works for string timezones and not for float.
2481   *
2482   * @package core
2483   * @category time
2484   * @param int $time must NOT be compensated at all, it has to be a pure timestamp
2485   * @param int|float|string $strtimezone user timezone
2486   * @return int
2487   */
2488  function dst_offset_on($time, $strtimezone = null) {
2489      $tz = core_date::get_user_timezone($strtimezone);
2490      $date = new DateTime('@' . $time);
2491      $date->setTimezone(new DateTimeZone($tz));
2492      if ($date->format('I') == '1') {
2493          if ($tz === 'Australia/Lord_Howe') {
2494              return 1800;
2495          }
2496          return 3600;
2497      }
2498      return 0;
2499  }
2500  
2501  /**
2502   * Calculates when the day appears in specific month
2503   *
2504   * @package core
2505   * @category time
2506   * @param int $startday starting day of the month
2507   * @param int $weekday The day when week starts (normally taken from user preferences)
2508   * @param int $month The month whose day is sought
2509   * @param int $year The year of the month whose day is sought
2510   * @return int
2511   */
2512  function find_day_in_month($startday, $weekday, $month, $year) {
2513      $calendartype = \core_calendar\type_factory::get_calendar_instance();
2514  
2515      $daysinmonth = days_in_month($month, $year);
2516      $daysinweek = count($calendartype->get_weekdays());
2517  
2518      if ($weekday == -1) {
2519          // Don't care about weekday, so return:
2520          //    abs($startday) if $startday != -1
2521          //    $daysinmonth otherwise.
2522          return ($startday == -1) ? $daysinmonth : abs($startday);
2523      }
2524  
2525      // From now on we 're looking for a specific weekday.
2526      // Give "end of month" its actual value, since we know it.
2527      if ($startday == -1) {
2528          $startday = -1 * $daysinmonth;
2529      }
2530  
2531      // Starting from day $startday, the sign is the direction.
2532      if ($startday < 1) {
2533          $startday = abs($startday);
2534          $lastmonthweekday = dayofweek($daysinmonth, $month, $year);
2535  
2536          // This is the last such weekday of the month.
2537          $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday;
2538          if ($lastinmonth > $daysinmonth) {
2539              $lastinmonth -= $daysinweek;
2540          }
2541  
2542          // Find the first such weekday <= $startday.
2543          while ($lastinmonth > $startday) {
2544              $lastinmonth -= $daysinweek;
2545          }
2546  
2547          return $lastinmonth;
2548      } else {
2549          $indexweekday = dayofweek($startday, $month, $year);
2550  
2551          $diff = $weekday - $indexweekday;
2552          if ($diff < 0) {
2553              $diff += $daysinweek;
2554          }
2555  
2556          // This is the first such weekday of the month equal to or after $startday.
2557          $firstfromindex = $startday + $diff;
2558  
2559          return $firstfromindex;
2560      }
2561  }
2562  
2563  /**
2564   * Calculate the number of days in a given month
2565   *
2566   * @package core
2567   * @category time
2568   * @param int $month The month whose day count is sought
2569   * @param int $year The year of the month whose day count is sought
2570   * @return int
2571   */
2572  function days_in_month($month, $year) {
2573      $calendartype = \core_calendar\type_factory::get_calendar_instance();
2574      return $calendartype->get_num_days_in_month($year, $month);
2575  }
2576  
2577  /**
2578   * Calculate the position in the week of a specific calendar day
2579   *
2580   * @package core
2581   * @category time
2582   * @param int $day The day of the date whose position in the week is sought
2583   * @param int $month The month of the date whose position in the week is sought
2584   * @param int $year The year of the date whose position in the week is sought
2585   * @return int
2586   */
2587  function dayofweek($day, $month, $year) {
2588      $calendartype = \core_calendar\type_factory::get_calendar_instance();
2589      return $calendartype->get_weekday($year, $month, $day);
2590  }
2591  
2592  // USER AUTHENTICATION AND LOGIN.
2593  
2594  /**
2595   * Returns full login url.
2596   *
2597   * Any form submissions for authentication to this URL must include username,
2598   * password as well as a logintoken generated by \core\session\manager::get_login_token().
2599   *
2600   * @return string login url
2601   */
2602  function get_login_url() {
2603      global $CFG;
2604  
2605      return "$CFG->wwwroot/login/index.php";
2606  }
2607  
2608  /**
2609   * This function checks that the current user is logged in and has the
2610   * required privileges
2611   *
2612   * This function checks that the current user is logged in, and optionally
2613   * whether they are allowed to be in a particular course and view a particular
2614   * course module.
2615   * If they are not logged in, then it redirects them to the site login unless
2616   * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which
2617   * case they are automatically logged in as guests.
2618   * If $courseid is given and the user is not enrolled in that course then the
2619   * user is redirected to the course enrolment page.
2620   * If $cm is given and the course module is hidden and the user is not a teacher
2621   * in the course then the user is redirected to the course home page.
2622   *
2623   * When $cm parameter specified, this function sets page layout to 'module'.
2624   * You need to change it manually later if some other layout needed.
2625   *
2626   * @package    core_access
2627   * @category   access
2628   *
2629   * @param mixed $courseorid id of the course or course object
2630   * @param bool $autologinguest default true
2631   * @param object $cm course module object
2632   * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2633   *             true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2634   *             in order to keep redirects working properly. MDL-14495
2635   * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2636   * @return mixed Void, exit, and die depending on path
2637   * @throws coding_exception
2638   * @throws require_login_exception
2639   * @throws moodle_exception
2640   */
2641  function require_login($courseorid = null, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
2642      global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT;
2643  
2644      // Must not redirect when byteserving already started.
2645      if (!empty($_SERVER['HTTP_RANGE'])) {
2646          $preventredirect = true;
2647      }
2648  
2649      if (AJAX_SCRIPT) {
2650          // We cannot redirect for AJAX scripts either.
2651          $preventredirect = true;
2652      }
2653  
2654      // Setup global $COURSE, themes, language and locale.
2655      if (!empty($courseorid)) {
2656          if (is_object($courseorid)) {
2657              $course = $courseorid;
2658          } else if ($courseorid == SITEID) {
2659              $course = clone($SITE);
2660          } else {
2661              $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST);
2662          }
2663          if ($cm) {
2664              if ($cm->course != $course->id) {
2665                  throw new coding_exception('course and cm parameters in require_login() call do not match!!');
2666              }
2667              // Make sure we have a $cm from get_fast_modinfo as this contains activity access details.
2668              if (!($cm instanceof cm_info)) {
2669                  // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
2670                  // db queries so this is not really a performance concern, however it is obviously
2671                  // better if you use get_fast_modinfo to get the cm before calling this.
2672                  $modinfo = get_fast_modinfo($course);
2673                  $cm = $modinfo->get_cm($cm->id);
2674              }
2675          }
2676      } else {
2677          // Do not touch global $COURSE via $PAGE->set_course(),
2678          // the reasons is we need to be able to call require_login() at any time!!
2679          $course = $SITE;
2680          if ($cm) {
2681              throw new coding_exception('cm parameter in require_login() requires valid course parameter!');
2682          }
2683      }
2684  
2685      // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false.
2686      // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future
2687      // risk leading the user back to the AJAX request URL.
2688      if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) {
2689          $setwantsurltome = false;
2690      }
2691  
2692      // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour.
2693      if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !empty($CFG->dbsessions)) {
2694          if ($preventredirect) {
2695              throw new require_login_session_timeout_exception();
2696          } else {
2697              if ($setwantsurltome) {
2698                  $SESSION->wantsurl = qualified_me();
2699              }
2700              redirect(get_login_url());
2701          }
2702      }
2703  
2704      // If the user is not even logged in yet then make sure they are.
2705      if (!isloggedin()) {
2706          if ($autologinguest and !empty($CFG->guestloginbutton) and !empty($CFG->autologinguests)) {
2707              if (!$guest = get_complete_user_data('id', $CFG->siteguest)) {
2708                  // Misconfigured site guest, just redirect to login page.
2709                  redirect(get_login_url());
2710                  exit; // Never reached.
2711              }
2712              $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang;
2713              complete_user_login($guest);
2714              $USER->autologinguest = true;
2715              $SESSION->lang = $lang;
2716          } else {
2717              // NOTE: $USER->site check was obsoleted by session test cookie, $USER->confirmed test is in login/index.php.
2718              if ($preventredirect) {
2719                  throw new require_login_exception('You are not logged in');
2720              }
2721  
2722              if ($setwantsurltome) {
2723                  $SESSION->wantsurl = qualified_me();
2724              }
2725  
2726              $referer = get_local_referer(false);
2727              if (!empty($referer)) {
2728                  $SESSION->fromurl = $referer;
2729              }
2730  
2731              // Give auth plugins an opportunity to authenticate or redirect to an external login page
2732              $authsequence = get_enabled_auth_plugins(); // Auths, in sequence.
2733              foreach($authsequence as $authname) {
2734                  $authplugin = get_auth_plugin($authname);
2735                  $authplugin->pre_loginpage_hook();
2736                  if (isloggedin()) {
2737                      if ($cm) {
2738                          $modinfo = get_fast_modinfo($course);
2739                          $cm = $modinfo->get_cm($cm->id);
2740                      }
2741                      set_access_log_user();
2742                      break;
2743                  }
2744              }
2745  
2746              // If we're still not logged in then go to the login page
2747              if (!isloggedin()) {
2748                  redirect(get_login_url());
2749                  exit; // Never reached.
2750              }
2751          }
2752      }
2753  
2754      // Loginas as redirection if needed.
2755      if ($course->id != SITEID and \core\session\manager::is_loggedinas()) {
2756          if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) {
2757              if ($USER->loginascontext->instanceid != $course->id) {
2758                  print_error('loginasonecourse', '', $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid);
2759              }
2760          }
2761      }
2762  
2763      // Check whether the user should be changing password (but only if it is REALLY them).
2764      if (get_user_preferences('auth_forcepasswordchange') && !\core\session\manager::is_loggedinas()) {
2765          $userauth = get_auth_plugin($USER->auth);
2766          if ($userauth->can_change_password() and !$preventredirect) {
2767              if ($setwantsurltome) {
2768                  $SESSION->wantsurl = qualified_me();
2769              }
2770              if ($changeurl = $userauth->change_password_url()) {
2771                  // Use plugin custom url.
2772                  redirect($changeurl);
2773              } else {
2774                  // Use moodle internal method.
2775                  redirect($CFG->wwwroot .'/login/change_password.php');
2776              }
2777          } else if ($userauth->can_change_password()) {
2778              throw new moodle_exception('forcepasswordchangenotice');
2779          } else {
2780              throw new moodle_exception('nopasswordchangeforced', 'auth');
2781          }
2782      }
2783  
2784      // Check that the user account is properly set up. If we can't redirect to
2785      // edit their profile and this is not a WS request, perform just the lax check.
2786      // It will allow them to use filepicker on the profile edit page.
2787  
2788      if ($preventredirect && !WS_SERVER) {
2789          $usernotfullysetup = user_not_fully_set_up($USER, false);
2790      } else {
2791          $usernotfullysetup = user_not_fully_set_up($USER, true);
2792      }
2793  
2794      if ($usernotfullysetup) {
2795          if ($preventredirect) {
2796              throw new moodle_exception('usernotfullysetup');
2797          }
2798          if ($setwantsurltome) {
2799              $SESSION->wantsurl = qualified_me();
2800          }
2801          redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&amp;course='. SITEID);
2802      }
2803  
2804      // Make sure the USER has a sesskey set up. Used for CSRF protection.
2805      sesskey();
2806  
2807      if (\core\session\manager::is_loggedinas()) {
2808          // During a "logged in as" session we should force all content to be cleaned because the
2809          // logged in user will be viewing potentially malicious user generated content.
2810          // See MDL-63786 for more details.
2811          $CFG->forceclean = true;
2812      }
2813  
2814      $afterlogins = get_plugins_with_function('after_require_login', 'lib.php');
2815  
2816      // Do not bother admins with any formalities, except for activities pending deletion.
2817      if (is_siteadmin() && !($cm && $cm->deletioninprogress)) {
2818          // Set the global $COURSE.
2819          if ($cm) {
2820              $PAGE->set_cm($cm, $course);
2821              $PAGE->set_pagelayout('incourse');
2822          } else if (!empty($courseorid)) {
2823              $PAGE->set_course($course);
2824          }
2825          // Set accesstime or the user will appear offline which messes up messaging.
2826          // Do not update access time for webservice or ajax requests.
2827          if (!WS_SERVER && !AJAX_SCRIPT) {
2828              user_accesstime_log($course->id);
2829          }
2830  
2831          foreach ($afterlogins as $plugintype => $plugins) {
2832              foreach ($plugins as $pluginfunction) {
2833                  $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2834              }
2835          }
2836          return;
2837      }
2838  
2839      // Scripts have a chance to declare that $USER->policyagreed should not be checked.
2840      // This is mostly for places where users are actually accepting the policies, to avoid the redirect loop.
2841      if (!defined('NO_SITEPOLICY_CHECK')) {
2842          define('NO_SITEPOLICY_CHECK', false);
2843      }
2844  
2845      // Check that the user has agreed to a site policy if there is one - do not test in case of admins.
2846      // Do not test if the script explicitly asked for skipping the site policies check.
2847      if (!$USER->policyagreed && !is_siteadmin() && !NO_SITEPOLICY_CHECK) {
2848          $manager = new \core_privacy\local\sitepolicy\manager();
2849          if ($policyurl = $manager->get_redirect_url(isguestuser())) {
2850              if ($preventredirect) {
2851                  throw new moodle_exception('sitepolicynotagreed', 'error', '', $policyurl->out());
2852              }
2853              if ($setwantsurltome) {
2854                  $SESSION->wantsurl = qualified_me();
2855              }
2856              redirect($policyurl);
2857          }
2858      }
2859  
2860      // Fetch the system context, the course context, and prefetch its child contexts.
2861      $sysctx = context_system::instance();
2862      $coursecontext = context_course::instance($course->id, MUST_EXIST);
2863      if ($cm) {
2864          $cmcontext = context_module::instance($cm->id, MUST_EXIST);
2865      } else {
2866          $cmcontext = null;
2867      }
2868  
2869      // If the site is currently under maintenance, then print a message.
2870      if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:maintenanceaccess', $sysctx)) {
2871          if ($preventredirect) {
2872              throw new require_login_exception('Maintenance in progress');
2873          }
2874          $PAGE->set_context(null);
2875          print_maintenance_message();
2876      }
2877  
2878      // Make sure the course itself is not hidden.
2879      if ($course->id == SITEID) {
2880          // Frontpage can not be hidden.
2881      } else {
2882          if (is_role_switched($course->id)) {
2883              // When switching roles ignore the hidden flag - user had to be in course to do the switch.
2884          } else {
2885              if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
2886                  // Originally there was also test of parent category visibility, BUT is was very slow in complex queries
2887                  // involving "my courses" now it is also possible to simply hide all courses user is not enrolled in :-).
2888                  if ($preventredirect) {
2889                      throw new require_login_exception('Course is hidden');
2890                  }
2891                  $PAGE->set_context(null);
2892                  // We need to override the navigation URL as the course won't have been added to the navigation and thus
2893                  // the navigation will mess up when trying to find it.
2894                  navigation_node::override_active_url(new moodle_url('/'));
2895                  notice(get_string('coursehidden'), $CFG->wwwroot .'/');
2896              }
2897          }
2898      }
2899  
2900      // Is the user enrolled?
2901      if ($course->id == SITEID) {
2902          // Everybody is enrolled on the frontpage.
2903      } else {
2904          if (\core\session\manager::is_loggedinas()) {
2905              // Make sure the REAL person can access this course first.
2906              $realuser = \core\session\manager::get_realuser();
2907              if (!is_enrolled($coursecontext, $realuser->id, '', true) and
2908                  !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) {
2909                  if ($preventredirect) {
2910                      throw new require_login_exception('Invalid course login-as access');
2911                  }
2912                  $PAGE->set_context(null);
2913                  echo $OUTPUT->header();
2914                  notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/');
2915              }
2916          }
2917  
2918          $access = false;
2919  
2920          if (is_role_switched($course->id)) {
2921              // Ok, user had to be inside this course before the switch.
2922              $access = true;
2923  
2924          } else if (is_viewing($coursecontext, $USER)) {
2925              // Ok, no need to mess with enrol.
2926              $access = true;
2927  
2928          } else {
2929              if (isset($USER->enrol['enrolled'][$course->id])) {
2930                  if ($USER->enrol['enrolled'][$course->id] > time()) {
2931                      $access = true;
2932                      if (isset($USER->enrol['tempguest'][$course->id])) {
2933                          unset($USER->enrol['tempguest'][$course->id]);
2934                          remove_temp_course_roles($coursecontext);
2935                      }
2936                  } else {
2937                      // Expired.
2938                      unset($USER->enrol['enrolled'][$course->id]);
2939                  }
2940              }
2941              if (isset($USER->enrol['tempguest'][$course->id])) {
2942                  if ($USER->enrol['tempguest'][$course->id] == 0) {
2943                      $access = true;
2944                  } else if ($USER->enrol['tempguest'][$course->id] > time()) {
2945                      $access = true;
2946                  } else {
2947                      // Expired.
2948                      unset($USER->enrol['tempguest'][$course->id]);
2949                      remove_temp_course_roles($coursecontext);
2950                  }
2951              }
2952  
2953              if (!$access) {
2954                  // Cache not ok.
2955                  $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id);
2956                  if ($until !== false) {
2957                      // Active participants may always access, a timestamp in the future, 0 (always) or false.
2958                      if ($until == 0) {
2959                          $until = ENROL_MAX_TIMESTAMP;
2960                      }
2961                      $USER->enrol['enrolled'][$course->id] = $until;
2962                      $access = true;
2963  
2964                  } else if (core_course_category::can_view_course_info($course)) {
2965                      $params = array('courseid' => $course->id, 'status' => ENROL_INSTANCE_ENABLED);
2966                      $instances = $DB->get_records('enrol', $params, 'sortorder, id ASC');
2967                      $enrols = enrol_get_plugins(true);
2968                      // First ask all enabled enrol instances in course if they want to auto enrol user.
2969                      foreach ($instances as $instance) {
2970                          if (!isset($enrols[$instance->enrol])) {
2971                              continue;
2972                          }
2973                          // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false.
2974                          $until = $enrols[$instance->enrol]->try_autoenrol($instance);
2975                          if ($until !== false) {
2976                              if ($until == 0) {
2977                                  $until = ENROL_MAX_TIMESTAMP;
2978                              }
2979                              $USER->enrol['enrolled'][$course->id] = $until;
2980                              $access = true;
2981                              break;
2982                          }
2983                      }
2984                      // If not enrolled yet try to gain temporary guest access.
2985                      if (!$access) {
2986                          foreach ($instances as $instance) {
2987                              if (!isset($enrols[$instance->enrol])) {
2988                                  continue;
2989                              }
2990                              // Get a duration for the guest access, a timestamp in the future or false.
2991                              $until = $enrols[$instance->enrol]->try_guestaccess($instance);
2992                              if ($until !== false and $until > time()) {
2993                                  $USER->enrol['tempguest'][$course->id] = $until;
2994                                  $access = true;
2995                                  break;
2996                              }
2997                          }
2998                      }
2999                  } else {
3000                      // User is not enrolled and is not allowed to browse courses here.
3001                      if ($preventredirect) {
3002                          throw new require_login_exception('Course is not available');
3003                      }
3004                      $PAGE->set_context(null);
3005                      // We need to override the navigation URL as the course won't have been added to the navigation and thus
3006                      // the navigation will mess up when trying to find it.
3007                      navigation_node::override_active_url(new moodle_url('/'));
3008                      notice(get_string('coursehidden'), $CFG->wwwroot .'/');
3009                  }
3010              }
3011          }
3012  
3013          if (!$access) {
3014              if ($preventredirect) {
3015                  throw new require_login_exception('Not enrolled');
3016              }
3017              if ($setwantsurltome) {
3018                  $SESSION->wantsurl = qualified_me();
3019              }
3020              redirect($CFG->wwwroot .'/enrol/index.php?id='. $course->id);
3021          }
3022      }
3023  
3024      // Check whether the activity has been scheduled for deletion. If so, then deny access, even for admins.
3025      if ($cm && $cm->deletioninprogress) {
3026          if ($preventredirect) {
3027              throw new moodle_exception('activityisscheduledfordeletion');
3028          }
3029          require_once($CFG->dirroot . '/course/lib.php');
3030          redirect(course_get_url($course), get_string('activityisscheduledfordeletion', 'error'));
3031      }
3032  
3033      // Check visibility of activity to current user; includes visible flag, conditional availability, etc.
3034      if ($cm && !$cm->uservisible) {
3035          if ($preventredirect) {
3036              throw new require_login_exception('Activity is hidden');
3037          }
3038          // Get the error message that activity is not available and why (if explanation can be shown to the user).
3039          $PAGE->set_course($course);
3040          $renderer = $PAGE->get_renderer('course');
3041          $message = $renderer->course_section_cm_unavailable_error_message($cm);
3042          redirect(course_get_url($course), $message, null, \core\output\notification::NOTIFY_ERROR);
3043      }
3044  
3045      // Set the global $COURSE.
3046      if ($cm) {
3047          $PAGE->set_cm($cm, $course);
3048          $PAGE->set_pagelayout('incourse');
3049      } else if (!empty($courseorid)) {
3050          $PAGE->set_course($course);
3051      }
3052  
3053      foreach ($afterlogins as $plugintype => $plugins) {
3054          foreach ($plugins as $pluginfunction) {
3055              $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3056          }
3057      }
3058  
3059      // Finally access granted, update lastaccess times.
3060      // Do not update access time for webservice or ajax requests.
3061      if (!WS_SERVER && !AJAX_SCRIPT) {
3062          user_accesstime_log($course->id);
3063      }
3064  }
3065  
3066  /**
3067   * A convenience function for where we must be logged in as admin
3068   * @return void
3069   */
3070  function require_admin() {
3071      require_login(null, false);
3072      require_capability('moodle/site:config', context_system::instance());
3073  }
3074  
3075  /**
3076   * This function just makes sure a user is logged out.
3077   *
3078   * @package    core_access
3079   * @category   access
3080   */
3081  function require_logout() {
3082      global $USER, $DB;
3083  
3084      if (!isloggedin()) {
3085          // This should not happen often, no need for hooks or events here.
3086          \core\session\manager::terminate_current();
3087          return;
3088      }
3089  
3090      // Execute hooks before action.
3091      $authplugins = array();
3092      $authsequence = get_enabled_auth_plugins();
3093      foreach ($authsequence as $authname) {
3094          $authplugins[$authname] = get_auth_plugin($authname);
3095          $authplugins[$authname]->prelogout_hook();
3096      }
3097  
3098      // Store info that gets removed during logout.
3099      $sid = session_id();
3100      $event = \core\event\user_loggedout::create(
3101          array(
3102              'userid' => $USER->id,
3103              'objectid' => $USER->id,
3104              'other' => array('sessionid' => $sid),
3105          )
3106      );
3107      if ($session = $DB->get_record('sessions', array('sid'=>$sid))) {
3108          $event->add_record_snapshot('sessions', $session);
3109      }
3110  
3111      // Clone of $USER object to be used by auth plugins.
3112      $user = fullclone($USER);
3113  
3114      // Delete session record and drop $_SESSION content.
3115      \core\session\manager::terminate_current();
3116  
3117      // Trigger event AFTER action.
3118      $event->trigger();
3119  
3120      // Hook to execute auth plugins redirection after event trigger.
3121      foreach ($authplugins as $authplugin) {
3122          $authplugin->postlogout_hook($user);
3123      }
3124  }
3125  
3126  /**
3127   * Weaker version of require_login()
3128   *
3129   * This is a weaker version of {@link require_login()} which only requires login
3130   * when called from within a course rather than the site page, unless
3131   * the forcelogin option is turned on.
3132   * @see require_login()
3133   *
3134   * @package    core_access
3135   * @category   access
3136   *
3137   * @param mixed $courseorid The course object or id in question
3138   * @param bool $autologinguest Allow autologin guests if that is wanted
3139   * @param object $cm Course activity module if known
3140   * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
3141   *             true. Used to avoid (=false) some scripts (file.php...) to set that variable,
3142   *             in order to keep redirects working properly. MDL-14495
3143   * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
3144   * @return void
3145   * @throws coding_exception
3146   */
3147  function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
3148      global $CFG, $PAGE, $SITE;
3149      $issite = ((is_object($courseorid) and $courseorid->id == SITEID)
3150            or (!is_object($courseorid) and $courseorid == SITEID));
3151      if ($issite && !empty($cm) && !($cm instanceof cm_info)) {
3152          // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
3153          // db queries so this is not really a performance concern, however it is obviously
3154          // better if you use get_fast_modinfo to get the cm before calling this.
3155          if (is_object($courseorid)) {
3156              $course = $courseorid;
3157          } else {
3158              $course = clone($SITE);
3159          }
3160          $modinfo = get_fast_modinfo($course);
3161          $cm = $modinfo->get_cm($cm->id);
3162      }
3163      if (!empty($CFG->forcelogin)) {
3164          // Login required for both SITE and courses.
3165          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3166  
3167      } else if ($issite && !empty($cm) and !$cm->uservisible) {
3168          // Always login for hidden activities.
3169          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3170  
3171      } else if (isloggedin() && !isguestuser()) {
3172          // User is already logged in. Make sure the login is complete (user is fully setup, policies agreed).
3173          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3174  
3175      } else if ($issite) {
3176          // Login for SITE not required.
3177          // We still need to instatiate PAGE vars properly so that things that rely on it like navigation function correctly.
3178          if (!empty($courseorid)) {
3179              if (is_object($courseorid)) {
3180                  $course = $courseorid;
3181              } else {
3182                  $course = clone $SITE;
3183              }
3184              if ($cm) {
3185                  if ($cm->course != $course->id) {
3186                      throw new coding_exception('course and cm parameters in require_course_login() call do not match!!');
3187                  }
3188                  $PAGE->set_cm($cm, $course);
3189                  $PAGE->set_pagelayout('incourse');
3190              } else {
3191                  $PAGE->set_course($course);
3192              }
3193          } else {
3194              // If $PAGE->course, and hence $PAGE->context, have not already been set up properly, set them up now.
3195              $PAGE->set_course($PAGE->course);
3196          }
3197          // Do not update access time for webservice or ajax requests.
3198          if (!WS_SERVER && !AJAX_SCRIPT) {
3199              user_accesstime_log(SITEID);
3200          }
3201          return;
3202  
3203      } else {
3204          // Course login always required.
3205          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3206      }
3207  }
3208  
3209  /**
3210   * Validates a user key, checking if the key exists, is not expired and the remote ip is correct.
3211   *
3212   * @param  string $keyvalue the key value
3213   * @param  string $script   unique script identifier
3214   * @param  int $instance    instance id
3215   * @return stdClass the key entry in the user_private_key table
3216   * @since Moodle 3.2
3217   * @throws moodle_exception
3218   */
3219  function validate_user_key($keyvalue, $script, $instance) {
3220      global $DB;
3221  
3222      if (!$key = $DB->get_record('user_private_key', array('script' => $script, 'value' => $keyvalue, 'instance' => $instance))) {
3223          print_error('invalidkey');
3224      }
3225  
3226      if (!empty($key->validuntil) and $key->validuntil < time()) {
3227          print_error('expiredkey');
3228      }
3229  
3230      if ($key->iprestriction) {
3231          $remoteaddr = getremoteaddr(null);
3232          if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) {
3233              print_error('ipmismatch');
3234          }
3235      }
3236      return $key;
3237  }
3238  
3239  /**
3240   * Require key login. Function terminates with error if key not found or incorrect.
3241   *
3242   * @uses NO_MOODLE_COOKIES
3243   * @uses PARAM_ALPHANUM
3244   * @param string $script unique script identifier
3245   * @param int $instance optional instance id
3246   * @param string $keyvalue The key. If not supplied, this will be fetched from the current session.
3247   * @return int Instance ID
3248   */
3249  function require_user_key_login($script, $instance = null, $keyvalue = null) {
3250      global $DB;
3251  
3252      if (!NO_MOODLE_COOKIES) {
3253          print_error('sessioncookiesdisable');
3254      }
3255  
3256      // Extra safety.
3257      \core\session\manager::write_close();
3258  
3259      if (null === $keyvalue) {
3260          $keyvalue = required_param('key', PARAM_ALPHANUM);
3261      }
3262  
3263      $key = validate_user_key($keyvalue, $script, $instance);
3264  
3265      if (!$user = $DB->get_record('user', array('id' => $key->userid))) {
3266          print_error('invaliduserid');
3267      }
3268  
3269      core_user::require_active_user($user, true, true);
3270  
3271      // Emulate normal session.
3272      enrol_check_plugins($user);
3273      \core\session\manager::set_user($user);
3274  
3275      // Note we are not using normal login.
3276      if (!defined('USER_KEY_LOGIN')) {
3277          define('USER_KEY_LOGIN', true);
3278      }
3279  
3280      // Return instance id - it might be empty.
3281      return $key->instance;
3282  }
3283  
3284  /**
3285   * Creates a new private user access key.
3286   *
3287   * @param string $script unique target identifier
3288   * @param int $userid
3289   * @param int $instance optional instance id
3290   * @param string $iprestriction optional ip restricted access
3291   * @param int $validuntil key valid only until given data
3292   * @return string access key value
3293   */
3294  function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3295      global $DB;
3296  
3297      $key = new stdClass();
3298      $key->script        = $script;
3299      $key->userid        = $userid;
3300      $key->instance      = $instance;
3301      $key->iprestriction = $iprestriction;
3302      $key->validuntil    = $validuntil;
3303      $key->timecreated   = time();
3304  
3305      // Something long and unique.
3306      $key->value         = md5($userid.'_'.time().random_string(40));
3307      while ($DB->record_exists('user_private_key', array('value' => $key->value))) {
3308          // Must be unique.
3309          $key->value     = md5($userid.'_'.time().random_string(40));
3310      }
3311      $DB->insert_record('user_private_key', $key);
3312      return $key->value;
3313  }
3314  
3315  /**
3316   * Delete the user's new private user access keys for a particular script.
3317   *
3318   * @param string $script unique target identifier
3319   * @param int $userid
3320   * @return void
3321   */
3322  function delete_user_key($script, $userid) {
3323      global $DB;
3324      $DB->delete_records('user_private_key', array('script' => $script, 'userid' => $userid));
3325  }
3326  
3327  /**
3328   * Gets a private user access key (and creates one if one doesn't exist).
3329   *
3330   * @param string $script unique target identifier
3331   * @param int $userid
3332   * @param int $instance optional instance id
3333   * @param string $iprestriction optional ip restricted access
3334   * @param int $validuntil key valid only until given date
3335   * @return string access key value
3336   */
3337  function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3338      global $DB;
3339  
3340      if ($key = $DB->get_record('user_private_key', array('script' => $script, 'userid' => $userid,
3341                                                           'instance' => $instance, 'iprestriction' => $iprestriction,
3342                                                           'validuntil' => $validuntil))) {
3343          return $key->value;
3344      } else {
3345          return create_user_key($script, $userid, $instance, $iprestriction, $validuntil);
3346      }
3347  }
3348  
3349  
3350  /**
3351   * Modify the user table by setting the currently logged in user's last login to now.
3352   *
3353   * @return bool Always returns true
3354   */
3355  function update_user_login_times() {
3356      global $USER, $DB;
3357  
3358      if (isguestuser()) {
3359          // Do not update guest access times/ips for performance.
3360          return true;
3361      }
3362  
3363      $now = time();
3364  
3365      $user = new stdClass();
3366      $user->id = $USER->id;
3367  
3368      // Make sure all users that logged in have some firstaccess.
3369      if ($USER->firstaccess == 0) {
3370          $USER->firstaccess = $user->firstaccess = $now;
3371      }
3372  
3373      // Store the previous current as lastlogin.
3374      $USER->lastlogin = $user->lastlogin = $USER->currentlogin;
3375  
3376      $USER->currentlogin = $user->currentlogin = $now;
3377  
3378      // Function user_accesstime_log() may not update immediately, better do it here.
3379      $USER->lastaccess = $user->lastaccess = $now;
3380      $USER->lastip = $user->lastip = getremoteaddr();
3381  
3382      // Note: do not call user_update_user() here because this is part of the login process,
3383      //       the login event means that these fields were updated.
3384      $DB->update_record('user', $user);
3385      return true;
3386  }
3387  
3388  /**
3389   * Determines if a user has completed setting up their account.
3390   *
3391   * The lax mode (with $strict = false) has been introduced for special cases
3392   * only where we want to skip certain checks intentionally. This is valid in
3393   * certain mnet or ajax scenarios when the user cannot / should not be
3394   * redirected to edit their profile. In most cases, you should perform the
3395   * strict check.
3396   *
3397   * @param stdClass $user A {@link $USER} object to test for the existence of a valid name and email
3398   * @param bool $strict Be more strict and assert id and custom profile fields set, too
3399   * @return bool
3400   */
3401  function user_not_fully_set_up($user, $strict = true) {
3402      global $CFG;
3403      require_once($CFG->dirroot.'/user/profile/lib.php');
3404  
3405      if (isguestuser($user)) {
3406          return false;
3407      }
3408  
3409      if (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user)) {
3410          return true;
3411      }
3412  
3413      if ($strict) {
3414          if (empty($user->id)) {
3415              // Strict mode can be used with existing accounts only.
3416              return true;
3417          }
3418          if (!profile_has_required_custom_fields_set($user->id)) {
3419              return true;
3420          }
3421      }
3422  
3423      return false;
3424  }
3425  
3426  /**
3427   * Check whether the user has exceeded the bounce threshold
3428   *
3429   * @param stdClass $user A {@link $USER} object
3430   * @return bool true => User has exceeded bounce threshold
3431   */
3432  function over_bounce_threshold($user) {
3433      global $CFG, $DB;
3434  
3435      if (empty($CFG->handlebounces)) {
3436          return false;
3437      }
3438  
3439      if (empty($user->id)) {
3440          // No real (DB) user, nothing to do here.
3441          return false;
3442      }
3443  
3444      // Set sensible defaults.
3445      if (empty($CFG->minbounces)) {
3446          $CFG->minbounces = 10;
3447      }
3448      if (empty($CFG->bounceratio)) {
3449          $CFG->bounceratio = .20;
3450      }
3451      $bouncecount = 0;
3452      $sendcount = 0;
3453      if ($bounce = $DB->get_record('user_preferences', array ('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3454          $bouncecount = $bounce->value;
3455      }
3456      if ($send = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3457          $sendcount = $send->value;
3458      }
3459      return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio);
3460  }
3461  
3462  /**
3463   * Used to increment or reset email sent count
3464   *
3465   * @param stdClass $user object containing an id
3466   * @param bool $reset will reset the count to 0
3467   * @return void
3468   */
3469  function set_send_count($user, $reset=false) {
3470      global $DB;
3471  
3472      if (empty($user->id)) {
3473          // No real (DB) user, nothing to do here.
3474          return;
3475      }
3476  
3477      if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3478          $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3479          $DB->update_record('user_preferences', $pref);
3480      } else if (!empty($reset)) {
3481          // If it's not there and we're resetting, don't bother. Make a new one.
3482          $pref = new stdClass();
3483          $pref->name   = 'email_send_count';
3484          $pref->value  = 1;
3485          $pref->userid = $user->id;
3486          $DB->insert_record('user_preferences', $pref, false);
3487      }
3488  }
3489  
3490  /**
3491   * Increment or reset user's email bounce count
3492   *
3493   * @param stdClass $user object containing an id
3494   * @param bool $reset will reset the count to 0
3495   */
3496  function set_bounce_count($user, $reset=false) {
3497      global $DB;
3498  
3499      if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3500          $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3501          $DB->update_record('user_preferences', $pref);
3502      } else if (!empty($reset)) {
3503          // If it's not there and we're resetting, don't bother. Make a new one.
3504          $pref = new stdClass();
3505          $pref->name   = 'email_bounce_count';
3506          $pref->value  = 1;
3507          $pref->userid = $user->id;
3508          $DB->insert_record('user_preferences', $pref, false);
3509      }
3510  }
3511  
3512  /**
3513   * Determines if the logged in user is currently moving an activity
3514   *
3515   * @param int $courseid The id of the course being tested
3516   * @return bool
3517   */
3518  function ismoving($courseid) {
3519      global $USER;
3520  
3521      if (!empty($USER->activitycopy)) {
3522          return ($USER->activitycopycourse == $courseid);
3523      }
3524      return false;
3525  }
3526  
3527  /**
3528   * Returns a persons full name
3529   *
3530   * Given an object containing all of the users name values, this function returns a string with the full name of the person.
3531   * The result may depend on system settings or language. 'override' will force the alternativefullnameformat to be used. In
3532   * English, fullname as well as alternativefullnameformat is set to 'firstname lastname' by default. But you could have
3533   * fullname set to 'firstname lastname' and alternativefullnameformat set to 'firstname middlename alternatename lastname'.
3534   *
3535   * @param stdClass $user A {@link $USER} object to get full name of.
3536   * @param bool $override If true then the alternativefullnameformat format rather than fullnamedisplay format will be used.
3537   * @return string
3538   */
3539  function fullname($user, $override=false) {
3540      global $CFG, $SESSION;
3541  
3542      if (!isset($user->firstname) and !isset($user->lastname)) {
3543          return '';
3544      }
3545  
3546      // Get all of the name fields.
3547      $allnames = get_all_user_name_fields();
3548      if ($CFG->debugdeveloper) {
3549          foreach ($allnames as $allname) {
3550              if (!property_exists($user, $allname)) {
3551                  // If all the user name fields are not set in the user object, then notify the programmer that it needs to be fixed.
3552                  debugging('You need to update your sql to include additional name fields in the user object.', DEBUG_DEVELOPER);
3553                  // Message has been sent, no point in sending the message multiple times.
3554                  break;
3555              }
3556          }
3557      }
3558  
3559      if (!$override) {
3560          if (!empty($CFG->forcefirstname)) {
3561              $user->firstname = $CFG->forcefirstname;
3562          }
3563          if (!empty($CFG->forcelastname)) {
3564              $user->lastname = $CFG->forcelastname;
3565          }
3566      }
3567  
3568      if (!empty($SESSION->fullnamedisplay)) {
3569          $CFG->fullnamedisplay = $SESSION->fullnamedisplay;
3570      }
3571  
3572      $template = null;
3573      // If the fullnamedisplay setting is available, set the template to that.
3574      if (isset($CFG->fullnamedisplay)) {
3575          $template = $CFG->fullnamedisplay;
3576      }
3577      // If the template is empty, or set to language, return the language string.
3578      if ((empty($template) || $template == 'language') && !$override) {
3579          return get_string('fullnamedisplay', null, $user);
3580      }
3581  
3582      // Check to see if we are displaying according to the alternative full name format.
3583      if ($override) {
3584          if (empty($CFG->alternativefullnameformat) || $CFG->alternativefullnameformat == 'language') {
3585              // Default to show just the user names according to the fullnamedisplay string.
3586              return get_string('fullnamedisplay', null, $user);
3587          } else {
3588              // If the override is true, then change the template to use the complete name.
3589              $template = $CFG->alternativefullnameformat;
3590          }
3591      }
3592  
3593      $requirednames = array();
3594      // With each name, see if it is in the display name template, and add it to the required names array if it is.
3595      foreach ($allnames as $allname) {
3596          if (strpos($template, $allname) !== false) {
3597              $requirednames[] = $allname;
3598          }
3599      }
3600  
3601      $displayname = $template;
3602      // Switch in the actual data into the template.
3603      foreach ($requirednames as $altname) {
3604          if (isset($user->$altname)) {
3605              // Using empty() on the below if statement causes breakages.
3606              if ((string)$user->$altname == '') {
3607                  $displayname = str_replace($altname, 'EMPTY', $displayname);
3608              } else {
3609                  $displayname = str_replace($altname, $user->$altname, $displayname);
3610              }
3611          } else {
3612              $displayname = str_replace($altname, 'EMPTY', $displayname);
3613          }
3614      }
3615      // Tidy up any misc. characters (Not perfect, but gets most characters).
3616      // Don't remove the "u" at the end of the first expression unless you want garbled characters when combining hiragana or
3617      // katakana and parenthesis.
3618      $patterns = array();
3619      // This regular expression replacement is to fix problems such as 'James () Kirk' Where 'Tiberius' (middlename) has not been
3620      // filled in by a user.
3621      // The special characters are Japanese brackets that are common enough to make allowances for them (not covered by :punct:).
3622      $patterns[] = '/[[:punct:]「」]*EMPTY[[:punct:]「」]*/u';
3623      // This regular expression is to remove any double spaces in the display name.
3624      $patterns[] = '/\s{2,}/u';
3625      foreach ($patterns as $pattern) {
3626          $displayname = preg_replace($pattern, ' ', $displayname);
3627      }
3628  
3629      // Trimming $displayname will help the next check to ensure that we don't have a display name with spaces.
3630      $displayname = trim($displayname);
3631      if (empty($displayname)) {
3632          // Going with just the first name if no alternate fields are filled out. May be changed later depending on what
3633          // people in general feel is a good setting to fall back on.
3634          $displayname = $user->firstname;
3635      }
3636      return $displayname;
3637  }
3638  
3639  /**
3640   * A centralised location for the all name fields. Returns an array / sql string snippet.
3641   *
3642   * @param bool $returnsql True for an sql select field snippet.
3643   * @param string $tableprefix table query prefix to use in front of each field.
3644   * @param string $prefix prefix added to the name fields e.g. authorfirstname.
3645   * @param string $fieldprefix sql field prefix e.g. id AS userid.
3646   * @param bool $order moves firstname and lastname to the top of the array / start of the string.
3647   * @return array|string All name fields.
3648   */
3649  function get_all_user_name_fields($returnsql = false, $tableprefix = null, $prefix = null, $fieldprefix = null, $order = false) {
3650      // This array is provided in this order because when called by fullname() (above) if firstname is before
3651      // firstnamephonetic str_replace() will change the wrong placeholder.
3652      $alternatenames = array('firstnamephonetic' => 'firstnamephonetic',
3653                              'lastnamephonetic' => 'lastnamephonetic',
3654                              'middlename' => 'middlename',
3655                              'alternatename' => 'alternatename',
3656                              'firstname' => 'firstname',
3657                              'lastname' => 'lastname');
3658  
3659      // Let's add a prefix to the array of user name fields if provided.
3660      if ($prefix) {
3661          foreach ($alternatenames as $key => $altname) {
3662              $alternatenames[$key] = $prefix . $altname;
3663          }
3664      }
3665  
3666      // If we want the end result to have firstname and lastname at the front / top of the result.
3667      if ($order) {
3668          // Move the last two elements (firstname, lastname) off the array and put them at the top.
3669          for ($i = 0; $i < 2; $i++) {
3670              // Get the last element.
3671              $lastelement = end($alternatenames);
3672              // Remove it from the array.
3673              unset($alternatenames[$lastelement]);
3674              // Put the element back on the top of the array.
3675              $alternatenames = array_merge(array($lastelement => $lastelement), $alternatenames);
3676          }
3677      }
3678  
3679      // Create an sql field snippet if requested.
3680      if ($returnsql) {
3681          if ($tableprefix) {
3682              if ($fieldprefix) {
3683                  foreach ($alternatenames as $key => $altname) {
3684                      $alternatenames[$key] = $tableprefix . '.' . $altname . ' AS ' . $fieldprefix . $altname;
3685                  }
3686              } else {
3687                  foreach ($alternatenames as $key => $altname) {
3688                      $alternatenames[$key] = $tableprefix . '.' . $altname;
3689                  }
3690              }
3691          }
3692          $alternatenames = implode(',', $alternatenames);
3693      }
3694      return $alternatenames;
3695  }
3696  
3697  /**
3698   * Reduces lines of duplicated code for getting user name fields.
3699   *
3700   * See also {@link user_picture::unalias()}
3701   *
3702   * @param object $addtoobject Object to add user name fields to.
3703   * @param object $secondobject Object that contains user name field information.
3704   * @param string $prefix prefix to be added to all fields (including $additionalfields) e.g. authorfirstname.
3705   * @param array $additionalfields Additional fields to be matched with data in the second object.
3706   * The key can be set to the user table field name.
3707   * @return object User name fields.
3708   */
3709  function username_load_fields_from_object($addtoobject, $secondobject, $prefix = null, $additionalfields = null) {
3710      $fields = get_all_user_name_fields(false, null, $prefix);
3711      if ($additionalfields) {
3712          // Additional fields can specify their own 'alias' such as 'id' => 'userid'. This checks to see if
3713          // the key is a number and then sets the key to the array value.
3714          foreach ($additionalfields as $key => $value) {
3715              if (is_numeric($key)) {
3716                  $additionalfields[$value] = $prefix . $value;
3717                  unset($additionalfields[$key]);
3718              } else {
3719                  $additionalfields[$key] = $prefix . $value;
3720              }
3721          }
3722          $fields = array_merge($fields, $additionalfields);
3723      }
3724      foreach ($fields as $key => $field) {
3725          // Important that we have all of the user name fields present in the object that we are sending back.
3726          $addtoobject->$key = '';
3727          if (isset($secondobject->$field)) {
3728              $addtoobject->$key = $secondobject->$field;
3729          }
3730      }
3731      return $addtoobject;
3732  }
3733  
3734  /**
3735   * Returns an array of values in order of occurance in a provided string.
3736   * The key in the result is the character postion in the string.
3737   *
3738   * @param array $values Values to be found in the string format
3739   * @param string $stringformat The string which may contain values being searched for.
3740   * @return array An array of values in order according to placement in the string format.
3741   */
3742  function order_in_string($values, $stringformat) {
3743      $valuearray = array();
3744      foreach ($values as $value) {
3745          $pattern = "/$value\b/";
3746          // Using preg_match as strpos() may match values that are similar e.g. firstname and firstnamephonetic.
3747          if (preg_match($pattern, $stringformat)) {
3748              $replacement = "thing";
3749              // Replace the value with something more unique to ensure we get the right position when using strpos().
3750              $newformat = preg_replace($pattern, $replacement, $stringformat);
3751              $position = strpos($newformat, $replacement);
3752              $valuearray[$position] = $value;
3753          }
3754      }
3755      ksort($valuearray);
3756      return $valuearray;
3757  }
3758  
3759  /**
3760   * Checks if current user is shown any extra fields when listing users.
3761   *
3762   * @param object $context Context
3763   * @param array $already Array of fields that we're going to show anyway
3764   *   so don't bother listing them
3765   * @return array Array of field names from user table, not including anything
3766   *   listed in $already
3767   */
3768  function get_extra_user_fields($context, $already = array()) {
3769      global $CFG;
3770  
3771      // Only users with permission get the extra fields.
3772      if (!has_capability('moodle/site:viewuseridentity', $context)) {
3773          return array();
3774      }
3775  
3776      // Split showuseridentity on comma (filter needed in case the showuseridentity is empty).
3777      $extra = array_filter(explode(',', $CFG->showuseridentity));
3778  
3779      foreach ($extra as $key => $field) {
3780          if (in_array($field, $already)) {
3781              unset($extra[$key]);
3782          }
3783      }
3784  
3785      // If the identity fields are also among hidden fields, make sure the user can see them.
3786      $hiddenfields = array_filter(explode(',', $CFG->hiddenuserfields));
3787      $hiddenidentifiers = array_intersect($extra, $hiddenfields);
3788  
3789      if ($hiddenidentifiers) {
3790          if ($context->get_course_context(false)) {
3791              // We are somewhere inside a course.
3792              $canviewhiddenuserfields = has_capability('moodle/course:viewhiddenuserfields', $context);
3793  
3794          } else {
3795              // We are not inside a course.
3796              $canviewhiddenuserfields = has_capability('moodle/user:viewhiddendetails', $context);
3797          }
3798  
3799          if (!$canviewhiddenuserfields) {
3800              // Remove hidden identifiers from the list.
3801              $extra = array_diff($extra, $hiddenidentifiers);
3802          }
3803      }
3804  
3805      // Re-index the entries.
3806      $extra = array_values($extra);
3807  
3808      return $extra;
3809  }
3810  
3811  /**
3812   * If the current user is to be shown extra user fields when listing or
3813   * selecting users, returns a string suitable for including in an SQL select
3814   * clause to retrieve those fields.
3815   *
3816   * @param context $context Context
3817   * @param string $alias Alias of user table, e.g. 'u' (default none)
3818   * @param string $prefix Prefix for field names using AS, e.g. 'u_' (default none)
3819   * @param array $already Array of fields that we're going to include anyway so don't list them (default none)
3820   * @return string Partial SQL select clause, beginning with comma, for example ',u.idnumber,u.department' unless it is blank
3821   */
3822  function get_extra_user_fields_sql($context, $alias='', $prefix='', $already = array()) {
3823      $fields = get_extra_user_fields($context, $already);
3824      $result = '';
3825      // Add punctuation for alias.
3826      if ($alias !== '') {
3827          $alias .= '.';
3828      }
3829      foreach ($fields as $field) {
3830          $result .= ', ' . $alias . $field;
3831          if ($prefix) {
3832              $result .= ' AS ' . $prefix . $field;
3833          }
3834      }
3835      return $result;
3836  }
3837  
3838  /**
3839   * Returns the display name of a field in the user table. Works for most fields that are commonly displayed to users.
3840   * @param string $field Field name, e.g. 'phone1'
3841   * @return string Text description taken from language file, e.g. 'Phone number'
3842   */
3843  function get_user_field_name($field) {
3844      // Some fields have language strings which are not the same as field name.
3845      switch ($field) {
3846          case 'url' : {
3847              return get_string('webpage');
3848          }
3849          case 'icq' : {
3850              return get_string('icqnumber');
3851          }
3852          case 'skype' : {
3853              return get_string('skypeid');
3854          }
3855          case 'aim' : {
3856              return get_string('aimid');
3857          }
3858          case 'yahoo' : {
3859              return get_string('yahooid');
3860          }
3861          case 'msn' : {
3862              return get_string('msnid');
3863          }
3864          case 'picture' : {
3865              return get_string('pictureofuser');
3866          }
3867      }
3868      // Otherwise just use the same lang string.
3869      return get_string($field);
3870  }
3871  
3872  /**
3873   * Returns whether a given authentication plugin exists.
3874   *
3875   * @param string $auth Form of authentication to check for. Defaults to the global setting in {@link $CFG}.
3876   * @return boolean Whether the plugin is available.
3877   */
3878  function exists_auth_plugin($auth) {
3879      global $CFG;
3880  
3881      if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) {
3882          return is_readable("{$CFG->dirroot}/auth/$auth/auth.php");
3883      }
3884      return false;
3885  }
3886  
3887  /**
3888   * Checks if a given plugin is in the list of enabled authentication plugins.
3889   *
3890   * @param string $auth Authentication plugin.
3891   * @return boolean Whether the plugin is enabled.
3892   */
3893  function is_enabled_auth($auth) {
3894      if (empty($auth)) {
3895          return false;
3896      }
3897  
3898      $enabled = get_enabled_auth_plugins();
3899  
3900      return in_array($auth, $enabled);
3901  }
3902  
3903  /**
3904   * Returns an authentication plugin instance.
3905   *
3906   * @param string $auth name of authentication plugin
3907   * @return auth_plugin_base An instance of the required authentication plugin.
3908   */
3909  function get_auth_plugin($auth) {
3910      global $CFG;
3911  
3912      // Check the plugin exists first.
3913      if (! exists_auth_plugin($auth)) {
3914          print_error('authpluginnotfound', 'debug', '', $auth);
3915      }
3916  
3917      // Return auth plugin instance.
3918      require_once("{$CFG->dirroot}/auth/$auth/auth.php");
3919      $class = "auth_plugin_$auth";
3920      return new $class;
3921  }
3922  
3923  /**
3924   * Returns array of active auth plugins.
3925   *
3926   * @param bool $fix fix $CFG->auth if needed. Only set if logged in as admin.
3927   * @return array
3928   */
3929  function get_enabled_auth_plugins($fix=false) {
3930      global $CFG;
3931  
3932      $default = array('manual', 'nologin');
3933  
3934      if (empty($CFG->auth)) {
3935          $auths = array();
3936      } else {
3937          $auths = explode(',', $CFG->auth);
3938      }
3939  
3940      $auths = array_unique($auths);
3941      $oldauthconfig = implode(',', $auths);
3942      foreach ($auths as $k => $authname) {
3943          if (in_array($authname, $default)) {
3944              // The manual and nologin plugin never need to be stored.
3945              unset($auths[$k]);
3946          } else if (!exists_auth_plugin($authname)) {
3947              debugging(get_string('authpluginnotfound', 'debug', $authname));
3948              unset($auths[$k]);
3949          }
3950      }
3951  
3952      // Ideally only explicit interaction from a human admin should trigger a
3953      // change in auth config, see MDL-70424 for details.
3954      if ($fix) {
3955          $newconfig = implode(',', $auths);
3956          if (!isset($CFG->auth) or $newconfig != $CFG->auth) {
3957              set_config('auth', $newconfig);
3958          }
3959      }
3960  
3961      return (array_merge($default, $auths));
3962  }
3963  
3964  /**
3965   * Returns true if an internal authentication method is being used.
3966   * if method not specified then, global default is assumed
3967   *
3968   * @param string $auth Form of authentication required
3969   * @return bool
3970   */
3971  function is_internal_auth($auth) {
3972      // Throws error if bad $auth.
3973      $authplugin = get_auth_plugin($auth);
3974      return $authplugin->is_internal();
3975  }
3976  
3977  /**
3978   * Returns true if the user is a 'restored' one.
3979   *
3980   * Used in the login process to inform the user and allow him/her to reset the password
3981   *
3982   * @param string $username username to be checked
3983   * @return bool
3984   */
3985  function is_restored_user($username) {
3986      global $CFG, $DB;
3987  
3988      return $DB->record_exists('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'password' => 'restored'));
3989  }
3990  
3991  /**
3992   * Returns an array of user fields
3993   *
3994   * @return array User field/column names
3995   */
3996  function get_user_fieldnames() {
3997      global $DB;
3998  
3999      $fieldarray = $DB->get_columns('user');
4000      unset($fieldarray['id']);
4001      $fieldarray = array_keys($fieldarray);
4002  
4003      return $fieldarray;
4004  }
4005  
4006  /**
4007   * Creates a bare-bones user record
4008   *
4009   * @todo Outline auth types and provide code example
4010   *
4011   * @param string $username New user's username to add to record
4012   * @param string $password New user's password to add to record
4013   * @param string $auth Form of authentication required
4014   * @return stdClass A complete user object
4015   */
4016  function create_user_record($username, $password, $auth = 'manual') {
4017      global $CFG, $DB;
4018      require_once($CFG->dirroot.'/user/profile/lib.php');
4019      require_once($CFG->dirroot.'/user/lib.php');
4020  
4021      // Just in case check text case.
4022      $username = trim(core_text::strtolower($username));
4023  
4024      $authplugin = get_auth_plugin($auth);
4025      $customfields = $authplugin->get_custom_user_profile_fields();
4026      $newuser = new stdClass();
4027      if ($newinfo = $authplugin->get_userinfo($username)) {
4028          $newinfo = truncate_userinfo($newinfo);
4029          foreach ($newinfo as $key => $value) {
4030              if (in_array($key, $authplugin->userfields) || (in_array($key, $customfields))) {
4031                  $newuser->$key = $value;
4032              }
4033          }
4034      }
4035  
4036      if (!empty($newuser->email)) {
4037          if (email_is_not_allowed($newuser->email)) {
4038              unset($newuser->email);
4039          }
4040      }
4041  
4042      if (!isset($newuser->city)) {
4043          $newuser->city = '';
4044      }
4045  
4046      $newuser->auth = $auth;
4047      $newuser->username = $username;
4048  
4049      // Fix for MDL-8480
4050      // user CFG lang for user if $newuser->lang is empty
4051      // or $user->lang is not an installed language.
4052      if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) {
4053          $newuser->lang = $CFG->lang;
4054      }
4055      $newuser->confirmed = 1;
4056      $newuser->lastip = getremoteaddr();
4057      $newuser->timecreated = time();
4058      $newuser->timemodified = $newuser->timecreated;
4059      $newuser->mnethostid = $CFG->mnet_localhost_id;
4060  
4061      $newuser->id = user_create_user($newuser, false, false);
4062  
4063      // Save user profile data.
4064      profile_save_data($newuser);
4065  
4066      $user = get_complete_user_data('id', $newuser->id);
4067      if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})) {
4068          set_user_preference('auth_forcepasswordchange', 1, $user);
4069      }
4070      // Set the password.
4071      update_internal_user_password($user, $password);
4072  
4073      // Trigger event.
4074      \core\event\user_created::create_from_userid($newuser->id)->trigger();
4075  
4076      return $user;
4077  }
4078  
4079  /**
4080   * Will update a local user record from an external source (MNET users can not be updated using this method!).
4081   *
4082   * @param string $username user's username to update the record
4083   * @return stdClass A complete user object
4084   */
4085  function update_user_record($username) {
4086      global $DB, $CFG;
4087      // Just in case check text case.
4088      $username = trim(core_text::strtolower($username));
4089  
4090      $oldinfo = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id), '*', MUST_EXIST);
4091      return update_user_record_by_id($oldinfo->id);
4092  }
4093  
4094  /**
4095   * Will update a local user record from an external source (MNET users can not be updated using this method!).
4096   *
4097   * @param int $id user id
4098   * @return stdClass A complete user object
4099   */
4100  function update_user_record_by_id($id) {
4101      global $DB, $CFG;
4102      require_once($CFG->dirroot."/user/profile/lib.php");
4103      require_once($CFG->dirroot.'/user/lib.php');
4104  
4105      $params = array('mnethostid' => $CFG->mnet_localhost_id, 'id' => $id, 'deleted' => 0);
4106      $oldinfo = $DB->get_record('user', $params, '*', MUST_EXIST);
4107  
4108      $newuser = array();
4109      $userauth = get_auth_plugin($oldinfo->auth);
4110  
4111      if ($newinfo = $userauth->get_userinfo($oldinfo->username)) {
4112          $newinfo = truncate_userinfo($newinfo);
4113          $customfields = $userauth->get_custom_user_profile_fields();
4114  
4115          foreach ($newinfo as $key => $value) {
4116              $iscustom = in_array($key, $customfields);
4117              if (!$iscustom) {
4118                  $key = strtolower($key);
4119              }
4120              if ((!property_exists($oldinfo, $key) && !$iscustom) or $key === 'username' or $key === 'id'
4121                      or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') {
4122                  // Unknown or must not be changed.
4123                  continue;
4124              }
4125              if (empty($userauth->config->{'field_updatelocal_' . $key}) || empty($userauth->config->{'field_lock_' . $key})) {
4126                  continue;
4127              }
4128              $confval = $userauth->config->{'field_updatelocal_' . $key};
4129              $lockval = $userauth->config->{'field_lock_' . $key};
4130              if ($confval === 'onlogin') {
4131                  // MDL-4207 Don't overwrite modified user profile values with
4132                  // empty LDAP values when 'unlocked if empty' is set. The purpose
4133                  // of the setting 'unlocked if empty' is to allow the user to fill
4134                  // in a value for the selected field _if LDAP is giving
4135                  // nothing_ for this field. Thus it makes sense to let this value
4136                  // stand in until LDAP is giving a value for this field.
4137                  if (!(empty($value) && $lockval === 'unlockedifempty')) {
4138                      if ($iscustom || (in_array($key, $userauth->userfields) &&
4139                              ((string)$oldinfo->$key !== (string)$value))) {
4140                          $newuser[$key] = (string)$value;
4141                      }
4142                  }
4143              }
4144          }
4145          if ($newuser) {
4146              $newuser['id'] = $oldinfo->id;
4147              $newuser['timemodified'] = time();
4148              user_update_user((object) $newuser, false, false);
4149  
4150              // Save user profile data.
4151              profile_save_data((object) $newuser);
4152  
4153              // Trigger event.
4154              \core\event\user_updated::create_from_userid($newuser['id'])->trigger();
4155          }
4156      }
4157  
4158      return get_complete_user_data('id', $oldinfo->id);
4159  }
4160  
4161  /**
4162   * Will truncate userinfo as it comes from auth_get_userinfo (from external auth) which may have large fields.
4163   *
4164   * @param array $info Array of user properties to truncate if needed
4165   * @return array The now truncated information that was passed in
4166   */
4167  function truncate_userinfo(array $info) {
4168      // Define the limits.
4169      $limit = array(
4170          'username'    => 100,
4171          'idnumber'    => 255,
4172          'firstname'   => 100,
4173          'lastname'    => 100,
4174          'email'       => 100,
4175          'icq'         =>  15,
4176          'phone1'      =>  20,
4177          'phone2'      =>  20,
4178          'institution' => 255,
4179          'department'  => 255,
4180          'address'     => 255,
4181          'city'        => 120,
4182          'country'     =>   2,
4183          'url'         => 255,
4184      );
4185  
4186      // Apply where needed.
4187      foreach (array_keys($info) as $key) {
4188          if (!empty($limit[$key])) {
4189              $info[$key] = trim(core_text::substr($info[$key], 0, $limit[$key]));
4190          }
4191      }
4192  
4193      return $info;
4194  }
4195  
4196  /**
4197   * Marks user deleted in internal user database and notifies the auth plugin.
4198   * Also unenrols user from all roles and does other cleanup.
4199   *
4200   * Any plugin that needs to purge user data should register the 'user_deleted' event.
4201   *
4202   * @param stdClass $user full user object before delete
4203   * @return boolean success
4204   * @throws coding_exception if invalid $user parameter detected
4205   */
4206  function delete_user(stdClass $user) {
4207      global $CFG, $DB, $SESSION;
4208      require_once($CFG->libdir.'/grouplib.php');
4209      require_once($CFG->libdir.'/gradelib.php');
4210      require_once($CFG->dirroot.'/message/lib.php');
4211      require_once($CFG->dirroot.'/user/lib.php');
4212  
4213      // Make sure nobody sends bogus record type as parameter.
4214      if (!property_exists($user, 'id') or !property_exists($user, 'username')) {
4215          throw new coding_exception('Invalid $user parameter in delete_user() detected');
4216      }
4217  
4218      // Better not trust the parameter and fetch the latest info this will be very expensive anyway.
4219      if (!$user = $DB->get_record('user', array('id' => $user->id))) {
4220          debugging('Attempt to delete unknown user account.');
4221          return false;
4222      }
4223  
4224      // There must be always exactly one guest record, originally the guest account was identified by username only,
4225      // now we use $CFG->siteguest for performance reasons.
4226      if ($user->username === 'guest' or isguestuser($user)) {
4227          debugging('Guest user account can not be deleted.');
4228          return false;
4229      }
4230  
4231      // Admin can be theoretically from different auth plugin, but we want to prevent deletion of internal accoutns only,
4232      // if anything goes wrong ppl may force somebody to be admin via config.php setting $CFG->siteadmins.
4233      if ($user->auth === 'manual' and is_siteadmin($user)) {
4234          debugging('Local administrator accounts can not be deleted.');
4235          return false;
4236      }
4237  
4238      // Allow plugins to use this user object before we completely delete it.
4239      if ($pluginsfunction = get_plugins_with_function('pre_user_delete')) {
4240          foreach ($pluginsfunction as $plugintype => $plugins) {
4241              foreach ($plugins as $pluginfunction) {
4242                  $pluginfunction($user);
4243              }
4244          }
4245      }
4246  
4247      // Keep user record before updating it, as we have to pass this to user_deleted event.
4248      $olduser = clone $user;
4249  
4250      // Keep a copy of user context, we need it for event.
4251      $usercontext = context_user::instance($user->id);
4252  
4253      // Delete all grades - backup is kept in grade_grades_history table.
4254      grade_user_delete($user->id);
4255  
4256      // TODO: remove from cohorts using standard API here.
4257  
4258      // Remove user tags.
4259      core_tag_tag::remove_all_item_tags('core', 'user', $user->id);
4260  
4261      // Unconditionally unenrol from all courses.
4262      enrol_user_delete($user);
4263  
4264      // Unenrol from all roles in all contexts.
4265      // This might be slow but it is really needed - modules might do some extra cleanup!
4266      role_unassign_all(array('userid' => $user->id));
4267  
4268      // Notify the competency subsystem.
4269      \core_competency\api::hook_user_deleted($user->id);
4270  
4271      // Now do a brute force cleanup.
4272  
4273      // Delete all user events and subscription events.
4274      $DB->delete_records_select('event', 'userid = :userid AND subscriptionid IS NOT NULL', ['userid' => $user->id]);
4275  
4276      // Now, delete all calendar subscription from the user.
4277      $DB->delete_records('event_subscriptions', ['userid' => $user->id]);
4278  
4279      // Remove from all cohorts.
4280      $DB->delete_records('cohort_members', array('userid' => $user->id));
4281  
4282      // Remove from all groups.
4283      $DB->delete_records('groups_members', array('userid' => $user->id));
4284  
4285      // Brute force unenrol from all courses.
4286      $DB->delete_records('user_enrolments', array('userid' => $user->id));
4287  
4288      // Purge user preferences.
4289      $DB->delete_records('user_preferences', array('userid' => $user->id));
4290  
4291      // Purge user extra profile info.
4292      $DB->delete_records('user_info_data', array('userid' => $user->id));
4293  
4294      // Purge log of previous password hashes.
4295      $DB->delete_records('user_password_history', array('userid' => $user->id));
4296  
4297      // Last course access not necessary either.
4298      $DB->delete_records('user_lastaccess', array('userid' => $user->id));
4299      // Remove all user tokens.
4300      $DB->delete_records('external_tokens', array('userid' => $user->id));
4301  
4302      // Unauthorise the user for all services.
4303      $DB->delete_records('external_services_users', array('userid' => $user->id));
4304  
4305      // Remove users private keys.
4306      $DB->delete_records('user_private_key', array('userid' => $user->id));
4307  
4308      // Remove users customised pages.
4309      $DB->delete_records('my_pages', array('userid' => $user->id, 'private' => 1));
4310  
4311      // Delete user from $SESSION->bulk_users.
4312      if (isset($SESSION->bulk_users[$user->id])) {
4313          unset($SESSION->bulk_users[$user->id]);
4314      }
4315  
4316      // Force logout - may fail if file based sessions used, sorry.
4317      \core\session\manager::kill_user_sessions($user->id);
4318  
4319      // Generate username from email address, or a fake email.
4320      $delemail = !empty($user->email) ? $user->email : $user->username . '.' . $user->id . '@unknownemail.invalid';
4321  
4322      $deltime = time();
4323      $deltimelength = core_text::strlen((string) $deltime);
4324  
4325      // Max username length is 100 chars. Select up to limit - (length of current time + 1 [period character]) from users email.
4326      $delname = clean_param($delemail, PARAM_USERNAME);
4327      $delname = core_text::substr($delname, 0, 100 - ($deltimelength + 1)) . ".{$deltime}";
4328  
4329      // Workaround for bulk deletes of users with the same email address.
4330      while ($DB->record_exists('user', array('username' => $delname))) { // No need to use mnethostid here.
4331          $delname++;
4332      }
4333  
4334      // Mark internal user record as "deleted".
4335      $updateuser = new stdClass();
4336      $updateuser->id           = $user->id;
4337      $updateuser->deleted      = 1;
4338      $updateuser->username     = $delname;            // Remember it just in case.
4339      $updateuser->email        = md5($user->username);// Store hash of username, useful importing/restoring users.
4340      $updateuser->idnumber     = '';                  // Clear this field to free it up.
4341      $updateuser->picture      = 0;
4342      $updateuser->timemodified = $deltime;
4343  
4344      // Don't trigger update event, as user is being deleted.
4345      user_update_user($updateuser, false, false);
4346  
4347      // Delete all content associated with the user context, but not the context itself.
4348      $usercontext->delete_content();
4349  
4350      // Delete any search data.
4351      \core_search\manager::context_deleted($usercontext);
4352  
4353      // Any plugin that needs to cleanup should register this event.
4354      // Trigger event.
4355      $event = \core\event\user_deleted::create(
4356              array(
4357                  'objectid' => $user->id,
4358                  'relateduserid' => $user->id,
4359                  'context' => $usercontext,
4360                  'other' => array(
4361                      'username' => $user->username,
4362                      'email' => $user->email,
4363                      'idnumber' => $user->idnumber,
4364                      'picture' => $user->picture,
4365                      'mnethostid' => $user->mnethostid
4366                      )
4367                  )
4368              );
4369      $event->add_record_snapshot('user', $olduser);
4370      $event->trigger();
4371  
4372      // We will update the user's timemodified, as it will be passed to the user_deleted event, which
4373      // should know about this updated property persisted to the user's table.
4374      $user->timemodified = $updateuser->timemodified;
4375  
4376      // Notify auth plugin - do not block the delete even when plugin fails.
4377      $authplugin = get_auth_plugin($user->auth);
4378      $authplugin->user_delete($user);
4379  
4380      return true;
4381  }
4382  
4383  /**
4384   * Retrieve the guest user object.
4385   *
4386   * @return stdClass A {@link $USER} object
4387   */
4388  function guest_user() {
4389      global $CFG, $DB;
4390  
4391      if ($newuser = $DB->get_record('user', array('id' => $CFG->siteguest))) {
4392          $newuser->confirmed = 1;
4393          $newuser->lang = $CFG->lang;
4394          $newuser->lastip = getremoteaddr();
4395      }
4396  
4397      return $newuser;
4398  }
4399  
4400  /**
4401   * Authenticates a user against the chosen authentication mechanism
4402   *
4403   * Given a username and password, this function looks them
4404   * up using the currently selected authentication mechanism,
4405   * and if the authentication is successful, it returns a
4406   * valid $user object from the 'user' table.
4407   *
4408   * Uses auth_ functions from the currently active auth module
4409   *
4410   * After authenticate_user_login() returns success, you will need to
4411   * log that the user has logged in, and call complete_user_login() to set
4412   * the session up.
4413   *
4414   * Note: this function works only with non-mnet accounts!
4415   *
4416   * @param string $username  User's username (or also email if $CFG->authloginviaemail enabled)
4417   * @param string $password  User's password
4418   * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO
4419   * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists)
4420   * @param mixed logintoken If this is set to a string it is validated against the login token for the session.
4421   * @return stdClass|false A {@link $USER} object or false if error
4422   */
4423  function authenticate_user_login($username, $password, $ignorelockout=false, &$failurereason=null, $logintoken=false) {
4424      global $CFG, $DB, $PAGE;
4425      require_once("$CFG->libdir/authlib.php");
4426  
4427      if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) {
4428          // we have found the user
4429  
4430      } else if (!empty($CFG->authloginviaemail)) {
4431          if ($email = clean_param($username, PARAM_EMAIL)) {
4432              $select = "mnethostid = :mnethostid AND LOWER(email) = LOWER(:email) AND deleted = 0";
4433              $params = array('mnethostid' => $CFG->mnet_localhost_id, 'email' => $email);
4434              $users = $DB->get_records_select('user', $select, $params, 'id', 'id', 0, 2);
4435              if (count($users) === 1) {
4436                  // Use email for login only if unique.
4437                  $user = reset($users);
4438                  $user = get_complete_user_data('id', $user->id);
4439                  $username = $user->username;
4440              }
4441              unset($users);
4442          }
4443      }
4444  
4445      // Make sure this request came from the login form.
4446      if (!\core\session\manager::validate_login_token($logintoken)) {
4447          $failurereason = AUTH_LOGIN_FAILED;
4448  
4449          // Trigger login failed event (specifying the ID of the found user, if available).
4450          \core\event\user_login_failed::create([
4451              'userid' => ($user->id ?? 0),
4452              'other' => [
4453                  'username' => $username,
4454                  'reason' => $failurereason,
4455              ],
4456          ])->trigger();
4457  
4458          error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Invalid Login Token:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4459          return false;
4460      }
4461  
4462      $authsenabled = get_enabled_auth_plugins();
4463  
4464      if ($user) {
4465          // Use manual if auth not set.
4466          $auth = empty($user->auth) ? 'manual' : $user->auth;
4467  
4468          if (in_array($user->auth, $authsenabled)) {
4469              $authplugin = get_auth_plugin($user->auth);
4470              $authplugin->pre_user_login_hook($user);
4471          }
4472  
4473          if (!empty($user->suspended)) {
4474              $failurereason = AUTH_LOGIN_SUSPENDED;
4475  
4476              // Trigger login failed event.
4477              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4478                      'other' => array('username' => $username, 'reason' => $failurereason)));
4479              $event->trigger();
4480              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4481              return false;
4482          }
4483          if ($auth=='nologin' or !is_enabled_auth($auth)) {
4484              // Legacy way to suspend user.
4485              $failurereason = AUTH_LOGIN_SUSPENDED;
4486  
4487              // Trigger login failed event.
4488              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4489                      'other' => array('username' => $username, 'reason' => $failurereason)));
4490              $event->trigger();
4491              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Disabled Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4492              return false;
4493          }
4494          $auths = array($auth);
4495  
4496      } else {
4497          // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user().
4498          if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id,  'deleted' => 1))) {
4499              $failurereason = AUTH_LOGIN_NOUSER;
4500  
4501              // Trigger login failed event.
4502              $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4503                      'reason' => $failurereason)));
4504              $event->trigger();
4505              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Deleted Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4506              return false;
4507          }
4508  
4509          // User does not exist.
4510          $auths = $authsenabled;
4511          $user = new stdClass();
4512          $user->id = 0;
4513      }
4514  
4515      if ($ignorelockout) {
4516          // Some other mechanism protects against brute force password guessing, for example login form might include reCAPTCHA
4517          // or this function is called from a SSO script.
4518      } else if ($user->id) {
4519          // Verify login lockout after other ways that may prevent user login.
4520          if (login_is_lockedout($user)) {
4521              $failurereason = AUTH_LOGIN_LOCKOUT;
4522  
4523              // Trigger login failed event.
4524              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4525                      'other' => array('username' => $username, 'reason' => $failurereason)));
4526              $event->trigger();
4527  
4528              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Login lockout:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4529              return false;
4530          }
4531      } else {
4532          // We can not lockout non-existing accounts.
4533      }
4534  
4535      foreach ($auths as $auth) {
4536          $authplugin = get_auth_plugin($auth);
4537  
4538          // On auth fail fall through to the next plugin.
4539          if (!$authplugin->user_login($username, $password)) {
4540              continue;
4541          }
4542  
4543          // Before performing login actions, check if user still passes password policy, if admin setting is enabled.
4544          if (!empty($CFG->passwordpolicycheckonlogin)) {
4545              $errmsg = '';
4546              $passed = check_password_policy($password, $errmsg, $user);
4547              if (!$passed) {
4548                  // First trigger event for failure.
4549                  $failedevent = \core\event\user_password_policy_failed::create_from_user($user);
4550                  $failedevent->trigger();
4551  
4552                  // If able to change password, set flag and move on.
4553                  if ($authplugin->can_change_password()) {
4554                      // Check if we are on internal change password page, or service is external, don't show notification.
4555                      $internalchangeurl = new moodle_url('/login/change_password.php');
4556                      if (!($PAGE->has_set_url() && $internalchangeurl->compare($PAGE->url)) && $authplugin->is_internal()) {
4557                          \core\notification::error(get_string('passwordpolicynomatch', '', $errmsg));
4558                      }
4559                      set_user_preference('auth_forcepasswordchange', 1, $user);
4560                  } else if ($authplugin->can_reset_password()) {
4561                      // Else force a reset if possible.
4562                      \core\notification::error(get_string('forcepasswordresetnotice', '', $errmsg));
4563                      redirect(new moodle_url('/login/forgot_password.php'));
4564                  } else {
4565                      $notifymsg = get_string('forcepasswordresetfailurenotice', '', $errmsg);
4566                      // If support page is set, add link for help.
4567                      if (!empty($CFG->supportpage)) {
4568                          $link = \html_writer::link($CFG->supportpage, $CFG->supportpage);
4569                          $link = \html_writer::tag('p', $link);
4570                          $notifymsg .= $link;
4571                      }
4572  
4573                      // If no change or reset is possible, add a notification for user.
4574                      \core\notification::error($notifymsg);
4575                  }
4576              }
4577          }
4578  
4579          // Successful authentication.
4580          if ($user->id) {
4581              // User already exists in database.
4582              if (empty($user->auth)) {
4583                  // For some reason auth isn't set yet.
4584                  $DB->set_field('user', 'auth', $auth, array('id' => $user->id));
4585                  $user->auth = $auth;
4586              }
4587  
4588              // If the existing hash is using an out-of-date algorithm (or the legacy md5 algorithm), then we should update to
4589              // the current hash algorithm while we have access to the user's password.
4590              update_internal_user_password($user, $password);
4591  
4592              if ($authplugin->is_synchronised_with_external()) {
4593                  // Update user record from external DB.
4594                  $user = update_user_record_by_id($user->id);
4595              }
4596          } else {
4597              // The user is authenticated but user creation may be disabled.
4598              if (!empty($CFG->authpreventaccountcreation)) {
4599                  $failurereason = AUTH_LOGIN_UNAUTHORISED;
4600  
4601                  // Trigger login failed event.
4602                  $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4603                          'reason' => $failurereason)));
4604                  $event->trigger();
4605  
4606                  error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Unknown user, can not create new accounts:  $username  ".
4607                          $_SERVER['HTTP_USER_AGENT']);
4608                  return false;
4609              } else {
4610                  $user = create_user_record($username, $password, $auth);
4611              }
4612          }
4613  
4614          $authplugin->sync_roles($user);
4615  
4616          foreach ($authsenabled as $hau) {
4617              $hauth = get_auth_plugin($hau);
4618              $hauth->user_authenticated_hook($user, $username, $password);
4619          }
4620  
4621          if (empty($user->id)) {
4622              $failurereason = AUTH_LOGIN_NOUSER;
4623              // Trigger login failed event.
4624              $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4625                      'reason' => $failurereason)));
4626              $event->trigger();
4627              return false;
4628          }
4629  
4630          if (!empty($user->suspended)) {
4631              // Just in case some auth plugin suspended account.
4632              $failurereason = AUTH_LOGIN_SUSPENDED;
4633              // Trigger login failed event.
4634              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4635                      'other' => array('username' => $username, 'reason' => $failurereason)));
4636              $event->trigger();
4637              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4638              return false;
4639          }
4640  
4641          login_attempt_valid($user);
4642          $failurereason = AUTH_LOGIN_OK;
4643          return $user;
4644      }
4645  
4646      // Failed if all the plugins have failed.
4647      if (debugging('', DEBUG_ALL)) {
4648          error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Failed Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4649      }
4650  
4651      if ($user->id) {
4652          login_attempt_failed($user);
4653          $failurereason = AUTH_LOGIN_FAILED;
4654          // Trigger login failed event.
4655          $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4656                  'other' => array('username' => $username, 'reason' => $failurereason)));
4657          $event->trigger();
4658      } else {
4659          $failurereason = AUTH_LOGIN_NOUSER;
4660          // Trigger login failed event.
4661          $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4662                  'reason' => $failurereason)));
4663          $event->trigger();
4664      }
4665  
4666      return false;
4667  }
4668  
4669  /**
4670   * Call to complete the user login process after authenticate_user_login()
4671   * has succeeded. It will setup the $USER variable and other required bits
4672   * and pieces.
4673   *
4674   * NOTE:
4675   * - It will NOT log anything -- up to the caller to decide what to log.
4676   * - this function does not set any cookies any more!
4677   *
4678   * @param stdClass $user
4679   * @return stdClass A {@link $USER} object - BC only, do not use
4680   */
4681  function complete_user_login($user) {
4682      global $CFG, $DB, $USER, $SESSION;
4683  
4684      \core\session\manager::login_user($user);
4685  
4686      // Reload preferences from DB.
4687      unset($USER->preference);
4688      check_user_preferences_loaded($USER);
4689  
4690      // Update login times.
4691      update_user_login_times();
4692  
4693      // Extra session prefs init.
4694      set_login_session_preferences();
4695  
4696      // Trigger login event.
4697      $event = \core\event\user_loggedin::create(
4698          array(
4699              'userid' => $USER->id,
4700              'objectid' => $USER->id,
4701              'other' => array('username' => $USER->username),
4702          )
4703      );
4704      $event->trigger();
4705  
4706      // Queue migrating the messaging data, if we need to.
4707      if (!get_user_preferences('core_message_migrate_data', false, $USER->id)) {
4708          // Check if there are any legacy messages to migrate.
4709          if (\core_message\helper::legacy_messages_exist($USER->id)) {
4710              \core_message\task\migrate_message_data::queue_task($USER->id);
4711          } else {
4712              set_user_preference('core_message_migrate_data', true, $USER->id);
4713          }
4714      }
4715  
4716      if (isguestuser()) {
4717          // No need to continue when user is THE guest.
4718          return $USER;
4719      }
4720  
4721      if (CLI_SCRIPT) {
4722          // We can redirect to password change URL only in browser.
4723          return $USER;
4724      }
4725  
4726      // Select password change url.
4727      $userauth = get_auth_plugin($USER->auth);
4728  
4729      // Check whether the user should be changing password.
4730      if (get_user_preferences('auth_forcepasswordchange', false)) {
4731          if ($userauth->can_change_password()) {
4732              if ($changeurl = $userauth->change_password_url()) {
4733                  redirect($changeurl);
4734              } else {
4735                  require_once($CFG->dirroot . '/login/lib.php');
4736                  $SESSION->wantsurl = core_login_get_return_url();
4737                  redirect($CFG->wwwroot.'/login/change_password.php');
4738              }
4739          } else {
4740              print_error('nopasswordchangeforced', 'auth');
4741          }
4742      }
4743      return $USER;
4744  }
4745  
4746  /**
4747   * Check a password hash to see if it was hashed using the legacy hash algorithm (md5).
4748   *
4749   * @param string $password String to check.
4750   * @return boolean True if the $password matches the format of an md5 sum.
4751   */
4752  function password_is_legacy_hash($password) {
4753      return (bool) preg_match('/^[0-9a-f]{32}$/', $password);
4754  }
4755  
4756  /**
4757   * Compare password against hash stored in user object to determine if it is valid.
4758   *
4759   * If necessary it also updates the stored hash to the current format.
4760   *
4761   * @param stdClass $user (Password property may be updated).
4762   * @param string $password Plain text password.
4763   * @return bool True if password is valid.
4764   */
4765  function validate_internal_user_password($user, $password) {
4766      global $CFG;
4767  
4768      if ($user->password === AUTH_PASSWORD_NOT_CACHED) {
4769          // Internal password is not used at all, it can not validate.
4770          return false;
4771      }
4772  
4773      // If hash isn't a legacy (md5) hash, validate using the library function.
4774      if (!password_is_legacy_hash($user->password)) {
4775          return password_verify($password, $user->password);
4776      }
4777  
4778      // Otherwise we need to check for a legacy (md5) hash instead. If the hash
4779      // is valid we can then update it to the new algorithm.
4780  
4781      $sitesalt = isset($CFG->passwordsaltmain) ? $CFG->passwordsaltmain : '';
4782      $validated = false;
4783  
4784      if ($user->password === md5($password.$sitesalt)
4785              or $user->password === md5($password)
4786              or $user->password === md5(addslashes($password).$sitesalt)
4787              or $user->password === md5(addslashes($password))) {
4788          // Note: we are intentionally using the addslashes() here because we
4789          //       need to accept old password hashes of passwords with magic quotes.
4790          $validated = true;
4791  
4792      } else {
4793          for ($i=1; $i<=20; $i++) { // 20 alternative salts should be enough, right?
4794              $alt = 'passwordsaltalt'.$i;
4795              if (!empty($CFG->$alt)) {
4796                  if ($user->password === md5($password.$CFG->$alt) or $user->password === md5(addslashes($password).$CFG->$alt)) {
4797                      $validated = true;
4798                      break;
4799                  }
4800              }
4801          }
4802      }
4803  
4804      if ($validated) {
4805          // If the password matches the existing md5 hash, update to the
4806          // current hash algorithm while we have access to the user's password.
4807          update_internal_user_password($user, $password);
4808      }
4809  
4810      return $validated;
4811  }
4812  
4813  /**
4814   * Calculate hash for a plain text password.
4815   *
4816   * @param string $password Plain text password to be hashed.
4817   * @param bool $fasthash If true, use a low cost factor when generating the hash
4818   *                       This is much faster to generate but makes the hash
4819   *                       less secure. It is used when lots of hashes need to
4820   *                       be generated quickly.
4821   * @return string The hashed password.
4822   *
4823   * @throws moodle_exception If a problem occurs while generating the hash.
4824   */
4825  function hash_internal_user_password($password, $fasthash = false) {
4826      global $CFG;
4827  
4828      // Set the cost factor to 4 for fast hashing, otherwise use default cost.
4829      $options = ($fasthash) ? array('cost' => 4) : array();
4830  
4831      $generatedhash = password_hash($password, PASSWORD_DEFAULT, $options);
4832  
4833      if ($generatedhash === false || $generatedhash === null) {
4834          throw new moodle_exception('Failed to generate password hash.');
4835      }
4836  
4837      return $generatedhash;
4838  }
4839  
4840  /**
4841   * Update password hash in user object (if necessary).
4842   *
4843   * The password is updated if:
4844   * 1. The password has changed (the hash of $user->password is different
4845   *    to the hash of $password).
4846   * 2. The existing hash is using an out-of-date algorithm (or the legacy
4847   *    md5 algorithm).
4848   *
4849   * Updating the password will modify the $user object and the database
4850   * record to use the current hashing algorithm.
4851   * It will remove Web Services user tokens too.
4852   *
4853   * @param stdClass $user User object (password property may be updated).
4854   * @param string $password Plain text password.
4855   * @param bool $fasthash If true, use a low cost factor when generating the hash
4856   *                       This is much faster to generate but makes the hash
4857   *                       less secure. It is used when lots of hashes need to
4858   *                       be generated quickly.
4859   * @return bool Always returns true.
4860   */
4861  function update_internal_user_password($user, $password, $fasthash = false) {
4862      global $CFG, $DB;
4863  
4864      // Figure out what the hashed password should be.
4865      if (!isset($user->auth)) {
4866          debugging('User record in update_internal_user_password() must include field auth',
4867                  DEBUG_DEVELOPER);
4868          $user->auth = $DB->get_field('user', 'auth', array('id' => $user->id));
4869      }
4870      $authplugin = get_auth_plugin($user->auth);
4871      if ($authplugin->prevent_local_passwords()) {
4872          $hashedpassword = AUTH_PASSWORD_NOT_CACHED;
4873      } else {
4874          $hashedpassword = hash_internal_user_password($password, $fasthash);
4875      }
4876  
4877      $algorithmchanged = false;
4878  
4879      if ($hashedpassword === AUTH_PASSWORD_NOT_CACHED) {
4880          // Password is not cached, update it if not set to AUTH_PASSWORD_NOT_CACHED.
4881          $passwordchanged = ($user->password !== $hashedpassword);
4882  
4883      } else if (isset($user->password)) {
4884          // If verification fails then it means the password has changed.
4885          $passwordchanged = !password_verify($password, $user->password);
4886          $algorithmchanged = password_needs_rehash($user->password, PASSWORD_DEFAULT);
4887      } else {
4888          // While creating new user, password in unset in $user object, to avoid
4889          // saving it with user_create()
4890          $passwordchanged = true;
4891      }
4892  
4893      if ($passwordchanged || $algorithmchanged) {
4894          $DB->set_field('user', 'password',  $hashedpassword, array('id' => $user->id));
4895          $user->password = $hashedpassword;
4896  
4897          // Trigger event.
4898          $user = $DB->get_record('user', array('id' => $user->id));
4899          \core\event\user_password_updated::create_from_user($user)->trigger();
4900  
4901          // Remove WS user tokens.
4902          if (!empty($CFG->passwordchangetokendeletion)) {
4903              require_once($CFG->dirroot.'/webservice/lib.php');
4904              webservice::delete_user_ws_tokens($user->id);
4905          }
4906      }
4907  
4908      return true;
4909  }
4910  
4911  /**
4912   * Get a complete user record, which includes all the info in the user record.
4913   *
4914   * Intended for setting as $USER session variable
4915   *
4916   * @param string $field The user field to be checked for a given value.
4917   * @param string $value The value to match for $field.
4918   * @param int $mnethostid
4919   * @param bool $throwexception If true, it will throw an exception when there's no record found or when there are multiple records
4920   *                              found. Otherwise, it will just return false.
4921   * @return mixed False, or A {@link $USER} object.
4922   */
4923  function get_complete_user_data($field, $value, $mnethostid = null, $throwexception = false) {
4924      global $CFG, $DB;
4925  
4926      if (!$field || !$value) {
4927          return false;
4928      }
4929  
4930      // Change the field to lowercase.
4931      $field = core_text::strtolower($field);
4932  
4933      // List of case insensitive fields.
4934      $caseinsensitivefields = ['email'];
4935  
4936      // Username input is forced to lowercase and should be case sensitive.
4937      if ($field == 'username') {
4938          $value = core_text::strtolower($value);
4939      }
4940  
4941      // Build the WHERE clause for an SQL query.
4942      $params = array('fieldval' => $value);
4943  
4944      // Do a case-insensitive query, if necessary. These are generally very expensive. The performance can be improved on some DBs
4945      // such as MySQL by pre-filtering users with accent-insensitive subselect.
4946      if (in_array($field, $caseinsensitivefields)) {
4947          $fieldselect = $DB->sql_equal($field, ':fieldval', false);
4948          $idsubselect = $DB->sql_equal($field, ':fieldval2', false, false);
4949          $params['fieldval2'] = $value;
4950      } else {
4951          $fieldselect = "$field = :fieldval";
4952          $idsubselect = '';
4953      }
4954      $constraints = "$fieldselect AND deleted <> 1";
4955  
4956      // If we are loading user data based on anything other than id,
4957      // we must also restrict our search based on mnet host.
4958      if ($field != 'id') {
4959          if (empty($mnethostid)) {
4960              // If empty, we restrict to local users.
4961              $mnethostid = $CFG->mnet_localhost_id;
4962          }
4963      }
4964      if (!empty($mnethostid)) {
4965          $params['mnethostid'] = $mnethostid;
4966          $constraints .= " AND mnethostid = :mnethostid";
4967      }
4968  
4969      if ($idsubselect) {
4970          $constraints .= " AND id IN (SELECT id FROM {user} WHERE {$idsubselect})";
4971      }
4972  
4973      // Get all the basic user data.
4974      try {
4975          // Make sure that there's only a single record that matches our query.
4976          // For example, when fetching by email, multiple records might match the query as there's no guarantee that email addresses
4977          // are unique. Therefore we can't reliably tell whether the user profile data that we're fetching is the correct one.
4978          $user = $DB->get_record_select('user', $constraints, $params, '*', MUST_EXIST);
4979      } catch (dml_exception $exception) {
4980          if ($throwexception) {
4981              throw $exception;
4982          } else {
4983              // Return false when no records or multiple records were found.
4984              return false;
4985          }
4986      }
4987  
4988      // Get various settings and preferences.
4989  
4990      // Preload preference cache.
4991      check_user_preferences_loaded($user);
4992  
4993      // Load course enrolment related stuff.
4994      $user->lastcourseaccess    = array(); // During last session.
4995      $user->currentcourseaccess = array(); // During current session.
4996      if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid' => $user->id))) {
4997          foreach ($lastaccesses as $lastaccess) {
4998              $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess;
4999          }
5000      }
5001  
5002      $sql = "SELECT g.id, g.courseid
5003                FROM {groups} g, {groups_members} gm
5004               WHERE gm.groupid=g.id AND gm.userid=?";
5005  
5006      // This is a special hack to speedup calendar display.
5007      $user->groupmember = array();
5008      if (!isguestuser($user)) {
5009          if ($groups = $DB->get_records_sql($sql, array($user->id))) {
5010              foreach ($groups as $group) {
5011                  if (!array_key_exists($group->courseid, $user->groupmember)) {
5012                      $user->groupmember[$group->courseid] = array();
5013                  }
5014                  $user->groupmember[$group->courseid][$group->id] = $group->id;
5015              }
5016          }
5017      }
5018  
5019      // Add cohort theme.
5020      if (!empty($CFG->allowcohortthemes)) {
5021          require_once($CFG->dirroot . '/cohort/lib.php');
5022          if ($cohorttheme = cohort_get_user_cohort_theme($user->id)) {
5023              $user->cohorttheme = $cohorttheme;
5024          }
5025      }
5026  
5027      // Add the custom profile fields to the user record.
5028      $user->profile = array();
5029      if (!isguestuser($user)) {
5030          require_once($CFG->dirroot.'/user/profile/lib.php');
5031          profile_load_custom_fields($user);
5032      }
5033  
5034      // Rewrite some variables if necessary.
5035      if (!empty($user->description)) {
5036          // No need to cart all of it around.
5037          $user->description = true;
5038      }
5039      if (isguestuser($user)) {
5040          // Guest language always same as site.
5041          $user->lang = $CFG->lang;
5042          // Name always in current language.
5043          $user->firstname = get_string('guestuser');
5044          $user->lastname = ' ';
5045      }
5046  
5047      return $user;
5048  }
5049  
5050  /**
5051   * Validate a password against the configured password policy
5052   *
5053   * @param string $password the password to be checked against the password policy
5054   * @param string $errmsg the error message to display when the password doesn't comply with the policy.
5055   * @param stdClass $user the user object to perform password validation against. Defaults to null if not provided.
5056   *
5057   * @return bool true if the password is valid according to the policy. false otherwise.
5058   */
5059  function check_password_policy($password, &$errmsg, $user = null) {
5060      global $CFG;
5061  
5062      if (!empty($CFG->passwordpolicy)) {
5063          $errmsg = '';
5064          if (core_text::strlen($password) < $CFG->minpasswordlength) {
5065              $errmsg .= '<div>'. get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength) .'</div>';
5066          }
5067          if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) {
5068              $errmsg .= '<div>'. get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits) .'</div>';
5069          }
5070          if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) {
5071              $errmsg .= '<div>'. get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower) .'</div>';
5072          }
5073          if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) {
5074              $errmsg .= '<div>'. get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper) .'</div>';
5075          }
5076          if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) {
5077              $errmsg .= '<div>'. get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum) .'</div>';
5078          }
5079          if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) {
5080              $errmsg .= '<div>'. get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars) .'</div>';
5081          }
5082  
5083          // Fire any additional password policy functions from plugins.
5084          // Plugin functions should output an error message string or empty string for success.
5085          $pluginsfunction = get_plugins_with_function('check_password_policy');
5086          foreach ($pluginsfunction as $plugintype => $plugins) {
5087              foreach ($plugins as $pluginfunction) {
5088                  $pluginerr = $pluginfunction($password, $user);
5089                  if ($pluginerr) {
5090                      $errmsg .= '<div>'. $pluginerr .'</div>';
5091                  }
5092              }
5093          }
5094      }
5095  
5096      if ($errmsg == '') {
5097          return true;
5098      } else {
5099          return false;
5100      }
5101  }
5102  
5103  
5104  /**
5105   * When logging in, this function is run to set certain preferences for the current SESSION.
5106   */
5107  function set_login_session_preferences() {
5108      global $SESSION;
5109  
5110      $SESSION->justloggedin = true;
5111  
5112      unset($SESSION->lang);
5113      unset($SESSION->forcelang);
5114      unset($SESSION->load_navigation_admin);
5115  }
5116  
5117  
5118  /**
5119   * Delete a course, including all related data from the database, and any associated files.
5120   *
5121   * @param mixed $courseorid The id of the course or course object to delete.
5122   * @param bool $showfeedback Whether to display notifications of each action the function performs.
5123   * @return bool true if all the removals succeeded. false if there were any failures. If this
5124   *             method returns false, some of the removals will probably have succeeded, and others
5125   *             failed, but you have no way of knowing which.
5126   */
5127  function delete_course($courseorid, $showfeedback = true) {
5128      global $DB;
5129  
5130      if (is_object($courseorid)) {
5131          $courseid = $courseorid->id;
5132          $course   = $courseorid;
5133      } else {
5134          $courseid = $courseorid;
5135          if (!$course = $DB->get_record('course', array('id' => $courseid))) {
5136              return false;
5137          }
5138      }
5139      $context = context_course::instance($courseid);
5140  
5141      // Frontpage course can not be deleted!!
5142      if ($courseid == SITEID) {
5143          return false;
5144      }
5145  
5146      // Allow plugins to use this course before we completely delete it.
5147      if ($pluginsfunction = get_plugins_with_function('pre_course_delete')) {
5148          foreach ($pluginsfunction as $plugintype => $plugins) {
5149              foreach ($plugins as $pluginfunction) {
5150                  $pluginfunction($course);
5151              }
5152          }
5153      }
5154  
5155      // Tell the search manager we are about to delete a course. This prevents us sending updates
5156      // for each individual context being deleted.
5157      \core_search\manager::course_deleting_start($courseid);
5158  
5159      $handler = core_course\customfield\course_handler::create();
5160      $handler->delete_instance($courseid);
5161  
5162      // Make the course completely empty.
5163      remove_course_contents($courseid, $showfeedback);
5164  
5165      // Delete the course and related context instance.
5166      context_helper::delete_instance(CONTEXT_COURSE, $courseid);
5167  
5168      $DB->delete_records("course", array("id" => $courseid));
5169      $DB->delete_records("course_format_options", array("courseid" => $courseid));
5170  
5171      // Reset all course related caches here.
5172      if (class_exists('format_base', false)) {
5173          format_base::reset_course_cache($courseid);
5174      }
5175  
5176      // Tell search that we have deleted the course so it can delete course data from the index.
5177      \core_search\manager::course_deleting_finish($courseid);
5178  
5179      // Trigger a course deleted event.
5180      $event = \core\event\course_deleted::create(array(
5181          'objectid' => $course->id,
5182          'context' => $context,
5183          'other' => array(
5184              'shortname' => $course->shortname,
5185              'fullname' => $course->fullname,
5186              'idnumber' => $course->idnumber
5187              )
5188      ));
5189      $event->add_record_snapshot('course', $course);
5190      $event->trigger();
5191  
5192      return true;
5193  }
5194  
5195  /**
5196   * Clear a course out completely, deleting all content but don't delete the course itself.
5197   *
5198   * This function does not verify any permissions.
5199   *
5200   * Please note this function also deletes all user enrolments,
5201   * enrolment instances and role assignments by default.
5202   *
5203   * $options:
5204   *  - 'keep_roles_and_enrolments' - false by default
5205   *  - 'keep_groups_and_groupings' - false by default
5206   *
5207   * @param int $courseid The id of the course that is being deleted
5208   * @param bool $showfeedback Whether to display notifications of each action the function performs.
5209   * @param array $options extra options
5210   * @return bool true if all the removals succeeded. false if there were any failures. If this
5211   *             method returns false, some of the removals will probably have succeeded, and others
5212   *             failed, but you have no way of knowing which.
5213   */
5214  function remove_course_contents($courseid, $showfeedback = true, array $options = null) {
5215      global $CFG, $DB, $OUTPUT;
5216  
5217      require_once($CFG->libdir.'/badgeslib.php');
5218      require_once($CFG->libdir.'/completionlib.php');
5219      require_once($CFG->libdir.'/questionlib.php');
5220      require_once($CFG->libdir.'/gradelib.php');
5221      require_once($CFG->dirroot.'/group/lib.php');
5222      require_once($CFG->dirroot.'/comment/lib.php');
5223      require_once($CFG->dirroot.'/rating/lib.php');
5224      require_once($CFG->dirroot.'/notes/lib.php');
5225  
5226      // Handle course badges.
5227      badges_handle_course_deletion($courseid);
5228  
5229      // NOTE: these concatenated strings are suboptimal, but it is just extra info...
5230      $strdeleted = get_string('deleted').' - ';
5231  
5232      // Some crazy wishlist of stuff we should skip during purging of course content.
5233      $options = (array)$options;
5234  
5235      $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
5236      $coursecontext = context_course::instance($courseid);
5237      $fs = get_file_storage();
5238  
5239      // Delete course completion information, this has to be done before grades and enrols.
5240      $cc = new completion_info($course);
5241      $cc->clear_criteria();
5242      if ($showfeedback) {
5243          echo $OUTPUT->notification($strdeleted.get_string('completion', 'completion'), 'notifysuccess');
5244      }
5245  
5246      // Remove all data from gradebook - this needs to be done before course modules
5247      // because while deleting this information, the system may need to reference
5248      // the course modules that own the grades.
5249      remove_course_grades($courseid, $showfeedback);
5250      remove_grade_letters($coursecontext, $showfeedback);
5251  
5252      // Delete course blocks in any all child contexts,
5253      // they may depend on modules so delete them first.
5254      $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
5255      foreach ($childcontexts as $childcontext) {
5256          blocks_delete_all_for_context($childcontext->id);
5257      }
5258      unset($childcontexts);
5259      blocks_delete_all_for_context($coursecontext->id);
5260      if ($showfeedback) {
5261          echo $OUTPUT->notification($strdeleted.get_string('type_block_plural', 'plugin'), 'notifysuccess');
5262      }
5263  
5264      $DB->set_field('course_modules', 'deletioninprogress', '1', ['course' => $courseid]);
5265      rebuild_course_cache($courseid, true);
5266  
5267      // Get the list of all modules that are properly installed.
5268      $allmodules = $DB->get_records_menu('modules', array(), '', 'name, id');
5269  
5270      // Delete every instance of every module,
5271      // this has to be done before deleting of course level stuff.
5272      $locations = core_component::get_plugin_list('mod');
5273      foreach ($locations as $modname => $moddir) {
5274          if ($modname === 'NEWMODULE') {
5275              continue;
5276          }
5277          if (array_key_exists($modname, $allmodules)) {
5278              $sql = "SELECT cm.*, m.id AS modinstance, m.name, '$modname' AS modname
5279                FROM {".$modname."} m
5280                     LEFT JOIN {course_modules} cm ON cm.instance = m.id AND cm.module = :moduleid
5281               WHERE m.course = :courseid";
5282              $instances = $DB->get_records_sql($sql, array('courseid' => $course->id,
5283                  'modulename' => $modname, 'moduleid' => $allmodules[$modname]));
5284  
5285              include_once("$moddir/lib.php");                 // Shows php warning only if plugin defective.
5286              $moddelete = $modname .'_delete_instance';       // Delete everything connected to an instance.
5287  
5288              if ($instances) {
5289                  foreach ($instances as $cm) {
5290                      if ($cm->id) {
5291                          // Delete activity context questions and question categories.
5292                          question_delete_activity($cm);
5293                          // Notify the competency subsystem.
5294                          \core_competency\api::hook_course_module_deleted($cm);
5295                      }
5296                      if (function_exists($moddelete)) {
5297                          // This purges all module data in related tables, extra user prefs, settings, etc.
5298                          $moddelete($cm->modinstance);
5299                      } else {
5300                          // NOTE: we should not allow installation of modules with missing delete support!
5301                          debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!");
5302                          $DB->delete_records($modname, array('id' => $cm->modinstance));
5303                      }
5304  
5305                      if ($cm->id) {
5306                          // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition.
5307                          context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
5308                          $DB->delete_records('course_modules_completion', ['coursemoduleid' => $cm->id]);
5309                          $DB->delete_records('course_modules', array('id' => $cm->id));
5310                          rebuild_course_cache($cm->course, true);
5311                      }
5312                  }
5313              }
5314              if ($instances and $showfeedback) {
5315                  echo $OUTPUT->notification($strdeleted.get_string('pluginname', $modname), 'notifysuccess');
5316              }
5317          } else {
5318              // Ooops, this module is not properly installed, force-delete it in the next block.
5319          }
5320      }
5321  
5322      // We have tried to delete everything the nice way - now let's force-delete any remaining module data.
5323  
5324      // Delete completion defaults.
5325      $DB->delete_records("course_completion_defaults", array("course" => $courseid));
5326  
5327      // Remove all data from availability and completion tables that is associated
5328      // with course-modules belonging to this course. Note this is done even if the
5329      // features are not enabled now, in case they were enabled previously.
5330      $DB->delete_records_subquery('course_modules_completion', 'coursemoduleid', 'id',
5331              'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
5332  
5333      // Remove course-module data that has not been removed in modules' _delete_instance callbacks.
5334      $cms = $DB->get_records('course_modules', array('course' => $course->id));
5335      $allmodulesbyid = array_flip($allmodules);
5336      foreach ($cms as $cm) {
5337          if (array_key_exists($cm->module, $allmodulesbyid)) {
5338              try {
5339                  $DB->delete_records($allmodulesbyid[$cm->module], array('id' => $cm->instance));
5340              } catch (Exception $e) {
5341                  // Ignore weird or missing table problems.
5342              }
5343          }
5344          context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
5345          $DB->delete_records('course_modules', array('id' => $cm->id));
5346          rebuild_course_cache($cm->course, true);
5347      }
5348  
5349      if ($showfeedback) {
5350          echo $OUTPUT->notification($strdeleted.get_string('type_mod_plural', 'plugin'), 'notifysuccess');
5351      }
5352  
5353      // Delete questions and question categories.
5354      question_delete_course($course);
5355      if ($showfeedback) {
5356          echo $OUTPUT->notification($strdeleted.get_string('questions', 'question'), 'notifysuccess');
5357      }
5358  
5359      // Delete content bank contents.
5360      $cb = new \core_contentbank\contentbank();
5361      $cbdeleted = $cb->delete_contents($coursecontext);
5362      if ($showfeedback && $cbdeleted) {
5363          echo $OUTPUT->notification($strdeleted.get_string('contentbank', 'contentbank'), 'notifysuccess');
5364      }
5365  
5366      // Make sure there are no subcontexts left - all valid blocks and modules should be already gone.
5367      $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
5368      foreach ($childcontexts as $childcontext) {
5369          $childcontext->delete();
5370      }
5371      unset($childcontexts);
5372  
5373      // Remove roles and enrolments by default.
5374      if (empty($options['keep_roles_and_enrolments'])) {
5375          // This hack is used in restore when deleting contents of existing course.
5376          // During restore, we should remove only enrolment related data that the user performing the restore has a
5377          // permission to remove.
5378          $userid = $options['userid'] ?? null;
5379          enrol_course_delete($course, $userid);
5380          role_unassign_all(array('contextid' => $coursecontext->id, 'component' => ''), true);
5381          if ($showfeedback) {
5382              echo $OUTPUT->notification($strdeleted.get_string('type_enrol_plural', 'plugin'), 'notifysuccess');
5383          }
5384      }
5385  
5386      // Delete any groups, removing members and grouping/course links first.
5387      if (empty($options['keep_groups_and_groupings'])) {
5388          groups_delete_groupings($course->id, $showfeedback);
5389          groups_delete_groups($course->id, $showfeedback);
5390      }
5391  
5392      // Filters be gone!
5393      filter_delete_all_for_context($coursecontext->id);
5394  
5395      // Notes, you shall not pass!
5396      note_delete_all($course->id);
5397  
5398      // Die comments!
5399      comment::delete_comments($coursecontext->id);
5400  
5401      // Ratings are history too.
5402      $delopt = new stdclass();
5403      $delopt->contextid = $coursecontext->id;
5404      $rm = new rating_manager();
5405      $rm->delete_ratings($delopt);
5406  
5407      // Delete course tags.
5408      core_tag_tag::remove_all_item_tags('core', 'course', $course->id);
5409  
5410      // Notify the competency subsystem.
5411      \core_competency\api::hook_course_deleted($course);
5412  
5413      // Delete calendar events.
5414      $DB->delete_records('event', array('courseid' => $course->id));
5415      $fs->delete_area_files($coursecontext->id, 'calendar');
5416  
5417      // Delete all related records in other core tables that may have a courseid
5418      // This array stores the tables that need to be cleared, as
5419      // table_name => column_name that contains the course id.
5420      $tablestoclear = array(
5421          'backup_courses' => 'courseid',  // Scheduled backup stuff.
5422          'user_lastaccess' => 'courseid', // User access info.
5423      );
5424      foreach ($tablestoclear as $table => $col) {
5425          $DB->delete_records($table, array($col => $course->id));
5426      }
5427  
5428      // Delete all course backup files.
5429      $fs->delete_area_files($coursecontext->id, 'backup');
5430  
5431      // Cleanup course record - remove links to deleted stuff.
5432      $oldcourse = new stdClass();
5433      $oldcourse->id               = $course->id;
5434      $oldcourse->summary          = '';
5435      $oldcourse->cacherev         = 0;
5436      $oldcourse->legacyfiles      = 0;
5437      if (!empty($options['keep_groups_and_groupings'])) {
5438          $oldcourse->defaultgroupingid = 0;
5439      }
5440      $DB->update_record('course', $oldcourse);
5441  
5442      // Delete course sections.
5443      $DB->delete_records('course_sections', array('course' => $course->id));
5444  
5445      // Delete legacy, section and any other course files.
5446      $fs->delete_area_files($coursecontext->id, 'course'); // Files from summary and section.
5447  
5448      // Delete all remaining stuff linked to context such as files, comments, ratings, etc.
5449      if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) {
5450          // Easy, do not delete the context itself...
5451          $coursecontext->delete_content();
5452      } else {
5453          // Hack alert!!!!
5454          // We can not drop all context stuff because it would bork enrolments and roles,
5455          // there might be also files used by enrol plugins...
5456      }
5457  
5458      // Delete legacy files - just in case some files are still left there after conversion to new file api,
5459      // also some non-standard unsupported plugins may try to store something there.
5460      fulldelete($CFG->dataroot.'/'.$course->id);
5461  
5462      // Delete from cache to reduce the cache size especially makes sense in case of bulk course deletion.
5463      $cachemodinfo = cache::make('core', 'coursemodinfo');
5464      $cachemodinfo->delete($courseid);
5465  
5466      // Trigger a course content deleted event.
5467      $event = \core\event\course_content_deleted::create(array(
5468          'objectid' => $course->id,
5469          'context' => $coursecontext,
5470          'other' => array('shortname' => $course->shortname,
5471                           'fullname' => $course->fullname,
5472                           'options' => $options) // Passing this for legacy reasons.
5473      ));
5474      $event->add_record_snapshot('course', $course);
5475      $event->trigger();
5476  
5477      return true;
5478  }
5479  
5480  /**
5481   * Change dates in module - used from course reset.
5482   *
5483   * @param string $modname forum, assignment, etc
5484   * @param array $fields array of date fields from mod table
5485   * @param int $timeshift time difference
5486   * @param int $courseid
5487   * @param int $modid (Optional) passed if specific mod instance in course needs to be updated.
5488   * @return bool success
5489   */
5490  function shift_course_mod_dates($modname, $fields, $timeshift, $courseid, $modid = 0) {
5491      global $CFG, $DB;
5492      include_once($CFG->dirroot.'/mod/'.$modname.'/lib.php');
5493  
5494      $return = true;
5495      $params = array($timeshift, $courseid);
5496      foreach ($fields as $field) {
5497          $updatesql = "UPDATE {".$modname."}
5498                            SET $field = $field + ?
5499                          WHERE course=? AND $field<>0";
5500          if ($modid) {
5501              $updatesql .= ' AND id=?';
5502              $params[] = $modid;
5503          }
5504          $return = $DB->execute($updatesql, $params) && $return;
5505      }
5506  
5507      return $return;
5508  }
5509  
5510  /**
5511   * This function will empty a course of user data.
5512   * It will retain the activities and the structure of the course.
5513   *
5514   * @param object $data an object containing all the settings including courseid (without magic quotes)
5515   * @return array status array of array component, item, error
5516   */
5517  function reset_course_userdata($data) {
5518      global $CFG, $DB;
5519      require_once($CFG->libdir.'/gradelib.php');
5520      require_once($CFG->libdir.'/completionlib.php');
5521      require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php');
5522      require_once($CFG->dirroot.'/group/lib.php');
5523  
5524      $data->courseid = $data->id;
5525      $context = context_course::instance($data->courseid);
5526  
5527      $eventparams = array(
5528          'context' => $context,
5529          'courseid' => $data->id,
5530          'other' => array(
5531              'reset_options' => (array) $data
5532          )
5533      );
5534      $event = \core\event\course_reset_started::create($eventparams);
5535      $event->trigger();
5536  
5537      // Calculate the time shift of dates.
5538      if (!empty($data->reset_start_date)) {
5539          // Time part of course startdate should be zero.
5540          $data->timeshift = $data->reset_start_date - usergetmidnight($data->reset_start_date_old);
5541      } else {
5542          $data->timeshift = 0;
5543      }
5544  
5545      // Result array: component, item, error.
5546      $status = array();
5547  
5548      // Start the resetting.
5549      $componentstr = get_string('general');
5550  
5551      // Move the course start time.
5552      if (!empty($data->reset_start_date) and $data->timeshift) {
5553          // Change course start data.
5554          $DB->set_field('course', 'startdate', $data->reset_start_date, array('id' => $data->courseid));
5555          // Update all course and group events - do not move activity events.
5556          $updatesql = "UPDATE {event}
5557                           SET timestart = timestart + ?
5558                         WHERE courseid=? AND instance=0";
5559          $DB->execute($updatesql, array($data->timeshift, $data->courseid));
5560  
5561          // Update any date activity restrictions.
5562          if ($CFG->enableavailability) {
5563              \availability_date\condition::update_all_dates($data->courseid, $data->timeshift);
5564          }
5565  
5566          // Update completion expected dates.
5567          if ($CFG->enablecompletion) {
5568              $modinfo = get_fast_modinfo($data->courseid);
5569              $changed = false;
5570              foreach ($modinfo->get_cms() as $cm) {
5571                  if ($cm->completion && !empty($cm->completionexpected)) {
5572                      $DB->set_field('course_modules', 'completionexpected', $cm->completionexpected + $data->timeshift,
5573                          array('id' => $cm->id));
5574                      $changed = true;
5575                  }
5576              }
5577  
5578              // Clear course cache if changes made.
5579              if ($changed) {
5580                  rebuild_course_cache($data->courseid, true);
5581              }
5582  
5583              // Update course date completion criteria.
5584              \completion_criteria_date::update_date($data->courseid, $data->timeshift);
5585          }
5586  
5587          $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false);
5588      }
5589  
5590      if (!empty($data->reset_end_date)) {
5591          // If the user set a end date value respect it.
5592          $DB->set_field('course', 'enddate', $data->reset_end_date, array('id' => $data->courseid));
5593      } else if ($data->timeshift > 0 && $data->reset_end_date_old) {
5594          // If there is a time shift apply it to the end date as well.
5595          $enddate = $data->reset_end_date_old + $data->timeshift;
5596          $DB->set_field('course', 'enddate', $enddate, array('id' => $data->courseid));
5597      }
5598  
5599      if (!empty($data->reset_events)) {
5600          $DB->delete_records('event', array('courseid' => $data->courseid));
5601          $status[] = array('component' => $componentstr, 'item' => get_string('deleteevents', 'calendar'), 'error' => false);
5602      }
5603  
5604      if (!empty($data->reset_notes)) {
5605          require_once($CFG->dirroot.'/notes/lib.php');
5606          note_delete_all($data->courseid);
5607          $status[] = array('component' => $componentstr, 'item' => get_string('deletenotes', 'notes'), 'error' => false);
5608      }
5609  
5610      if (!empty($data->delete_blog_associations)) {
5611          require_once($CFG->dirroot.'/blog/lib.php');
5612          blog_remove_associations_for_course($data->courseid);
5613          $status[] = array('component' => $componentstr, 'item' => get_string('deleteblogassociations', 'blog'), 'error' => false);
5614      }
5615  
5616      if (!empty($data->reset_completion)) {
5617          // Delete course and activity completion information.
5618          $course = $DB->get_record('course', array('id' => $data->courseid));
5619          $cc = new completion_info($course);
5620          $cc->delete_all_completion_data();
5621          $status[] = array('component' => $componentstr,
5622                  'item' => get_string('deletecompletiondata', 'completion'), 'error' => false);
5623      }
5624  
5625      if (!empty($data->reset_competency_ratings)) {
5626          \core_competency\api::hook_course_reset_competency_ratings($data->courseid);
5627          $status[] = array('component' => $componentstr,
5628              'item' => get_string('deletecompetencyratings', 'core_competency'), 'error' => false);
5629      }
5630  
5631      $componentstr = get_string('roles');
5632  
5633      if (!empty($data->reset_roles_overrides)) {
5634          $children = $context->get_child_contexts();
5635          foreach ($children as $child) {
5636              $child->delete_capabilities();
5637          }
5638          $context->delete_capabilities();
5639          $status[] = array('component' => $componentstr, 'item' => get_string('deletecourseoverrides', 'role'), 'error' => false);
5640      }
5641  
5642      if (!empty($data->reset_roles_local)) {
5643          $children = $context->get_child_contexts();
5644          foreach ($children as $child) {
5645              role_unassign_all(array('contextid' => $child->id));
5646          }
5647          $status[] = array('component' => $componentstr, 'item' => get_string('deletelocalroles', 'role'), 'error' => false);
5648      }
5649  
5650      // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc.
5651      $data->unenrolled = array();
5652      if (!empty($data->unenrol_users)) {
5653          $plugins = enrol_get_plugins(true);
5654          $instances = enrol_get_instances($data->courseid, true);
5655          foreach ($instances as $key => $instance) {
5656              if (!isset($plugins[$instance->enrol])) {
5657                  unset($instances[$key]);
5658                  continue;
5659              }
5660          }
5661  
5662          $usersroles = enrol_get_course_users_roles($data->courseid);
5663          foreach ($data->unenrol_users as $withroleid) {
5664              if ($withroleid) {
5665                  $sql = "SELECT ue.*
5666                            FROM {user_enrolments} ue
5667                            JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5668                            JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5669                            JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)";
5670                  $params = array('courseid' => $data->courseid, 'roleid' => $withroleid, 'courselevel' => CONTEXT_COURSE);
5671  
5672              } else {
5673                  // Without any role assigned at course context.
5674                  $sql = "SELECT ue.*
5675                            FROM {user_enrolments} ue
5676                            JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5677                            JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5678                       LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid)
5679                           WHERE ra.id IS null";
5680                  $params = array('courseid' => $data->courseid, 'courselevel' => CONTEXT_COURSE);
5681              }
5682  
5683              $rs = $DB->get_recordset_sql($sql, $params);
5684              foreach ($rs as $ue) {
5685                  if (!isset($instances[$ue->enrolid])) {
5686                      continue;
5687                  }
5688                  $instance = $instances[$ue->enrolid];
5689                  $plugin = $plugins[$instance->enrol];
5690                  if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) {
5691                      continue;
5692                  }
5693  
5694                  if ($withroleid && count($usersroles[$ue->userid]) > 1) {
5695                      // If we don't remove all roles and user has more than one role, just remove this role.
5696                      role_unassign($withroleid, $ue->userid, $context->id);
5697  
5698                      unset($usersroles[$ue->userid][$withroleid]);
5699                  } else {
5700                      // If we remove all roles or user has only one role, unenrol user from course.
5701                      $plugin->unenrol_user($instance, $ue->userid);
5702                  }
5703                  $data->unenrolled[$ue->userid] = $ue->userid;
5704              }
5705              $rs->close();
5706          }
5707      }
5708      if (!empty($data->unenrolled)) {
5709          $status[] = array(
5710              'component' => $componentstr,
5711              'item' => get_string('unenrol', 'enrol').' ('.count($data->unenrolled).')',
5712              'error' => false
5713          );
5714      }
5715  
5716      $componentstr = get_string('groups');
5717  
5718      // Remove all group members.
5719      if (!empty($data->reset_groups_members)) {
5720          groups_delete_group_members($data->courseid);
5721          $status[] = array('component' => $componentstr, 'item' => get_string('removegroupsmembers', 'group'), 'error' => false);
5722      }
5723  
5724      // Remove all groups.
5725      if (!empty($data->reset_groups_remove)) {
5726          groups_delete_groups($data->courseid, false);
5727          $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroups', 'group'), 'error' => false);
5728      }
5729  
5730      // Remove all grouping members.
5731      if (!empty($data->reset_groupings_members)) {
5732          groups_delete_groupings_groups($data->courseid, false);
5733          $status[] = array('component' => $componentstr, 'item' => get_string('removegroupingsmembers', 'group'), 'error' => false);
5734      }
5735  
5736      // Remove all groupings.
5737      if (!empty($data->reset_groupings_remove)) {
5738          groups_delete_groupings($data->courseid, false);
5739          $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroupings', 'group'), 'error' => false);
5740      }
5741  
5742      // Look in every instance of every module for data to delete.
5743      $unsupportedmods = array();
5744      if ($allmods = $DB->get_records('modules') ) {
5745          foreach ($allmods as $mod) {
5746              $modname = $mod->name;
5747              $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php';
5748              $moddeleteuserdata = $modname.'_reset_userdata';   // Function to delete user data.
5749              if (file_exists($modfile)) {
5750                  if (!$DB->count_records($modname, array('course' => $data->courseid))) {
5751                      continue; // Skip mods with no instances.
5752                  }
5753                  include_once($modfile);
5754                  if (function_exists($moddeleteuserdata)) {
5755                      $modstatus = $moddeleteuserdata($data);
5756                      if (is_array($modstatus)) {
5757                          $status = array_merge($status, $modstatus);
5758                      } else {
5759                          debugging('Module '.$modname.' returned incorrect staus - must be an array!');
5760                      }
5761                  } else {
5762                      $unsupportedmods[] = $mod;
5763                  }
5764              } else {
5765                  debugging('Missing lib.php in '.$modname.' module!');
5766              }
5767              // Update calendar events for all modules.
5768              course_module_bulk_update_calendar_events($modname, $data->courseid);
5769          }
5770      }
5771  
5772      // Mention unsupported mods.
5773      if (!empty($unsupportedmods)) {
5774          foreach ($unsupportedmods as $mod) {
5775              $status[] = array(
5776                  'component' => get_string('modulenameplural', $mod->name),
5777                  'item' => '',
5778                  'error' => get_string('resetnotimplemented')
5779              );
5780          }
5781      }
5782  
5783      $componentstr = get_string('gradebook', 'grades');
5784      // Reset gradebook,.
5785      if (!empty($data->reset_gradebook_items)) {
5786          remove_course_grades($data->courseid, false);
5787          grade_grab_course_grades($data->courseid);
5788          grade_regrade_final_grades($data->courseid);
5789          $status[] = array('component' => $componentstr, 'item' => get_string('removeallcourseitems', 'grades'), 'error' => false);
5790  
5791      } else if (!empty($data->reset_gradebook_grades)) {
5792          grade_course_reset($data->courseid);
5793          $status[] = array('component' => $componentstr, 'item' => get_string('removeallcoursegrades', 'grades'), 'error' => false);
5794      }
5795      // Reset comments.
5796      if (!empty($data->reset_comments)) {
5797          require_once($CFG->dirroot.'/comment/lib.php');
5798          comment::reset_course_page_comments($context);
5799      }
5800  
5801      $event = \core\event\course_reset_ended::create($eventparams);
5802      $event->trigger();
5803  
5804      return $status;
5805  }
5806  
5807  /**
5808   * Generate an email processing address.
5809   *
5810   * @param int $modid
5811   * @param string $modargs
5812   * @return string Returns email processing address
5813   */
5814  function generate_email_processing_address($modid, $modargs) {
5815      global $CFG;
5816  
5817      $header = $CFG->mailprefix . substr(base64_encode(pack('C', $modid)), 0, 2).$modargs;
5818      return $header . substr(md5($header.get_site_identifier()), 0, 16).'@'.$CFG->maildomain;
5819  }
5820  
5821  /**
5822   * ?
5823   *
5824   * @todo Finish documenting this function
5825   *
5826   * @param string $modargs
5827   * @param string $body Currently unused
5828   */
5829  function moodle_process_email($modargs, $body) {
5830      global $DB;
5831  
5832      // The first char should be an unencoded letter. We'll take this as an action.
5833      switch ($modargs[0]) {
5834          case 'B': { // Bounce.
5835              list(, $userid) = unpack('V', base64_decode(substr($modargs, 1, 8)));
5836              if ($user = $DB->get_record("user", array('id' => $userid), "id,email")) {
5837                  // Check the half md5 of their email.
5838                  $md5check = substr(md5($user->email), 0, 16);
5839                  if ($md5check == substr($modargs, -16)) {
5840                      set_bounce_count($user);
5841                  }
5842                  // Else maybe they've already changed it?
5843              }
5844          }
5845          break;
5846          // Maybe more later?
5847      }
5848  }
5849  
5850  // CORRESPONDENCE.
5851  
5852  /**
5853   * Get mailer instance, enable buffering, flush buffer or disable buffering.
5854   *
5855   * @param string $action 'get', 'buffer', 'close' or 'flush'
5856   * @return moodle_phpmailer|null mailer instance if 'get' used or nothing
5857   */
5858  function get_mailer($action='get') {
5859      global $CFG;
5860  
5861      /** @var moodle_phpmailer $mailer */
5862      static $mailer  = null;
5863      static $counter = 0;
5864  
5865      if (!isset($CFG->smtpmaxbulk)) {
5866          $CFG->smtpmaxbulk = 1;
5867      }
5868  
5869      if ($action == 'get') {
5870          $prevkeepalive = false;
5871  
5872          if (isset($mailer) and $mailer->Mailer == 'smtp') {
5873              if ($counter < $CFG->smtpmaxbulk and !$mailer->isError()) {
5874                  $counter++;
5875                  // Reset the mailer.
5876                  $mailer->Priority         = 3;
5877                  $mailer->CharSet          = 'UTF-8'; // Our default.
5878                  $mailer->ContentType      = "text/plain";
5879                  $mailer->Encoding         = "8bit";
5880                  $mailer->From             = "root@localhost";
5881                  $mailer->FromName         = "Root User";
5882                  $mailer->Sender           = "";
5883                  $mailer->Subject          = "";
5884                  $mailer->Body             = "";
5885                  $mailer->AltBody          = "";
5886                  $mailer->ConfirmReadingTo = "";
5887  
5888                  $mailer->clearAllRecipients();
5889                  $mailer->clearReplyTos();
5890                  $mailer->clearAttachments();
5891                  $mailer->clearCustomHeaders();
5892                  return $mailer;
5893              }
5894  
5895              $prevkeepalive = $mailer->SMTPKeepAlive;
5896              get_mailer('flush');
5897          }
5898  
5899          require_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php');
5900          $mailer = new moodle_phpmailer();
5901  
5902          $counter = 1;
5903  
5904          if ($CFG->smtphosts == 'qmail') {
5905              // Use Qmail system.
5906              $mailer->isQmail();
5907  
5908          } else if (empty($CFG->smtphosts)) {
5909              // Use PHP mail() = sendmail.
5910              $mailer->isMail();
5911  
5912          } else {
5913              // Use SMTP directly.
5914              $mailer->isSMTP();
5915              if (!empty($CFG->debugsmtp) && (!empty($CFG->debugdeveloper))) {
5916                  $mailer->SMTPDebug = 3;
5917              }
5918              // Specify main and backup servers.
5919              $mailer->Host          = $CFG->smtphosts;
5920              // Specify secure connection protocol.
5921              $mailer->SMTPSecure    = $CFG->smtpsecure;
5922              // Use previous keepalive.
5923              $mailer->SMTPKeepAlive = $prevkeepalive;
5924  
5925              if ($CFG->smtpuser) {
5926                  // Use SMTP authentication.
5927                  $mailer->SMTPAuth = true;
5928                  $mailer->Username = $CFG->smtpuser;
5929                  $mailer->Password = $CFG->smtppass;
5930              }
5931          }
5932  
5933          return $mailer;
5934      }
5935  
5936      $nothing = null;
5937  
5938      // Keep smtp session open after sending.
5939      if ($action == 'buffer') {
5940          if (!empty($CFG->smtpmaxbulk)) {
5941              get_mailer('flush');
5942              $m = get_mailer();
5943              if ($m->Mailer == 'smtp') {
5944                  $m->SMTPKeepAlive = true;
5945              }
5946          }
5947          return $nothing;
5948      }
5949  
5950      // Close smtp session, but continue buffering.
5951      if ($action == 'flush') {
5952          if (isset($mailer) and $mailer->Mailer == 'smtp') {
5953              if (!empty($mailer->SMTPDebug)) {
5954                  echo '<pre>'."\n";
5955              }
5956              $mailer->SmtpClose();
5957              if (!empty($mailer->SMTPDebug)) {
5958                  echo '</pre>';
5959              }
5960          }
5961          return $nothing;
5962      }
5963  
5964      // Close smtp session, do not buffer anymore.
5965      if ($action == 'close') {
5966          if (isset($mailer) and $mailer->Mailer == 'smtp') {
5967              get_mailer('flush');
5968              $mailer->SMTPKeepAlive = false;
5969          }
5970          $mailer = null; // Better force new instance.
5971          return $nothing;
5972      }
5973  }
5974  
5975  /**
5976   * A helper function to test for email diversion
5977   *
5978   * @param string $email
5979   * @return bool Returns true if the email should be diverted
5980   */
5981  function email_should_be_diverted($email) {
5982      global $CFG;
5983  
5984      if (empty($CFG->divertallemailsto)) {
5985          return false;
5986      }
5987  
5988      if (empty($CFG->divertallemailsexcept)) {
5989          return true;
5990      }
5991  
5992      $patterns = array_map('trim', explode(',', $CFG->divertallemailsexcept));
5993      foreach ($patterns as $pattern) {
5994          if (preg_match("/$pattern/", $email)) {
5995              return false;
5996          }
5997      }
5998  
5999      return true;
6000  }
6001  
6002  /**
6003   * Generate a unique email Message-ID using the moodle domain and install path
6004   *
6005   * @param string $localpart An optional unique message id prefix.
6006   * @return string The formatted ID ready for appending to the email headers.
6007   */
6008  function generate_email_messageid($localpart = null) {
6009      global $CFG;
6010  
6011      $urlinfo = parse_url($CFG->wwwroot);
6012      $base = '@' . $urlinfo['host'];
6013  
6014      // If multiple moodles are on the same domain we want to tell them
6015      // apart so we add the install path to the local part. This means
6016      // that the id local part should never contain a / character so
6017      // we can correctly parse the id to reassemble the wwwroot.
6018      if (isset($urlinfo['path'])) {
6019          $base = $urlinfo['path'] . $base;
6020      }
6021  
6022      if (empty($localpart)) {
6023          $localpart = uniqid('', true);
6024      }
6025  
6026      // Because we may have an option /installpath suffix to the local part
6027      // of the id we need to escape any / chars which are in the $localpart.
6028      $localpart = str_replace('/', '%2F', $localpart);
6029  
6030      return '<' . $localpart . $base . '>';
6031  }
6032  
6033  /**
6034   * Send an email to a specified user
6035   *
6036   * @param stdClass $user  A {@link $USER} object
6037   * @param stdClass $from A {@link $USER} object
6038   * @param string $subject plain text subject line of the email
6039   * @param string $messagetext plain text version of the message
6040   * @param string $messagehtml complete html version of the message (optional)
6041   * @param string $attachment a file on the filesystem, either relative to $CFG->dataroot or a full path to a file in one of
6042   *          the following directories: $CFG->cachedir, $CFG->dataroot, $CFG->dirroot, $CFG->localcachedir, $CFG->tempdir
6043   * @param string $attachname the name of the file (extension indicates MIME)
6044   * @param bool $usetrueaddress determines whether $from email address should
6045   *          be sent out. Will be overruled by user profile setting for maildisplay
6046   * @param string $replyto Email address to reply to
6047   * @param string $replytoname Name of reply to recipient
6048   * @param int $wordwrapwidth custom word wrap width, default 79
6049   * @return bool Returns true if mail was sent OK and false if there was an error.
6050   */
6051  function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '', $attachment = '', $attachname = '',
6052                         $usetrueaddress = true, $replyto = '', $replytoname = '', $wordwrapwidth = 79) {
6053  
6054      global $CFG, $PAGE, $SITE;
6055  
6056      if (empty($user) or empty($user->id)) {
6057          debugging('Can not send email to null user', DEBUG_DEVELOPER);
6058          return false;
6059      }
6060  
6061      if (empty($user->email)) {
6062          debugging('Can not send email to user without email: '.$user->id, DEBUG_DEVELOPER);
6063          return false;
6064      }
6065  
6066      if (!empty($user->deleted)) {
6067          debugging('Can not send email to deleted user: '.$user->id, DEBUG_DEVELOPER);
6068          return false;
6069      }
6070  
6071      if (defined('BEHAT_SITE_RUNNING')) {
6072          // Fake email sending in behat.
6073          return true;
6074      }
6075  
6076      if (!empty($CFG->noemailever)) {
6077          // Hidden setting for development sites, set in config.php if needed.
6078          debugging('Not sending email due to $CFG->noemailever config setting', DEBUG_NORMAL);
6079          return true;
6080      }
6081  
6082      if (email_should_be_diverted($user->email)) {
6083          $subject = "[DIVERTED {$user->email}] $subject";
6084          $user = clone($user);
6085          $user->email = $CFG->divertallemailsto;
6086      }
6087  
6088      // Skip mail to suspended users.
6089      if ((isset($user->auth) && $user->auth=='nologin') or (isset($user->suspended) && $user->suspended)) {
6090          return true;
6091      }
6092  
6093      if (!validate_email($user->email)) {
6094          // We can not send emails to invalid addresses - it might create security issue or confuse the mailer.
6095          debugging("email_to_user: User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending.");
6096          return false;
6097      }
6098  
6099      if (over_bounce_threshold($user)) {
6100          debugging("email_to_user: User $user->id (".fullname($user).") is over bounce threshold! Not sending.");
6101          return false;
6102      }
6103  
6104      // TLD .invalid  is specifically reserved for invalid domain names.
6105      // For More information, see {@link http://tools.ietf.org/html/rfc2606#section-2}.
6106      if (substr($user->email, -8) == '.invalid') {
6107          debugging("email_to_user: User $user->id (".fullname($user).") email domain ($user->email) is invalid! Not sending.");
6108          return true; // This is not an error.
6109      }
6110  
6111      // If the user is a remote mnet user, parse the email text for URL to the
6112      // wwwroot and modify the url to direct the user's browser to login at their
6113      // home site (identity provider - idp) before hitting the link itself.
6114      if (is_mnet_remote_user($user)) {
6115          require_once($CFG->dirroot.'/mnet/lib.php');
6116  
6117          $jumpurl = mnet_get_idp_jump_url($user);
6118          $callback = partial('mnet_sso_apply_indirection', $jumpurl);
6119  
6120          $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%",
6121                  $callback,
6122                  $messagetext);
6123          $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%",
6124                  $callback,
6125                  $messagehtml);
6126      }
6127      $mail = get_mailer();
6128  
6129      if (!empty($mail->SMTPDebug)) {
6130          echo '<pre>' . "\n";
6131      }
6132  
6133      $temprecipients = array();
6134      $tempreplyto = array();
6135  
6136      // Make sure that we fall back onto some reasonable no-reply address.
6137      $noreplyaddressdefault = 'noreply@' . get_host_from_url($CFG->wwwroot);
6138      $noreplyaddress = empty($CFG->noreplyaddress) ? $noreplyaddressdefault : $CFG->noreplyaddress;
6139  
6140      if (!validate_email($noreplyaddress)) {
6141          debugging('email_to_user: Invalid noreply-email '.s($noreplyaddress));
6142          $noreplyaddress = $noreplyaddressdefault;
6143      }
6144  
6145      // Make up an email address for handling bounces.
6146      if (!empty($CFG->handlebounces)) {
6147          $modargs = 'B'.base64_encode(pack('V', $user->id)).substr(md5($user->email), 0, 16);
6148          $mail->Sender = generate_email_processing_address(0, $modargs);
6149      } else {
6150          $mail->Sender = $noreplyaddress;
6151      }
6152  
6153      // Make sure that the explicit replyto is valid, fall back to the implicit one.
6154      if (!empty($replyto) && !validate_email($replyto)) {
6155          debugging('email_to_user: Invalid replyto-email '.s($replyto));
6156          $replyto = $noreplyaddress;
6157      }
6158  
6159      if (is_string($from)) { // So we can pass whatever we want if there is need.
6160          $mail->From     = $noreplyaddress;
6161          $mail->FromName = $from;
6162      // Check if using the true address is true, and the email is in the list of allowed domains for sending email,
6163      // and that the senders email setting is either displayed to everyone, or display to only other users that are enrolled
6164      // in a course with the sender.
6165      } else if ($usetrueaddress && can_send_from_real_email_address($from, $user)) {
6166          if (!validate_email($from->email)) {
6167              debugging('email_to_user: Invalid from-email '.s($from->email).' - not sending');
6168              // Better not to use $noreplyaddress in this case.
6169              return false;
6170          }
6171          $mail->From = $from->email;
6172          $fromdetails = new stdClass();
6173          $fromdetails->name = fullname($from);
6174          $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
6175          $fromdetails->siteshortname = format_string($SITE->shortname);
6176          $fromstring = $fromdetails->name;
6177          if ($CFG->emailfromvia == EMAIL_VIA_ALWAYS) {
6178              $fromstring = get_string('emailvia', 'core', $fromdetails);
6179          }
6180          $mail->FromName = $fromstring;
6181          if (empty($replyto)) {
6182              $tempreplyto[] = array($from->email, fullname($from));
6183          }
6184      } else {
6185          $mail->From = $noreplyaddress;
6186          $fromdetails = new stdClass();
6187          $fromdetails->name = fullname($from);
6188          $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
6189          $fromdetails->siteshortname = format_string($SITE->shortname);
6190          $fromstring = $fromdetails->name;
6191          if ($CFG->emailfromvia != EMAIL_VIA_NEVER) {
6192              $fromstring = get_string('emailvia', 'core', $fromdetails);
6193          }
6194          $mail->FromName = $fromstring;
6195          if (empty($replyto)) {
6196              $tempreplyto[] = array($noreplyaddress, get_string('noreplyname'));
6197          }
6198      }
6199  
6200      if (!empty($replyto)) {
6201          $tempreplyto[] = array($replyto, $replytoname);
6202      }
6203  
6204      $temprecipients[] = array($user->email, fullname($user));
6205  
6206      // Set word wrap.
6207      $mail->WordWrap = $wordwrapwidth;
6208  
6209      if (!empty($from->customheaders)) {
6210          // Add custom headers.
6211          if (is_array($from->customheaders)) {
6212              foreach ($from->customheaders as $customheader) {
6213                  $mail->addCustomHeader($customheader);
6214              }
6215          } else {
6216              $mail->addCustomHeader($from->customheaders);
6217          }
6218      }
6219  
6220      // If the X-PHP-Originating-Script email header is on then also add an additional
6221      // header with details of where exactly in moodle the email was triggered from,
6222      // either a call to message_send() or to email_to_user().
6223      if (ini_get('mail.add_x_header')) {
6224  
6225          $stack = debug_backtrace(false);
6226          $origin = $stack[0];
6227  
6228          foreach ($stack as $depth => $call) {
6229              if ($call['function'] == 'message_send') {
6230                  $origin = $call;
6231              }
6232          }
6233  
6234          $originheader = $CFG->wwwroot . ' => ' . gethostname() . ':'
6235               . str_replace($CFG->dirroot . '/', '', $origin['file']) . ':' . $origin['line'];
6236          $mail->addCustomHeader('X-Moodle-Originating-Script: ' . $originheader);
6237      }
6238  
6239      if (!empty($from->priority)) {
6240          $mail->Priority = $from->priority;
6241      }
6242  
6243      $renderer = $PAGE->get_renderer('core');
6244      $context = array(
6245          'sitefullname' => $SITE->fullname,
6246          'siteshortname' => $SITE->shortname,
6247          'sitewwwroot' => $CFG->wwwroot,
6248          'subject' => $subject,
6249          'prefix' => $CFG->emailsubjectprefix,
6250          'to' => $user->email,
6251          'toname' => fullname($user),
6252          'from' => $mail->From,
6253          'fromname' => $mail->FromName,
6254      );
6255      if (!empty($tempreplyto[0])) {
6256          $context['replyto'] = $tempreplyto[0][0];
6257          $context['replytoname'] = $tempreplyto[0][1];
6258      }
6259      if ($user->id > 0) {
6260          $context['touserid'] = $user->id;
6261          $context['tousername'] = $user->username;
6262      }
6263  
6264      if (!empty($user->mailformat) && $user->mailformat == 1) {
6265          // Only process html templates if the user preferences allow html email.
6266  
6267          if (!$messagehtml) {
6268              // If no html has been given, BUT there is an html wrapping template then
6269              // auto convert the text to html and then wrap it.
6270              $messagehtml = trim(text_to_html($messagetext));
6271          }
6272          $context['body'] = $messagehtml;
6273          $messagehtml = $renderer->render_from_template('core/email_html', $context);
6274      }
6275  
6276      $context['body'] = html_to_text(nl2br($messagetext));
6277      $mail->Subject = $renderer->render_from_template('core/email_subject', $context);
6278      $mail->FromName = $renderer->render_from_template('core/email_fromname', $context);
6279      $messagetext = $renderer->render_from_template('core/email_text', $context);
6280  
6281      // Autogenerate a MessageID if it's missing.
6282      if (empty($mail->MessageID)) {
6283          $mail->MessageID = generate_email_messageid();
6284      }
6285  
6286      if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) {
6287          // Don't ever send HTML to users who don't want it.
6288          $mail->isHTML(true);
6289          $mail->Encoding = 'quoted-printable';
6290          $mail->Body    =  $messagehtml;
6291          $mail->AltBody =  "\n$messagetext\n";
6292      } else {
6293          $mail->IsHTML(false);
6294          $mail->Body =  "\n$messagetext\n";
6295      }
6296  
6297      if ($attachment && $attachname) {
6298          if (preg_match( "~\\.\\.~" , $attachment )) {
6299              // Security check for ".." in dir path.
6300              $supportuser = core_user::get_support_user();
6301              $temprecipients[] = array($supportuser->email, fullname($supportuser, true));
6302              $mail->addStringAttachment('Error in attachment.  User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain');
6303          } else {
6304              require_once($CFG->libdir.'/filelib.php');
6305              $mimetype = mimeinfo('type', $attachname);
6306  
6307              $attachmentpath = $attachment;
6308  
6309              // Before doing the comparison, make sure that the paths are correct (Windows uses slashes in the other direction).
6310              $attachpath = str_replace('\\', '/', $attachmentpath);
6311  
6312              // Add allowed paths to an array (also check if it's not empty).
6313              $allowedpaths = array_filter([
6314                  $CFG->cachedir,
6315                  $CFG->dataroot,
6316                  $CFG->dirroot,
6317                  $CFG->localcachedir,
6318                  $CFG->tempdir
6319              ]);
6320              // Set addpath to true.
6321              $addpath = true;
6322              // Check if attachment includes one of the allowed paths.
6323              foreach ($allowedpaths as $tmpvar) {
6324                  // Make sure both variables are normalised before comparing.
6325                  $temppath = str_replace('\\', '/', realpath($tmpvar));
6326                  // Set addpath to false if the attachment includes one of the allowed paths.
6327                  if (strpos($attachpath, $temppath) === 0) {
6328                      $addpath = false;
6329                      break;
6330                  }
6331              }
6332  
6333              // If the attachment is a full path to a file in the multiple allowed paths, use it as is,
6334              // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons).
6335              if ($addpath == true) {
6336                  $attachmentpath = $CFG->dataroot . '/' . $attachmentpath;
6337              }
6338  
6339              $mail->addAttachment($attachmentpath, $attachname, 'base64', $mimetype);
6340          }
6341      }
6342  
6343      // Check if the email should be sent in an other charset then the default UTF-8.
6344      if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) {
6345  
6346          // Use the defined site mail charset or eventually the one preferred by the recipient.
6347          $charset = $CFG->sitemailcharset;
6348          if (!empty($CFG->allowusermailcharset)) {
6349              if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) {
6350                  $charset = $useremailcharset;
6351              }
6352          }
6353  
6354          // Convert all the necessary strings if the charset is supported.
6355          $charsets = get_list_of_charsets();
6356          unset($charsets['UTF-8']);
6357          if (in_array($charset, $charsets)) {
6358              $mail->CharSet  = $charset;
6359              $mail->FromName = core_text::convert($mail->FromName, 'utf-8', strtolower($charset));
6360              $mail->Subject  = core_text::convert($mail->Subject, 'utf-8', strtolower($charset));
6361              $mail->Body     = core_text::convert($mail->Body, 'utf-8', strtolower($charset));
6362              $mail->AltBody  = core_text::convert($mail->AltBody, 'utf-8', strtolower($charset));
6363  
6364              foreach ($temprecipients as $key => $values) {
6365                  $temprecipients[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
6366              }
6367              foreach ($tempreplyto as $key => $values) {
6368                  $tempreplyto[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
6369              }
6370          }
6371      }
6372  
6373      foreach ($temprecipients as $values) {
6374          $mail->addAddress($values[0], $values[1]);
6375      }
6376      foreach ($tempreplyto as $values) {
6377          $mail->addReplyTo($values[0], $values[1]);
6378      }
6379  
6380      if ($mail->send()) {
6381          set_send_count($user);
6382          if (!empty($mail->SMTPDebug)) {
6383              echo '</pre>';
6384          }
6385          return true;
6386      } else {
6387          // Trigger event for failing to send email.
6388          $event = \core\event\email_failed::create(array(
6389              'context' => context_system::instance(),
6390              'userid' => $from->id,
6391              'relateduserid' => $user->id,
6392              'other' => array(
6393                  'subject' => $subject,
6394                  'message' => $messagetext,
6395                  'errorinfo' => $mail->ErrorInfo
6396              )
6397          ));
6398          $event->trigger();
6399          if (CLI_SCRIPT) {
6400              mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo);
6401          }
6402          if (!empty($mail->SMTPDebug)) {
6403              echo '</pre>';
6404          }
6405          return false;
6406      }
6407  }
6408  
6409  /**
6410   * Check to see if a user's real email address should be used for the "From" field.
6411   *
6412   * @param  object $from The user object for the user we are sending the email from.
6413   * @param  object $user The user object that we are sending the email to.
6414   * @param  array $unused No longer used.
6415   * @return bool Returns true if we can use the from user's email adress in the "From" field.
6416   */
6417  function can_send_from_real_email_address($from, $user, $unused = null) {
6418      global $CFG;
6419      if (!isset($CFG->allowedemaildomains) || empty(trim($CFG->allowedemaildomains))) {
6420          return false;
6421      }
6422      $alloweddomains = array_map('trim', explode("\n", $CFG->allowedemaildomains));
6423      // Email is in the list of allowed domains for sending email,
6424      // and the senders email setting is either displayed to everyone, or display to only other users that are enrolled
6425      // in a course with the sender.
6426      if (\core\ip_utils::is_domain_in_allowed_list(substr($from->email, strpos($from->email, '@') + 1), $alloweddomains)
6427                  && ($from->maildisplay == core_user::MAILDISPLAY_EVERYONE
6428                  || ($from->maildisplay == core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY
6429                  && enrol_get_shared_courses($user, $from, false, true)))) {
6430          return true;
6431      }
6432      return false;
6433  }
6434  
6435  /**
6436   * Generate a signoff for emails based on support settings
6437   *
6438   * @return string
6439   */
6440  function generate_email_signoff() {
6441      global $CFG;
6442  
6443      $signoff = "\n";
6444      if (!empty($CFG->supportname)) {
6445          $signoff .= $CFG->supportname."\n";
6446      }
6447      if (!empty($CFG->supportemail)) {
6448          $signoff .= $CFG->supportemail."\n";
6449      }
6450      if (!empty($CFG->supportpage)) {
6451          $signoff .= $CFG->supportpage."\n";
6452      }
6453      return $signoff;
6454  }
6455  
6456  /**
6457   * Sets specified user's password and send the new password to the user via email.
6458   *
6459   * @param stdClass $user A {@link $USER} object
6460   * @param bool $fasthash If true, use a low cost factor when generating the hash for speed.
6461   * @return bool|string Returns "true" if mail was sent OK and "false" if there was an error
6462   */
6463  function setnew_password_and_mail($user, $fasthash = false) {
6464      global $CFG, $DB;
6465  
6466      // We try to send the mail in language the user understands,
6467      // unfortunately the filter_string() does not support alternative langs yet
6468      // so multilang will not work properly for site->fullname.
6469      $lang = empty($user->lang) ? $CFG->lang : $user->lang;
6470  
6471      $site  = get_site();
6472  
6473      $supportuser = core_user::get_support_user();
6474  
6475      $newpassword = generate_password();
6476  
6477      update_internal_user_password($user, $newpassword, $fasthash);
6478  
6479      $a = new stdClass();
6480      $a->firstname   = fullname($user, true);
6481      $a->sitename    = format_string($site->fullname);
6482      $a->username    = $user->username;
6483      $a->newpassword = $newpassword;
6484      $a->link        = $CFG->wwwroot .'/login/?lang='.$lang;
6485      $a->signoff     = generate_email_signoff();
6486  
6487      $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang);
6488  
6489      $subject = format_string($site->fullname) .': '. (string)new lang_string('newusernewpasswordsubj', '', $a, $lang);
6490  
6491      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6492      return email_to_user($user, $supportuser, $subject, $message);
6493  
6494  }
6495  
6496  /**
6497   * Resets specified user's password and send the new password to the user via email.
6498   *
6499   * @param stdClass $user A {@link $USER} object
6500   * @return bool Returns true if mail was sent OK and false if there was an error.
6501   */
6502  function reset_password_and_mail($user) {
6503      global $CFG;
6504  
6505      $site  = get_site();
6506      $supportuser = core_user::get_support_user();
6507  
6508      $userauth = get_auth_plugin($user->auth);
6509      if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) {
6510          trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth.");
6511          return false;
6512      }
6513  
6514      $newpassword = generate_password();
6515  
6516      if (!$userauth->user_update_password($user, $newpassword)) {
6517          print_error("cannotsetpassword");
6518      }
6519  
6520      $a = new stdClass();
6521      $a->firstname   = $user->firstname;
6522      $a->lastname    = $user->lastname;
6523      $a->sitename    = format_string($site->fullname);
6524      $a->username    = $user->username;
6525      $a->newpassword = $newpassword;
6526      $a->link        = $CFG->wwwroot .'/login/change_password.php';
6527      $a->signoff     = generate_email_signoff();
6528  
6529      $message = get_string('newpasswordtext', '', $a);
6530  
6531      $subject  = format_string($site->fullname) .': '. get_string('changedpassword');
6532  
6533      unset_user_preference('create_password', $user); // Prevent cron from generating the password.
6534  
6535      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6536      return email_to_user($user, $supportuser, $subject, $message);
6537  }
6538  
6539  /**
6540   * Send email to specified user with confirmation text and activation link.
6541   *
6542   * @param stdClass $user A {@link $USER} object
6543   * @param string $confirmationurl user confirmation URL
6544   * @return bool Returns true if mail was sent OK and false if there was an error.
6545   */
6546  function send_confirmation_email($user, $confirmationurl = null) {
6547      global $CFG;
6548  
6549      $site = get_site();
6550      $supportuser = core_user::get_support_user();
6551  
6552      $data = new stdClass();
6553      $data->sitename  = format_string($site->fullname);
6554      $data->admin     = generate_email_signoff();
6555  
6556      $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname));
6557  
6558      if (empty($confirmationurl)) {
6559          $confirmationurl = '/login/confirm.php';
6560      }
6561  
6562      $confirmationurl = new moodle_url($confirmationurl);
6563      // Remove data parameter just in case it was included in the confirmation so we can add it manually later.
6564      $confirmationurl->remove_params('data');
6565      $confirmationpath = $confirmationurl->out(false);
6566  
6567      // We need to custom encode the username to include trailing dots in the link.
6568      // Because of this custom encoding we can't use moodle_url directly.
6569      // Determine if a query string is present in the confirmation url.
6570      $hasquerystring = strpos($confirmationpath, '?') !== false;
6571      // Perform normal url encoding of the username first.
6572      $username = urlencode($user->username);
6573      // Prevent problems with trailing dots not being included as part of link in some mail clients.
6574      $username = str_replace('.', '%2E', $username);
6575  
6576      $data->link = $confirmationpath . ( $hasquerystring ? '&' : '?') . 'data='. $user->secret .'/'. $username;
6577  
6578      $message     = get_string('emailconfirmation', '', $data);
6579      $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true);
6580  
6581      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6582      return email_to_user($user, $supportuser, $subject, $message, $messagehtml);
6583  }
6584  
6585  /**
6586   * Sends a password change confirmation email.
6587   *
6588   * @param stdClass $user A {@link $USER} object
6589   * @param stdClass $resetrecord An object tracking metadata regarding password reset request
6590   * @return bool Returns true if mail was sent OK and false if there was an error.
6591   */
6592  function send_password_change_confirmation_email($user, $resetrecord) {
6593      global $CFG;
6594  
6595      $site = get_site();
6596      $supportuser = core_user::get_support_user();
6597      $pwresetmins = isset($CFG->pwresettime) ? floor($CFG->pwresettime / MINSECS) : 30;
6598  
6599      $data = new stdClass();
6600      $data->firstname = $user->firstname;
6601      $data->lastname  = $user->lastname;
6602      $data->username  = $user->username;
6603      $data->sitename  = format_string($site->fullname);
6604      $data->link      = $CFG->wwwroot .'/login/forgot_password.php?token='. $resetrecord->token;
6605      $data->admin     = generate_email_signoff();
6606      $data->resetminutes = $pwresetmins;
6607  
6608      $message = get_string('emailresetconfirmation', '', $data);
6609      $subject = get_string('emailresetconfirmationsubject', '', format_string($site->fullname));
6610  
6611      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6612      return email_to_user($user, $supportuser, $subject, $message);
6613  
6614  }
6615  
6616  /**
6617   * Sends an email containing information on how to change your password.
6618   *
6619   * @param stdClass $user A {@link $USER} object
6620   * @return bool Returns true if mail was sent OK and false if there was an error.
6621   */
6622  function send_password_change_info($user) {
6623      $site = get_site();
6624      $supportuser = core_user::get_support_user();
6625  
6626      $data = new stdClass();
6627      $data->firstname = $user->firstname;
6628      $data->lastname  = $user->lastname;
6629      $data->username  = $user->username;
6630      $data->sitename  = format_string($site->fullname);
6631      $data->admin     = generate_email_signoff();
6632  
6633      if (!is_enabled_auth($user->auth)) {
6634          $message = get_string('emailpasswordchangeinfodisabled', '', $data);
6635          $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
6636          // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6637          return email_to_user($user, $supportuser, $subject, $message);
6638      }
6639  
6640      $userauth = get_auth_plugin($user->auth);
6641      ['subject' => $subject, 'message' => $message] = $userauth->get_password_change_info($user);
6642  
6643      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6644      return email_to_user($user, $supportuser, $subject, $message);
6645  }
6646  
6647  /**
6648   * Check that an email is allowed.  It returns an error message if there was a problem.
6649   *
6650   * @param string $email Content of email
6651   * @return string|false
6652   */
6653  function email_is_not_allowed($email) {
6654      global $CFG;
6655  
6656      // Comparing lowercase domains.
6657      $email = strtolower($email);
6658      if (!empty($CFG->allowemailaddresses)) {
6659          $allowed = explode(' ', strtolower($CFG->allowemailaddresses));
6660          foreach ($allowed as $allowedpattern) {
6661              $allowedpattern = trim($allowedpattern);
6662              if (!$allowedpattern) {
6663                  continue;
6664              }
6665              if (strpos($allowedpattern, '.') === 0) {
6666                  if (strpos(strrev($email), strrev($allowedpattern)) === 0) {
6667                      // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6668                      return false;
6669                  }
6670  
6671              } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) {
6672                  return false;
6673              }
6674          }
6675          return get_string('emailonlyallowed', '', $CFG->allowemailaddresses);
6676  
6677      } else if (!empty($CFG->denyemailaddresses)) {
6678          $denied = explode(' ', strtolower($CFG->denyemailaddresses));
6679          foreach ($denied as $deniedpattern) {
6680              $deniedpattern = trim($deniedpattern);
6681              if (!$deniedpattern) {
6682                  continue;
6683              }
6684              if (strpos($deniedpattern, '.') === 0) {
6685                  if (strpos(strrev($email), strrev($deniedpattern)) === 0) {
6686                      // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6687                      return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6688                  }
6689  
6690              } else if (strpos(strrev($email), strrev('@'.$deniedpattern)) === 0) {
6691                  return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6692              }
6693          }
6694      }
6695  
6696      return false;
6697  }
6698  
6699  // FILE HANDLING.
6700  
6701  /**
6702   * Returns local file storage instance
6703   *
6704   * @return file_storage
6705   */
6706  function get_file_storage($reset = false) {
6707      global $CFG;
6708  
6709      static $fs = null;
6710  
6711      if ($reset) {
6712          $fs = null;
6713          return;
6714      }
6715  
6716      if ($fs) {
6717          return $fs;
6718      }
6719  
6720      require_once("$CFG->libdir/filelib.php");
6721  
6722      $fs = new file_storage();
6723  
6724      return $fs;
6725  }
6726  
6727  /**
6728   * Returns local file storage instance
6729   *
6730   * @return file_browser
6731   */
6732  function get_file_browser() {
6733      global $CFG;
6734  
6735      static $fb = null;
6736  
6737      if ($fb) {
6738          return $fb;
6739      }
6740  
6741      require_once("$CFG->libdir/filelib.php");
6742  
6743      $fb = new file_browser();
6744  
6745      return $fb;
6746  }
6747  
6748  /**
6749   * Returns file packer
6750   *
6751   * @param string $mimetype default application/zip
6752   * @return file_packer
6753   */
6754  function get_file_packer($mimetype='application/zip') {
6755      global $CFG;
6756  
6757      static $fp = array();
6758  
6759      if (isset($fp[$mimetype])) {
6760          return $fp[$mimetype];
6761      }
6762  
6763      switch ($mimetype) {
6764          case 'application/zip':
6765          case 'application/vnd.moodle.profiling':
6766              $classname = 'zip_packer';
6767              break;
6768  
6769          case 'application/x-gzip' :
6770              $classname = 'tgz_packer';
6771              break;
6772  
6773          case 'application/vnd.moodle.backup':
6774              $classname = 'mbz_packer';
6775              break;
6776  
6777          default:
6778              return false;
6779      }
6780  
6781      require_once("$CFG->libdir/filestorage/$classname.php");
6782      $fp[$mimetype] = new $classname();
6783  
6784      return $fp[$mimetype];
6785  }
6786  
6787  /**
6788   * Returns current name of file on disk if it exists.
6789   *
6790   * @param string $newfile File to be verified
6791   * @return string Current name of file on disk if true
6792   */
6793  function valid_uploaded_file($newfile) {
6794      if (empty($newfile)) {
6795          return '';
6796      }
6797      if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) {
6798          return $newfile['tmp_name'];
6799      } else {
6800          return '';
6801      }
6802  }
6803  
6804  /**
6805   * Returns the maximum size for uploading files.
6806   *
6807   * There are seven possible upload limits:
6808   * 1. in Apache using LimitRequestBody (no way of checking or changing this)
6809   * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
6810   * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
6811   * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
6812   * 5. by the Moodle admin in $CFG->maxbytes
6813   * 6. by the teacher in the current course $course->maxbytes
6814   * 7. by the teacher for the current module, eg $assignment->maxbytes
6815   *
6816   * These last two are passed to this function as arguments (in bytes).
6817   * Anything defined as 0 is ignored.
6818   * The smallest of all the non-zero numbers is returned.
6819   *
6820   * @todo Finish documenting this function
6821   *
6822   * @param int $sitebytes Set maximum size
6823   * @param int $coursebytes Current course $course->maxbytes (in bytes)
6824   * @param int $modulebytes Current module ->maxbytes (in bytes)
6825   * @param bool $unused This parameter has been deprecated and is not used any more.
6826   * @return int The maximum size for uploading files.
6827   */
6828  function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0, $unused = false) {
6829  
6830      if (! $filesize = ini_get('upload_max_filesize')) {
6831          $filesize = '5M';
6832      }
6833      $minimumsize = get_real_size($filesize);
6834  
6835      if ($postsize = ini_get('post_max_size')) {
6836          $postsize = get_real_size($postsize);
6837          if ($postsize < $minimumsize) {
6838              $minimumsize = $postsize;
6839          }
6840      }
6841  
6842      if (($sitebytes > 0) and ($sitebytes < $minimumsize)) {
6843          $minimumsize = $sitebytes;
6844      }
6845  
6846      if (($coursebytes > 0) and ($coursebytes < $minimumsize)) {
6847          $minimumsize = $coursebytes;
6848      }
6849  
6850      if (($modulebytes > 0) and ($modulebytes < $minimumsize)) {
6851          $minimumsize = $modulebytes;
6852      }
6853  
6854      return $minimumsize;
6855  }
6856  
6857  /**
6858   * Returns the maximum size for uploading files for the current user
6859   *
6860   * This function takes in account {@link get_max_upload_file_size()} the user's capabilities
6861   *
6862   * @param context $context The context in which to check user capabilities
6863   * @param int $sitebytes Set maximum size
6864   * @param int $coursebytes Current course $course->maxbytes (in bytes)
6865   * @param int $modulebytes Current module ->maxbytes (in bytes)
6866   * @param stdClass $user The user
6867   * @param bool $unused This parameter has been deprecated and is not used any more.
6868   * @return int The maximum size for uploading files.
6869   */
6870  function get_user_max_upload_file_size($context, $sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $user = null,
6871          $unused = false) {
6872      global $USER;
6873  
6874      if (empty($user)) {
6875          $user = $USER;
6876      }
6877  
6878      if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) {
6879          return USER_CAN_IGNORE_FILE_SIZE_LIMITS;
6880      }
6881  
6882      return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes);
6883  }
6884  
6885  /**
6886   * Returns an array of possible sizes in local language
6887   *
6888   * Related to {@link get_max_upload_file_size()} - this function returns an
6889   * array of possible sizes in an array, translated to the
6890   * local language.
6891   *
6892   * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes.
6893   *
6894   * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)"
6895   * with the value set to 0. This option will be the first in the list.
6896   *
6897   * @uses SORT_NUMERIC
6898   * @param int $sitebytes Set maximum size
6899   * @param int $coursebytes Current course $course->maxbytes (in bytes)
6900   * @param int $modulebytes Current module ->maxbytes (in bytes)
6901   * @param int|array $custombytes custom upload size/s which will be added to list,
6902   *        Only value/s smaller then maxsize will be added to list.
6903   * @return array
6904   */
6905  function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null) {
6906      global $CFG;
6907  
6908      if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) {
6909          return array();
6910      }
6911  
6912      if ($sitebytes == 0) {
6913          // Will get the minimum of upload_max_filesize or post_max_size.
6914          $sitebytes = get_max_upload_file_size();
6915      }
6916  
6917      $filesize = array();
6918      $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152,
6919                        5242880, 10485760, 20971520, 52428800, 104857600,
6920                        262144000, 524288000, 786432000, 1073741824,
6921                        2147483648, 4294967296, 8589934592);
6922  
6923      // If custombytes is given and is valid then add it to the list.
6924      if (is_number($custombytes) and $custombytes > 0) {
6925          $custombytes = (int)$custombytes;
6926          if (!in_array($custombytes, $sizelist)) {
6927              $sizelist[] = $custombytes;
6928          }
6929      } else if (is_array($custombytes)) {
6930          $sizelist = array_unique(array_merge($sizelist, $custombytes));
6931      }
6932  
6933      // Allow maxbytes to be selected if it falls outside the above boundaries.
6934      if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) {
6935          // Note: get_real_size() is used in order to prevent problems with invalid values.
6936          $sizelist[] = get_real_size($CFG->maxbytes);
6937      }
6938  
6939      foreach ($sizelist as $sizebytes) {
6940          if ($sizebytes < $maxsize && $sizebytes > 0) {
6941              $filesize[(string)intval($sizebytes)] = display_size($sizebytes);
6942          }
6943      }
6944  
6945      $limitlevel = '';
6946      $displaysize = '';
6947      if ($modulebytes &&
6948          (($modulebytes < $coursebytes || $coursebytes == 0) &&
6949           ($modulebytes < $sitebytes || $sitebytes == 0))) {
6950          $limitlevel = get_string('activity', 'core');
6951          $displaysize = display_size($modulebytes);
6952          $filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list.
6953  
6954      } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) {
6955          $limitlevel = get_string('course', 'core');
6956          $displaysize = display_size($coursebytes);
6957          $filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list.
6958  
6959      } else if ($sitebytes) {
6960          $limitlevel = get_string('site', 'core');
6961          $displaysize = display_size($sitebytes);
6962          $filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list.
6963      }
6964  
6965      krsort($filesize, SORT_NUMERIC);
6966      if ($limitlevel) {
6967          $params = (object) array('contextname' => $limitlevel, 'displaysize' => $displaysize);
6968          $filesize  = array('0' => get_string('uploadlimitwithsize', 'core', $params)) + $filesize;
6969      }
6970  
6971      return $filesize;
6972  }
6973  
6974  /**
6975   * Returns an array with all the filenames in all subdirectories, relative to the given rootdir.
6976   *
6977   * If excludefiles is defined, then that file/directory is ignored
6978   * If getdirs is true, then (sub)directories are included in the output
6979   * If getfiles is true, then files are included in the output
6980   * (at least one of these must be true!)
6981   *
6982   * @todo Finish documenting this function. Add examples of $excludefile usage.
6983   *
6984   * @param string $rootdir A given root directory to start from
6985   * @param string|array $excludefiles If defined then the specified file/directory is ignored
6986   * @param bool $descend If true then subdirectories are recursed as well
6987   * @param bool $getdirs If true then (sub)directories are included in the output
6988   * @param bool $getfiles  If true then files are included in the output
6989   * @return array An array with all the filenames in all subdirectories, relative to the given rootdir
6990   */
6991  function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) {
6992  
6993      $dirs = array();
6994  
6995      if (!$getdirs and !$getfiles) {   // Nothing to show.
6996          return $dirs;
6997      }
6998  
6999      if (!is_dir($rootdir)) {          // Must be a directory.
7000          return $dirs;
7001      }
7002  
7003      if (!$dir = opendir($rootdir)) {  // Can't open it for some reason.
7004          return $dirs;
7005      }
7006  
7007      if (!is_array($excludefiles)) {
7008          $excludefiles = array($excludefiles);
7009      }
7010  
7011      while (false !== ($file = readdir($dir))) {
7012          $firstchar = substr($file, 0, 1);
7013          if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) {
7014              continue;
7015          }
7016          $fullfile = $rootdir .'/'. $file;
7017          if (filetype($fullfile) == 'dir') {
7018              if ($getdirs) {
7019                  $dirs[] = $file;
7020              }
7021              if ($descend) {
7022                  $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles);
7023                  foreach ($subdirs as $subdir) {
7024                      $dirs[] = $file .'/'. $subdir;
7025                  }
7026              }
7027          } else if ($getfiles) {
7028              $dirs[] = $file;
7029          }
7030      }
7031      closedir($dir);
7032  
7033      asort($dirs);
7034  
7035      return $dirs;
7036  }
7037  
7038  
7039  /**
7040   * Adds up all the files in a directory and works out the size.
7041   *
7042   * @param string $rootdir  The directory to start from
7043   * @param string $excludefile A file to exclude when summing directory size
7044   * @return int The summed size of all files and subfiles within the root directory
7045   */
7046  function get_directory_size($rootdir, $excludefile='') {
7047      global $CFG;
7048  
7049      // Do it this way if we can, it's much faster.
7050      if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) {
7051          $command = trim($CFG->pathtodu).' -sk '.escapeshellarg($rootdir);
7052          $output = null;
7053          $return = null;
7054          exec($command, $output, $return);
7055          if (is_array($output)) {
7056              // We told it to return k.
7057              return get_real_size(intval($output[0]).'k');
7058          }
7059      }
7060  
7061      if (!is_dir($rootdir)) {
7062          // Must be a directory.
7063          return 0;
7064      }
7065  
7066      if (!$dir = @opendir($rootdir)) {
7067          // Can't open it for some reason.
7068          return 0;
7069      }
7070  
7071      $size = 0;
7072  
7073      while (false !== ($file = readdir($dir))) {
7074          $firstchar = substr($file, 0, 1);
7075          if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) {
7076              continue;
7077          }
7078          $fullfile = $rootdir .'/'. $file;
7079          if (filetype($fullfile) == 'dir') {
7080              $size += get_directory_size($fullfile, $excludefile);
7081          } else {
7082              $size += filesize($fullfile);
7083          }
7084      }
7085      closedir($dir);
7086  
7087      return $size;
7088  }
7089  
7090  /**
7091   * Converts bytes into display form
7092   *
7093   * @static string $gb Localized string for size in gigabytes
7094   * @static string $mb Localized string for size in megabytes
7095   * @static string $kb Localized string for size in kilobytes
7096   * @static string $b Localized string for size in bytes
7097   * @param int $size  The size to convert to human readable form
7098   * @return string
7099   */
7100  function display_size($size) {
7101  
7102      static $gb, $mb, $kb, $b;
7103  
7104      if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) {
7105          return get_string('unlimited');
7106      }
7107  
7108      if (empty($gb)) {
7109          $gb = get_string('sizegb');
7110          $mb = get_string('sizemb');
7111          $kb = get_string('sizekb');
7112          $b  = get_string('sizeb');
7113      }
7114  
7115      if ($size >= 1073741824) {
7116          $size = round($size / 1073741824 * 10) / 10 . $gb;
7117      } else if ($size >= 1048576) {
7118          $size = round($size / 1048576 * 10) / 10 . $mb;
7119      } else if ($size >= 1024) {
7120          $size = round($size / 1024 * 10) / 10 . $kb;
7121      } else {
7122          $size = intval($size) .' '. $b; // File sizes over 2GB can not work in 32bit PHP anyway.
7123      }
7124      return $size;
7125  }
7126  
7127  /**
7128   * Cleans a given filename by removing suspicious or troublesome characters
7129   *
7130   * @see clean_param()
7131   * @param string $string file name
7132   * @return string cleaned file name
7133   */
7134  function clean_filename($string) {
7135      return clean_param($string, PARAM_FILE);
7136  }
7137  
7138  // STRING TRANSLATION.
7139  
7140  /**
7141   * Returns the code for the current language
7142   *
7143   * @category string
7144   * @return string
7145   */
7146  function current_language() {
7147      global $CFG, $USER, $SESSION, $COURSE;
7148  
7149      if (!empty($SESSION->forcelang)) {
7150          // Allows overriding course-forced language (useful for admins to check
7151          // issues in courses whose language they don't understand).
7152          // Also used by some code to temporarily get language-related information in a
7153          // specific language (see force_current_language()).
7154          $return = $SESSION->forcelang;
7155  
7156      } else if (!empty($COURSE->id) and $COURSE->id != SITEID and !empty($COURSE->lang)) {
7157          // Course language can override all other settings for this page.
7158          $return = $COURSE->lang;
7159  
7160      } else if (!empty($SESSION->lang)) {
7161          // Session language can override other settings.
7162          $return = $SESSION->lang;
7163  
7164      } else if (!empty($USER->lang)) {
7165          $return = $USER->lang;
7166  
7167      } else if (isset($CFG->lang)) {
7168          $return = $CFG->lang;
7169  
7170      } else {
7171          $return = 'en';
7172      }
7173  
7174      // Just in case this slipped in from somewhere by accident.
7175      $return = str_replace('_utf8', '', $return);
7176  
7177      return $return;
7178  }
7179  
7180  /**
7181   * Returns parent language of current active language if defined
7182   *
7183   * @category string
7184   * @param string $lang null means current language
7185   * @return string
7186   */
7187  function get_parent_language($lang=null) {
7188  
7189      $parentlang = get_string_manager()->get_string('parentlanguage', 'langconfig', null, $lang);
7190  
7191      if ($parentlang === 'en') {
7192          $parentlang = '';
7193      }
7194  
7195      return $parentlang;
7196  }
7197  
7198  /**
7199   * Force the current language to get strings and dates localised in the given language.
7200   *
7201   * After calling this function, all strings will be provided in the given language
7202   * until this function is called again, or equivalent code is run.
7203   *
7204   * @param string $language
7205   * @return string previous $SESSION->forcelang value
7206   */
7207  function force_current_language($language) {
7208      global $SESSION;
7209      $sessionforcelang = isset($SESSION->forcelang) ? $SESSION->forcelang : '';
7210      if ($language !== $sessionforcelang) {
7211          // Seting forcelang to null or an empty string disables it's effect.
7212          if (empty($language) || get_string_manager()->translation_exists($language, false)) {
7213              $SESSION->forcelang = $language;
7214              moodle_setlocale();
7215          }
7216      }
7217      return $sessionforcelang;
7218  }
7219  
7220  /**
7221   * Returns current string_manager instance.
7222   *
7223   * The param $forcereload is needed for CLI installer only where the string_manager instance
7224   * must be replaced during the install.php script life time.
7225   *
7226   * @category string
7227   * @param bool $forcereload shall the singleton be released and new instance created instead?
7228   * @return core_string_manager
7229   */
7230  function get_string_manager($forcereload=false) {
7231      global $CFG;
7232  
7233      static $singleton = null;
7234  
7235      if ($forcereload) {
7236          $singleton = null;
7237      }
7238      if ($singleton === null) {
7239          if (empty($CFG->early_install_lang)) {
7240  
7241              $transaliases = array();
7242              if (empty($CFG->langlist)) {
7243                   $translist = array();
7244              } else {
7245                  $translist = explode(',', $CFG->langlist);
7246                  $translist = array_map('trim', $translist);
7247                  // Each language in the $CFG->langlist can has an "alias" that would substitute the default language name.
7248                  foreach ($translist as $i => $value) {
7249                      $parts = preg_split('/\s*\|\s*/', $value, 2);
7250                      if (count($parts) == 2) {
7251                          $transaliases[$parts[0]] = $parts[1];
7252                          $translist[$i] = $parts[0];
7253                      }
7254                  }
7255              }
7256  
7257              if (!empty($CFG->config_php_settings['customstringmanager'])) {
7258                  $classname = $CFG->config_php_settings['customstringmanager'];
7259  
7260                  if (class_exists($classname)) {
7261                      $implements = class_implements($classname);
7262  
7263                      if (isset($implements['core_string_manager'])) {
7264                          $singleton = new $classname($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
7265                          return $singleton;
7266  
7267                      } else {
7268                          debugging('Unable to instantiate custom string manager: class '.$classname.
7269                              ' does not implement the core_string_manager interface.');
7270                      }
7271  
7272                  } else {
7273                      debugging('Unable to instantiate custom string manager: class '.$classname.' can not be found.');
7274                  }
7275              }
7276  
7277              $singleton = new core_string_manager_standard($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
7278  
7279          } else {
7280              $singleton = new core_string_manager_install();
7281          }
7282      }
7283  
7284      return $singleton;
7285  }
7286  
7287  /**
7288   * Returns a localized string.
7289   *
7290   * Returns the translated string specified by $identifier as
7291   * for $module.  Uses the same format files as STphp.
7292   * $a is an object, string or number that can be used
7293   * within translation strings
7294   *
7295   * eg 'hello {$a->firstname} {$a->lastname}'
7296   * or 'hello {$a}'
7297   *
7298   * If you would like to directly echo the localized string use
7299   * the function {@link print_string()}
7300   *
7301   * Example usage of this function involves finding the string you would
7302   * like a local equivalent of and using its identifier and module information
7303   * to retrieve it.<br/>
7304   * If you open moodle/lang/en/moodle.php and look near line 278
7305   * you will find a string to prompt a user for their word for 'course'
7306   * <code>
7307   * $string['course'] = 'Course';
7308   * </code>
7309   * So if you want to display the string 'Course'
7310   * in any language that supports it on your site
7311   * you just need to use the identifier 'course'
7312   * <code>
7313   * $mystring = '<strong>'. get_string('course') .'</strong>';
7314   * or
7315   * </code>
7316   * If the string you want is in another file you'd take a slightly
7317   * different approach. Looking in moodle/lang/en/calendar.php you find
7318   * around line 75:
7319   * <code>
7320   * $string['typecourse'] = 'Course event';
7321   * </code>
7322   * If you want to display the string "Course event" in any language
7323   * supported you would use the identifier 'typecourse' and the module 'calendar'
7324   * (because it is in the file calendar.php):
7325   * <code>
7326   * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>';
7327   * </code>
7328   *
7329   * As a last resort, should the identifier fail to map to a string
7330   * the returned string will be [[ $identifier ]]
7331   *
7332   * In Moodle 2.3 there is a new argument to this function $lazyload.
7333   * Setting $lazyload to true causes get_string to return a lang_string object
7334   * rather than the string itself. The fetching of the string is then put off until
7335   * the string object is first used. The object can be used by calling it's out
7336   * method or by casting the object to a string, either directly e.g.
7337   *     (string)$stringobject
7338   * or indirectly by using the string within another string or echoing it out e.g.
7339   *     echo $stringobject
7340   *     return "<p>{$stringobject}</p>";
7341   * It is worth noting that using $lazyload and attempting to use the string as an
7342   * array key will cause a fatal error as objects cannot be used as array keys.
7343   * But you should never do that anyway!
7344   * For more information {@link lang_string}
7345   *
7346   * @category string
7347   * @param string $identifier The key identifier for the localized string
7348   * @param string $component The module where the key identifier is stored,
7349   *      usually expressed as the filename in the language pack without the
7350   *      .php on the end but can also be written as mod/forum or grade/export/xls.
7351   *      If none is specified then moodle.php is used.
7352   * @param string|object|array $a An object, string or number that can be used
7353   *      within translation strings
7354   * @param bool $lazyload If set to true a string object is returned instead of
7355   *      the string itself. The string then isn't calculated until it is first used.
7356   * @return string The localized string.
7357   * @throws coding_exception
7358   */
7359  function get_string($identifier, $component = '', $a = null, $lazyload = false) {
7360      global $CFG;
7361  
7362      // If the lazy load argument has been supplied return a lang_string object
7363      // instead.
7364      // We need to make sure it is true (and a bool) as you will see below there
7365      // used to be a forth argument at one point.
7366      if ($lazyload === true) {
7367          return new lang_string($identifier, $component, $a);
7368      }
7369  
7370      if ($CFG->debugdeveloper && clean_param($identifier, PARAM_STRINGID) === '') {
7371          throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.', DEBUG_DEVELOPER);
7372      }
7373  
7374      // There is now a forth argument again, this time it is a boolean however so
7375      // we can still check for the old extralocations parameter.
7376      if (!is_bool($lazyload) && !empty($lazyload)) {
7377          debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.');
7378      }
7379  
7380      if (strpos($component, '/') !== false) {
7381          debugging('The module name you passed to get_string is the deprecated format ' .
7382                  'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER);
7383          $componentpath = explode('/', $component);
7384  
7385          switch ($componentpath[0]) {
7386              case 'mod':
7387                  $component = $componentpath[1];
7388                  break;
7389              case 'blocks':
7390              case 'block':
7391                  $component = 'block_'.$componentpath[1];
7392                  break;
7393              case 'enrol':
7394                  $component = 'enrol_'.$componentpath[1];
7395                  break;
7396              case 'format':
7397                  $component = 'format_'.$componentpath[1];
7398                  break;
7399              case 'grade':
7400                  $component = 'grade'.$componentpath[1].'_'.$componentpath[2];
7401                  break;
7402          }
7403      }
7404  
7405      $result = get_string_manager()->get_string($identifier, $component, $a);
7406  
7407      // Debugging feature lets you display string identifier and component.
7408      if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
7409          $result .= ' {' . $identifier . '/' . $component . '}';
7410      }
7411      return $result;
7412  }
7413  
7414  /**
7415   * Converts an array of strings to their localized value.
7416   *
7417   * @param array $array An array of strings
7418   * @param string $component The language module that these strings can be found in.
7419   * @return stdClass translated strings.
7420   */
7421  function get_strings($array, $component = '') {
7422      $string = new stdClass;
7423      foreach ($array as $item) {
7424          $string->$item = get_string($item, $component);
7425      }
7426      return $string;
7427  }
7428  
7429  /**
7430   * Prints out a translated string.
7431   *
7432   * Prints out a translated string using the return value from the {@link get_string()} function.
7433   *
7434   * Example usage of this function when the string is in the moodle.php file:<br/>
7435   * <code>
7436   * echo '<strong>';
7437   * print_string('course');
7438   * echo '</strong>';
7439   * </code>
7440   *
7441   * Example usage of this function when the string is not in the moodle.php file:<br/>
7442   * <code>
7443   * echo '<h1>';
7444   * print_string('typecourse', 'calendar');
7445   * echo '</h1>';
7446   * </code>
7447   *
7448   * @category string
7449   * @param string $identifier The key identifier for the localized string
7450   * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used.
7451   * @param string|object|array $a An object, string or number that can be used within translation strings
7452   */
7453  function print_string($identifier, $component = '', $a = null) {
7454      echo get_string($identifier, $component, $a);
7455  }
7456  
7457  /**
7458   * Returns a list of charset codes
7459   *
7460   * Returns a list of charset codes. It's hardcoded, so they should be added manually
7461   * (checking that such charset is supported by the texlib library!)
7462   *
7463   * @return array And associative array with contents in the form of charset => charset
7464   */
7465  function get_list_of_charsets() {
7466  
7467      $charsets = array(
7468          'EUC-JP'     => 'EUC-JP',
7469          'ISO-2022-JP'=> 'ISO-2022-JP',
7470          'ISO-8859-1' => 'ISO-8859-1',
7471          'SHIFT-JIS'  => 'SHIFT-JIS',
7472          'GB2312'     => 'GB2312',
7473          'GB18030'    => 'GB18030', // GB18030 not supported by typo and mbstring.
7474          'UTF-8'      => 'UTF-8');
7475  
7476      asort($charsets);
7477  
7478      return $charsets;
7479  }
7480  
7481  /**
7482   * Returns a list of valid and compatible themes
7483   *
7484   * @return array
7485   */
7486  function get_list_of_themes() {
7487      global $CFG;
7488  
7489      $themes = array();
7490  
7491      if (!empty($CFG->themelist)) {       // Use admin's list of themes.
7492          $themelist = explode(',', $CFG->themelist);
7493      } else {
7494          $themelist = array_keys(core_component::get_plugin_list("theme"));
7495      }
7496  
7497      foreach ($themelist as $key => $themename) {
7498          $theme = theme_config::load($themename);
7499          $themes[$themename] = $theme;
7500      }
7501  
7502      core_collator::asort_objects_by_method($themes, 'get_theme_name');
7503  
7504      return $themes;
7505  }
7506  
7507  /**
7508   * Factory function for emoticon_manager
7509   *
7510   * @return emoticon_manager singleton
7511   */
7512  function get_emoticon_manager() {
7513      static $singleton = null;
7514  
7515      if (is_null($singleton)) {
7516          $singleton = new emoticon_manager();
7517      }
7518  
7519      return $singleton;
7520  }
7521  
7522  /**
7523   * Provides core support for plugins that have to deal with emoticons (like HTML editor or emoticon filter).
7524   *
7525   * Whenever this manager mentiones 'emoticon object', the following data
7526   * structure is expected: stdClass with properties text, imagename, imagecomponent,
7527   * altidentifier and altcomponent
7528   *
7529   * @see admin_setting_emoticons
7530   *
7531   * @copyright 2010 David Mudrak
7532   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
7533   */
7534  class emoticon_manager {
7535  
7536      /**
7537       * Returns the currently enabled emoticons
7538       *
7539       * @param boolean $selectable - If true, only return emoticons that should be selectable from a list.
7540       * @return array of emoticon objects
7541       */
7542      public function get_emoticons($selectable = false) {
7543          global $CFG;
7544          $notselectable = ['martin', 'egg'];
7545  
7546          if (empty($CFG->emoticons)) {
7547              return array();
7548          }
7549  
7550          $emoticons = $this->decode_stored_config($CFG->emoticons);
7551  
7552          if (!is_array($emoticons)) {
7553              // Something is wrong with the format of stored setting.
7554              debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL);
7555              return array();
7556          }
7557          if ($selectable) {
7558              foreach ($emoticons as $index => $emote) {
7559                  if (in_array($emote->altidentifier, $notselectable)) {
7560                      // Skip this one.
7561                      unset($emoticons[$index]);
7562                  }
7563              }
7564          }
7565  
7566          return $emoticons;
7567      }
7568  
7569      /**
7570       * Converts emoticon object into renderable pix_emoticon object
7571       *
7572       * @param stdClass $emoticon emoticon object
7573       * @param array $attributes explicit HTML attributes to set
7574       * @return pix_emoticon
7575       */
7576      public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array()) {
7577          $stringmanager = get_string_manager();
7578          if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) {
7579              $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent);
7580          } else {
7581              $alt = s($emoticon->text);
7582          }
7583          return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes);
7584      }
7585  
7586      /**
7587       * Encodes the array of emoticon objects into a string storable in config table
7588       *
7589       * @see self::decode_stored_config()
7590       * @param array $emoticons array of emtocion objects
7591       * @return string
7592       */
7593      public function encode_stored_config(array $emoticons) {
7594          return json_encode($emoticons);
7595      }
7596  
7597      /**
7598       * Decodes the string into an array of emoticon objects
7599       *
7600       * @see self::encode_stored_config()
7601       * @param string $encoded
7602       * @return string|null
7603       */
7604      public function decode_stored_config($encoded) {
7605          $decoded = json_decode($encoded);
7606          if (!is_array($decoded)) {
7607              return null;
7608          }
7609          return $decoded;
7610      }
7611  
7612      /**
7613       * Returns default set of emoticons supported by Moodle
7614       *
7615       * @return array of sdtClasses
7616       */
7617      public function default_emoticons() {
7618          return array(
7619              $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'),
7620              $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'),
7621              $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'),
7622              $this->prepare_emoticon_object(";-)", 's/wink', 'wink'),
7623              $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'),
7624              $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'),
7625              $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'),
7626              $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'),
7627              $this->prepare_emoticon_object("B-)", 's/cool', 'cool'),
7628              $this->prepare_emoticon_object("^-)", 's/approve', 'approve'),
7629              $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'),
7630              $this->prepare_emoticon_object(":o)", 's/clown', 'clown'),
7631              $this->prepare_emoticon_object(":-(", 's/sad', 'sad'),
7632              $this->prepare_emoticon_object(":(", 's/sad', 'sad'),
7633              $this->prepare_emoticon_object("8-.", 's/shy', 'shy'),
7634              $this->prepare_emoticon_object(":-I", 's/blush', 'blush'),
7635              $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'),
7636              $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'),
7637              $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'),
7638              $this->prepare_emoticon_object("8-[", 's/angry', 'angry'),
7639              $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'),
7640              $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'),
7641              $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'),
7642              $this->prepare_emoticon_object("}-]", 's/evil', 'evil'),
7643              $this->prepare_emoticon_object("(h)", 's/heart', 'heart'),
7644              $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'),
7645              $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'),
7646              $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'),
7647              $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'),
7648              $this->prepare_emoticon_object("( )", 's/egg', 'egg'),
7649          );
7650      }
7651  
7652      /**
7653       * Helper method preparing the stdClass with the emoticon properties
7654       *
7655       * @param string|array $text or array of strings
7656       * @param string $imagename to be used by {@link pix_emoticon}
7657       * @param string $altidentifier alternative string identifier, null for no alt
7658       * @param string $altcomponent where the alternative string is defined
7659       * @param string $imagecomponent to be used by {@link pix_emoticon}
7660       * @return stdClass
7661       */
7662      protected function prepare_emoticon_object($text, $imagename, $altidentifier = null,
7663                                                 $altcomponent = 'core_pix', $imagecomponent = 'core') {
7664          return (object)array(
7665              'text'           => $text,
7666              'imagename'      => $imagename,
7667              'imagecomponent' => $imagecomponent,
7668              'altidentifier'  => $altidentifier,
7669              'altcomponent'   => $altcomponent,
7670          );
7671      }
7672  }
7673  
7674  // ENCRYPTION.
7675  
7676  /**
7677   * rc4encrypt
7678   *
7679   * @param string $data        Data to encrypt.
7680   * @return string             The now encrypted data.
7681   */
7682  function rc4encrypt($data) {
7683      return endecrypt(get_site_identifier(), $data, '');
7684  }
7685  
7686  /**
7687   * rc4decrypt
7688   *
7689   * @param string $data        Data to decrypt.
7690   * @return string             The now decrypted data.
7691   */
7692  function rc4decrypt($data) {
7693      return endecrypt(get_site_identifier(), $data, 'de');
7694  }
7695  
7696  /**
7697   * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com]
7698   *
7699   * @todo Finish documenting this function
7700   *
7701   * @param string $pwd The password to use when encrypting or decrypting
7702   * @param string $data The data to be decrypted/encrypted
7703   * @param string $case Either 'de' for decrypt or '' for encrypt
7704   * @return string
7705   */
7706  function endecrypt ($pwd, $data, $case) {
7707  
7708      if ($case == 'de') {
7709          $data = urldecode($data);
7710      }
7711  
7712      $key[] = '';
7713      $box[] = '';
7714      $pwdlength = strlen($pwd);
7715  
7716      for ($i = 0; $i <= 255; $i++) {
7717          $key[$i] = ord(substr($pwd, ($i % $pwdlength), 1));
7718          $box[$i] = $i;
7719      }
7720  
7721      $x = 0;
7722  
7723      for ($i = 0; $i <= 255; $i++) {
7724          $x = ($x + $box[$i] + $key[$i]) % 256;
7725          $tempswap = $box[$i];
7726          $box[$i] = $box[$x];
7727          $box[$x] = $tempswap;
7728      }
7729  
7730      $cipher = '';
7731  
7732      $a = 0;
7733      $j = 0;
7734  
7735      for ($i = 0; $i < strlen($data); $i++) {
7736          $a = ($a + 1) % 256;
7737          $j = ($j + $box[$a]) % 256;
7738          $temp = $box[$a];
7739          $box[$a] = $box[$j];
7740          $box[$j] = $temp;
7741          $k = $box[(($box[$a] + $box[$j]) % 256)];
7742          $cipherby = ord(substr($data, $i, 1)) ^ $k;
7743          $cipher .= chr($cipherby);
7744      }
7745  
7746      if ($case == 'de') {
7747          $cipher = urldecode(urlencode($cipher));
7748      } else {
7749          $cipher = urlencode($cipher);
7750      }
7751  
7752      return $cipher;
7753  }
7754  
7755  // ENVIRONMENT CHECKING.
7756  
7757  /**
7758   * This method validates a plug name. It is much faster than calling clean_param.
7759   *
7760   * @param string $name a string that might be a plugin name.
7761   * @return bool if this string is a valid plugin name.
7762   */
7763  function is_valid_plugin_name($name) {
7764      // This does not work for 'mod', bad luck, use any other type.
7765      return core_component::is_valid_plugin_name('tool', $name);
7766  }
7767  
7768  /**
7769   * Get a list of all the plugins of a given type that define a certain API function
7770   * in a certain file. The plugin component names and function names are returned.
7771   *
7772   * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
7773   * @param string $function the part of the name of the function after the
7774   *      frankenstyle prefix. e.g 'hook' if you are looking for functions with
7775   *      names like report_courselist_hook.
7776   * @param string $file the name of file within the plugin that defines the
7777   *      function. Defaults to lib.php.
7778   * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
7779   *      and the function names as values (e.g. 'report_courselist_hook', 'forum_hook').
7780   */
7781  function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php') {
7782      global $CFG;
7783  
7784      // We don't include here as all plugin types files would be included.
7785      $plugins = get_plugins_with_function($function, $file, false);
7786  
7787      if (empty($plugins[$plugintype])) {
7788          return array();
7789      }
7790  
7791      $allplugins = core_component::get_plugin_list($plugintype);
7792  
7793      // Reformat the array and include the files.
7794      $pluginfunctions = array();
7795      foreach ($plugins[$plugintype] as $pluginname => $functionname) {
7796  
7797          // Check that it has not been removed and the file is still available.
7798          if (!empty($allplugins[$pluginname])) {
7799  
7800              $filepath = $allplugins[$pluginname] . DIRECTORY_SEPARATOR . $file;
7801              if (file_exists($filepath)) {
7802                  include_once($filepath);
7803  
7804                  // Now that the file is loaded, we must verify the function still exists.
7805                  if (function_exists($functionname)) {
7806                      $pluginfunctions[$plugintype . '_' . $pluginname] = $functionname;
7807                  } else {
7808                      // Invalidate the cache for next run.
7809                      \cache_helper::invalidate_by_definition('core', 'plugin_functions');
7810                  }
7811              }
7812          }
7813      }
7814  
7815      return $pluginfunctions;
7816  }
7817  
7818  /**
7819   * Get a list of all the plugins that define a certain API function in a certain file.
7820   *
7821   * @param string $function the part of the name of the function after the
7822   *      frankenstyle prefix. e.g 'hook' if you are looking for functions with
7823   *      names like report_courselist_hook.
7824   * @param string $file the name of file within the plugin that defines the
7825   *      function. Defaults to lib.php.
7826   * @param bool $include Whether to include the files that contain the functions or not.
7827   * @return array with [plugintype][plugin] = functionname
7828   */
7829  function get_plugins_with_function($function, $file = 'lib.php', $include = true) {
7830      global $CFG;
7831  
7832      if (during_initial_install() || isset($CFG->upgraderunning)) {
7833          // API functions _must not_ be called during an installation or upgrade.
7834          return [];
7835      }
7836  
7837      $cache = \cache::make('core', 'plugin_functions');
7838  
7839      // Including both although I doubt that we will find two functions definitions with the same name.
7840      // Clearning the filename as cache_helper::hash_key only allows a-zA-Z0-9_.
7841      $key = $function . '_' . clean_param($file, PARAM_ALPHA);
7842      $pluginfunctions = $cache->get($key);
7843      $dirty = false;
7844  
7845      // Use the plugin manager to check that plugins are currently installed.
7846      $pluginmanager = \core_plugin_manager::instance();
7847  
7848      if ($pluginfunctions !== false) {
7849  
7850          // Checking that the files are still available.
7851          foreach ($pluginfunctions as $plugintype => $plugins) {
7852  
7853              $allplugins = \core_component::get_plugin_list($plugintype);
7854              $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7855              foreach ($plugins as $plugin => $function) {
7856                  if (!isset($installedplugins[$plugin])) {
7857                      // Plugin code is still present on disk but it is not installed.
7858                      $dirty = true;
7859                      break 2;
7860                  }
7861  
7862                  // Cache might be out of sync with the codebase, skip the plugin if it is not available.
7863                  if (empty($allplugins[$plugin])) {
7864                      $dirty = true;
7865                      break 2;
7866                  }
7867  
7868                  $fileexists = file_exists($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7869                  if ($include && $fileexists) {
7870                      // Include the files if it was requested.
7871                      include_once($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7872                  } else if (!$fileexists) {
7873                      // If the file is not available any more it should not be returned.
7874                      $dirty = true;
7875                      break 2;
7876                  }
7877  
7878                  // Check if the function still exists in the file.
7879                  if ($include && !function_exists($function)) {
7880                      $dirty = true;
7881                      break 2;
7882                  }
7883              }
7884          }
7885  
7886          // If the cache is dirty, we should fall through and let it rebuild.
7887          if (!$dirty) {
7888              return $pluginfunctions;
7889          }
7890      }
7891  
7892      $pluginfunctions = array();
7893  
7894      // To fill the cached. Also, everything should continue working with cache disabled.
7895      $plugintypes = \core_component::get_plugin_types();
7896      foreach ($plugintypes as $plugintype => $unused) {
7897  
7898          // We need to include files here.
7899          $pluginswithfile = \core_component::get_plugin_list_with_file($plugintype, $file, true);
7900          $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7901          foreach ($pluginswithfile as $plugin => $notused) {
7902  
7903              if (!isset($installedplugins[$plugin])) {
7904                  continue;
7905              }
7906  
7907              $fullfunction = $plugintype . '_' . $plugin . '_' . $function;
7908  
7909              $pluginfunction = false;
7910              if (function_exists($fullfunction)) {
7911                  // Function exists with standard name. Store, indexed by frankenstyle name of plugin.
7912                  $pluginfunction = $fullfunction;
7913  
7914              } else if ($plugintype === 'mod') {
7915                  // For modules, we also allow plugin without full frankenstyle but just starting with the module name.
7916                  $shortfunction = $plugin . '_' . $function;
7917                  if (function_exists($shortfunction)) {
7918                      $pluginfunction = $shortfunction;
7919                  }
7920              }
7921  
7922              if ($pluginfunction) {
7923                  if (empty($pluginfunctions[$plugintype])) {
7924                      $pluginfunctions[$plugintype] = array();
7925                  }
7926                  $pluginfunctions[$plugintype][$plugin] = $pluginfunction;
7927              }
7928  
7929          }
7930      }
7931      $cache->set($key, $pluginfunctions);
7932  
7933      return $pluginfunctions;
7934  
7935  }
7936  
7937  /**
7938   * Lists plugin-like directories within specified directory
7939   *
7940   * This function was originally used for standard Moodle plugins, please use
7941   * new core_component::get_plugin_list() now.
7942   *
7943   * This function is used for general directory listing and backwards compatility.
7944   *
7945   * @param string $directory relative directory from root
7946   * @param string $exclude dir name to exclude from the list (defaults to none)
7947   * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot)
7948   * @return array Sorted array of directory names found under the requested parameters
7949   */
7950  function get_list_of_plugins($directory='mod', $exclude='', $basedir='') {
7951      global $CFG;
7952  
7953      $plugins = array();
7954  
7955      if (empty($basedir)) {
7956          $basedir = $CFG->dirroot .'/'. $directory;
7957  
7958      } else {
7959          $basedir = $basedir .'/'. $directory;
7960      }
7961  
7962      if ($CFG->debugdeveloper and empty($exclude)) {
7963          // Make sure devs do not use this to list normal plugins,
7964          // this is intended for general directories that are not plugins!
7965  
7966          $subtypes = core_component::get_plugin_types();
7967          if (in_array($basedir, $subtypes)) {
7968              debugging('get_list_of_plugins() should not be used to list real plugins, use core_component::get_plugin_list() instead!', DEBUG_DEVELOPER);
7969          }
7970          unset($subtypes);
7971      }
7972  
7973      if (file_exists($basedir) && filetype($basedir) == 'dir') {
7974          if (!$dirhandle = opendir($basedir)) {
7975              debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER);
7976              return array();
7977          }
7978          while (false !== ($dir = readdir($dirhandle))) {
7979              // Func: strpos is marginally but reliably faster than substr($dir, 0, 1).
7980              if (strpos($dir, '.') === 0 or $dir === 'CVS' or $dir === '_vti_cnf' or $dir === 'simpletest' or $dir === 'yui' or
7981                  $dir === 'tests' or $dir === 'classes' or $dir === $exclude) {
7982                  continue;
7983              }
7984              if (filetype($basedir .'/'. $dir) != 'dir') {
7985                  continue;
7986              }
7987              $plugins[] = $dir;
7988          }
7989          closedir($dirhandle);
7990      }
7991      if ($plugins) {
7992          asort($plugins);
7993      }
7994      return $plugins;
7995  }
7996  
7997  /**
7998   * Invoke plugin's callback functions
7999   *
8000   * @param string $type plugin type e.g. 'mod'
8001   * @param string $name plugin name
8002   * @param string $feature feature name
8003   * @param string $action feature's action
8004   * @param array $params parameters of callback function, should be an array
8005   * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
8006   * @return mixed
8007   *
8008   * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743
8009   */
8010  function plugin_callback($type, $name, $feature, $action, $params = null, $default = null) {
8011      return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default);
8012  }
8013  
8014  /**
8015   * Invoke component's callback functions
8016   *
8017   * @param string $component frankenstyle component name, e.g. 'mod_quiz'
8018   * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
8019   * @param array $params parameters of callback function
8020   * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
8021   * @return mixed
8022   */
8023  function component_callback($component, $function, array $params = array(), $default = null) {
8024  
8025      $functionname = component_callback_exists($component, $function);
8026  
8027      if ($functionname) {
8028          // Function exists, so just return function result.
8029          $ret = call_user_func_array($functionname, $params);
8030          if (is_null($ret)) {
8031              return $default;
8032          } else {
8033              return $ret;
8034          }
8035      }
8036      return $default;
8037  }
8038  
8039  /**
8040   * Determine if a component callback exists and return the function name to call. Note that this
8041   * function will include the required library files so that the functioname returned can be
8042   * called directly.
8043   *
8044   * @param string $component frankenstyle component name, e.g. 'mod_quiz'
8045   * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
8046   * @return mixed Complete function name to call if the callback exists or false if it doesn't.
8047   * @throws coding_exception if invalid component specfied
8048   */
8049  function component_callback_exists($component, $function) {
8050      global $CFG; // This is needed for the inclusions.
8051  
8052      $cleancomponent = clean_param($component, PARAM_COMPONENT);
8053      if (empty($cleancomponent)) {
8054          throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
8055      }
8056      $component = $cleancomponent;
8057  
8058      list($type, $name) = core_component::normalize_component($component);
8059      $component = $type . '_' . $name;
8060  
8061      $oldfunction = $name.'_'.$function;
8062      $function = $component.'_'.$function;
8063  
8064      $dir = core_component::get_component_directory($component);
8065      if (empty($dir)) {
8066          throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
8067      }
8068  
8069      // Load library and look for function.
8070      if (file_exists($dir.'/lib.php')) {
8071          require_once($dir.'/lib.php');
8072      }
8073  
8074      if (!function_exists($function) and function_exists($oldfunction)) {
8075          if ($type !== 'mod' and $type !== 'core') {
8076              debugging("Please use new function name $function instead of legacy $oldfunction", DEBUG_DEVELOPER);
8077          }
8078          $function = $oldfunction;
8079      }
8080  
8081      if (function_exists($function)) {
8082          return $function;
8083      }
8084      return false;
8085  }
8086  
8087  /**
8088   * Call the specified callback method on the provided class.
8089   *
8090   * If the callback returns null, then the default value is returned instead.
8091   * If the class does not exist, then the default value is returned.
8092   *
8093   * @param   string      $classname The name of the class to call upon.
8094   * @param   string      $methodname The name of the staticically defined method on the class.
8095   * @param   array       $params The arguments to pass into the method.
8096   * @param   mixed       $default The default value.
8097   * @return  mixed       The return value.
8098   */
8099  function component_class_callback($classname, $methodname, array $params, $default = null) {
8100      if (!class_exists($classname)) {
8101          return $default;
8102      }
8103  
8104      if (!method_exists($classname, $methodname)) {
8105          return $default;
8106      }
8107  
8108      $fullfunction = $classname . '::' . $methodname;
8109      $result = call_user_func_array($fullfunction, $params);
8110  
8111      if (null === $result) {
8112          return $default;
8113      } else {
8114          return $result;
8115      }
8116  }
8117  
8118  /**
8119   * Checks whether a plugin supports a specified feature.
8120   *
8121   * @param string $type Plugin type e.g. 'mod'
8122   * @param string $name Plugin name e.g. 'forum'
8123   * @param string $feature Feature code (FEATURE_xx constant)
8124   * @param mixed $default default value if feature support unknown
8125   * @return mixed Feature result (false if not supported, null if feature is unknown,
8126   *         otherwise usually true but may have other feature-specific value such as array)
8127   * @throws coding_exception
8128   */
8129  function plugin_supports($type, $name, $feature, $default = null) {
8130      global $CFG;
8131  
8132      if ($type === 'mod' and $name === 'NEWMODULE') {
8133          // Somebody forgot to rename the module template.
8134          return false;
8135      }
8136  
8137      $component = clean_param($type . '_' . $name, PARAM_COMPONENT);
8138      if (empty($component)) {
8139          throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name);
8140      }
8141  
8142      $function = null;
8143  
8144      if ($type === 'mod') {
8145          // We need this special case because we support subplugins in modules,
8146          // otherwise it would end up in infinite loop.
8147          if (file_exists("$CFG->dirroot/mod/$name/lib.php")) {
8148              include_once("$CFG->dirroot/mod/$name/lib.php");
8149              $function = $component.'_supports';
8150              if (!function_exists($function)) {
8151                  // Legacy non-frankenstyle function name.
8152                  $function = $name.'_supports';
8153              }
8154          }
8155  
8156      } else {
8157          if (!$path = core_component::get_plugin_directory($type, $name)) {
8158              // Non existent plugin type.
8159              return false;
8160          }
8161          if (file_exists("$path/lib.php")) {
8162              include_once("$path/lib.php");
8163              $function = $component.'_supports';
8164          }
8165      }
8166  
8167      if ($function and function_exists($function)) {
8168          $supports = $function($feature);
8169          if (is_null($supports)) {
8170              // Plugin does not know - use default.
8171              return $default;
8172          } else {
8173              return $supports;
8174          }
8175      }
8176  
8177      // Plugin does not care, so use default.
8178      return $default;
8179  }
8180  
8181  /**
8182   * Returns true if the current version of PHP is greater that the specified one.
8183   *
8184   * @todo Check PHP version being required here is it too low?
8185   *
8186   * @param string $version The version of php being tested.
8187   * @return bool
8188   */
8189  function check_php_version($version='5.2.4') {
8190      return (version_compare(phpversion(), $version) >= 0);
8191  }
8192  
8193  /**
8194   * Determine if moodle installation requires update.
8195   *
8196   * Checks version numbers of main code and all plugins to see
8197   * if there are any mismatches.
8198   *
8199   * @return bool
8200   */
8201  function moodle_needs_upgrading() {
8202      global $CFG;
8203  
8204      if (empty($CFG->version)) {
8205          return true;
8206      }
8207  
8208      // There is no need to purge plugininfo caches here because
8209      // these caches are not used during upgrade and they are purged after
8210      // every upgrade.
8211  
8212      if (empty($CFG->allversionshash)) {
8213          return true;
8214      }
8215  
8216      $hash = core_component::get_all_versions_hash();
8217  
8218      return ($hash !== $CFG->allversionshash);
8219  }
8220  
8221  /**
8222   * Returns the major version of this site
8223   *
8224   * Moodle version numbers consist of three numbers separated by a dot, for
8225   * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so
8226   * called major version. This function extracts the major version from either
8227   * $CFG->release (default) or eventually from the $release variable defined in
8228   * the main version.php.
8229   *
8230   * @param bool $fromdisk should the version if source code files be used
8231   * @return string|false the major version like '2.3', false if could not be determined
8232   */
8233  function moodle_major_version($fromdisk = false) {
8234      global $CFG;
8235  
8236      if ($fromdisk) {
8237          $release = null;
8238          require($CFG->dirroot.'/version.php');
8239          if (empty($release)) {
8240              return false;
8241          }
8242  
8243      } else {
8244          if (empty($CFG->release)) {
8245              return false;
8246          }
8247          $release = $CFG->release;
8248      }
8249  
8250      if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) {
8251          return $matches[0];
8252      } else {
8253          return false;
8254      }
8255  }
8256  
8257  // MISCELLANEOUS.
8258  
8259  /**
8260   * Gets the system locale
8261   *
8262   * @return string Retuns the current locale.
8263   */
8264  function moodle_getlocale() {
8265      global $CFG;
8266  
8267      // Fetch the correct locale based on ostype.
8268      if ($CFG->ostype == 'WINDOWS') {
8269          $stringtofetch = 'localewin';
8270      } else {
8271          $stringtofetch = 'locale';
8272      }
8273  
8274      if (!empty($CFG->locale)) { // Override locale for all language packs.
8275          return $CFG->locale;
8276      }
8277  
8278      return get_string($stringtofetch, 'langconfig');
8279  }
8280  
8281  /**
8282   * Sets the system locale
8283   *
8284   * @category string
8285   * @param string $locale Can be used to force a locale
8286   */
8287  function moodle_setlocale($locale='') {
8288      global $CFG;
8289  
8290      static $currentlocale = ''; // Last locale caching.
8291  
8292      $oldlocale = $currentlocale;
8293  
8294      // The priority is the same as in get_string() - parameter, config, course, session, user, global language.
8295      if (!empty($locale)) {
8296          $currentlocale = $locale;
8297      } else {
8298          $currentlocale = moodle_getlocale();
8299      }
8300  
8301      // Do nothing if locale already set up.
8302      if ($oldlocale == $currentlocale) {
8303          return;
8304      }
8305  
8306      // Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values,
8307      // set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7
8308      // Some day, numeric, monetary and other categories should be set too, I think. :-/.
8309  
8310      // Get current values.
8311      $monetary= setlocale (LC_MONETARY, 0);
8312      $numeric = setlocale (LC_NUMERIC, 0);
8313      $ctype   = setlocale (LC_CTYPE, 0);
8314      if ($CFG->ostype != 'WINDOWS') {
8315          $messages= setlocale (LC_MESSAGES, 0);
8316      }
8317      // Set locale to all.
8318      $result = setlocale (LC_ALL, $currentlocale);
8319      // If setting of locale fails try the other utf8 or utf-8 variant,
8320      // some operating systems support both (Debian), others just one (OSX).
8321      if ($result === false) {
8322          if (stripos($currentlocale, '.UTF-8') !== false) {
8323              $newlocale = str_ireplace('.UTF-8', '.UTF8', $currentlocale);
8324              setlocale (LC_ALL, $newlocale);
8325          } else if (stripos($currentlocale, '.UTF8') !== false) {
8326              $newlocale = str_ireplace('.UTF8', '.UTF-8', $currentlocale);
8327              setlocale (LC_ALL, $newlocale);
8328          }
8329      }
8330      // Set old values.
8331      setlocale (LC_MONETARY, $monetary);
8332      setlocale (LC_NUMERIC, $numeric);
8333      if ($CFG->ostype != 'WINDOWS') {
8334          setlocale (LC_MESSAGES, $messages);
8335      }
8336      if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') {
8337          // To workaround a well-known PHP problem with Turkish letter Ii.
8338          setlocale (LC_CTYPE, $ctype);
8339      }
8340  }
8341  
8342  /**
8343   * Count words in a string.
8344   *
8345   * Words are defined as things between whitespace.
8346   *
8347   * @category string
8348   * @param string $string The text to be searched for words. May be HTML.
8349   * @return int The count of words in the specified string
8350   */
8351  function count_words($string) {
8352      // Before stripping tags, add a space after the close tag of anything that is not obviously inline.
8353      // Also, br is a special case because it definitely delimits a word, but has no close tag.
8354      $string = preg_replace('~
8355              (                                   # Capture the tag we match.
8356                  </                              # Start of close tag.
8357                  (?!                             # Do not match any of these specific close tag names.
8358                      a> | b> | del> | em> | i> |
8359                      ins> | s> | small> |
8360                      strong> | sub> | sup> | u>
8361                  )
8362                  \w+                             # But, apart from those execptions, match any tag name.
8363                  >                               # End of close tag.
8364              |
8365                  <br> | <br\s*/>                 # Special cases that are not close tags.
8366              )
8367              ~x', '$1 ', $string); // Add a space after the close tag.
8368      // Now remove HTML tags.
8369      $string = strip_tags($string);
8370      // Decode HTML entities.
8371      $string = html_entity_decode($string);
8372  
8373      // Now, the word count is the number of blocks of characters separated
8374      // by any sort of space. That seems to be the definition used by all other systems.
8375      // To be precise about what is considered to separate words:
8376      // * Anything that Unicode considers a 'Separator'
8377      // * Anything that Unicode considers a 'Control character'
8378      // * An em- or en- dash.
8379      return count(preg_split('~[\p{Z}\p{Cc}—–]+~u', $string, -1, PREG_SPLIT_NO_EMPTY));
8380  }
8381  
8382  /**
8383   * Count letters in a string.
8384   *
8385   * Letters are defined as chars not in tags and different from whitespace.
8386   *
8387   * @category string
8388   * @param string $string The text to be searched for letters. May be HTML.
8389   * @return int The count of letters in the specified text.
8390   */
8391  function count_letters($string) {
8392      $string = strip_tags($string); // Tags are out now.
8393      $string = html_entity_decode($string);
8394      $string = preg_replace('/[[:space:]]*/', '', $string); // Whitespace are out now.
8395  
8396      return core_text::strlen($string);
8397  }
8398  
8399  /**
8400   * Generate and return a random string of the specified length.
8401   *
8402   * @param int $length The length of the string to be created.
8403   * @return string
8404   */
8405  function random_string($length=15) {
8406      $randombytes = random_bytes_emulate($length);
8407      $pool  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
8408      $pool .= 'abcdefghijklmnopqrstuvwxyz';
8409      $pool .= '0123456789';
8410      $poollen = strlen($pool);
8411      $string = '';
8412      for ($i = 0; $i < $length; $i++) {
8413          $rand = ord($randombytes[$i]);
8414          $string .= substr($pool, ($rand%($poollen)), 1);
8415      }
8416      return $string;
8417  }
8418  
8419  /**
8420   * Generate a complex random string (useful for md5 salts)
8421   *
8422   * This function is based on the above {@link random_string()} however it uses a
8423   * larger pool of characters and generates a string between 24 and 32 characters
8424   *
8425   * @param int $length Optional if set generates a string to exactly this length
8426   * @return string
8427   */
8428  function complex_random_string($length=null) {
8429      $pool  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
8430      $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
8431      $poollen = strlen($pool);
8432      if ($length===null) {
8433          $length = floor(rand(24, 32));
8434      }
8435      $randombytes = random_bytes_emulate($length);
8436      $string = '';
8437      for ($i = 0; $i < $length; $i++) {
8438          $rand = ord($randombytes[$i]);
8439          $string .= $pool[($rand%$poollen)];
8440      }
8441      return $string;
8442  }
8443  
8444  /**
8445   * Try to generates cryptographically secure pseudo-random bytes.
8446   *
8447   * Note this is achieved by fallbacking between:
8448   *  - PHP 7 random_bytes().
8449   *  - OpenSSL openssl_random_pseudo_bytes().
8450   *  - In house random generator getting its entropy from various, hard to guess, pseudo-random sources.
8451   *
8452   * @param int $length requested length in bytes
8453   * @return string binary data
8454   */
8455  function random_bytes_emulate($length) {
8456      global $CFG;
8457      if ($length <= 0) {
8458          debugging('Invalid random bytes length', DEBUG_DEVELOPER);
8459          return '';
8460      }
8461      if (function_exists('random_bytes')) {
8462          // Use PHP 7 goodness.
8463          $hash = @random_bytes($length);
8464          if ($hash !== false) {
8465              return $hash;
8466          }
8467      }
8468      if (function_exists('openssl_random_pseudo_bytes')) {
8469          // If you have the openssl extension enabled.
8470          $hash = openssl_random_pseudo_bytes($length);
8471          if ($hash !== false) {
8472              return $hash;
8473          }
8474      }
8475  
8476      // Bad luck, there is no reliable random generator, let's just slowly hash some unique stuff that is hard to guess.
8477      $staticdata = serialize($CFG) . serialize($_SERVER);
8478      $hash = '';
8479      do {
8480          $hash .= sha1($staticdata . microtime(true) . uniqid('', true), true);
8481      } while (strlen($hash) < $length);
8482  
8483      return substr($hash, 0, $length);
8484  }
8485  
8486  /**
8487   * Given some text (which may contain HTML) and an ideal length,
8488   * this function truncates the text neatly on a word boundary if possible
8489   *
8490   * @category string
8491   * @param string $text text to be shortened
8492   * @param int $ideal ideal string length
8493   * @param boolean $exact if false, $text will not be cut mid-word
8494   * @param string $ending The string to append if the passed string is truncated
8495   * @return string $truncate shortened string
8496   */
8497  function shorten_text($text, $ideal=30, $exact = false, $ending='...') {
8498      // If the plain text is shorter than the maximum length, return the whole text.
8499      if (core_text::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) {
8500          return $text;
8501      }
8502  
8503      // Splits on HTML tags. Each open/close/empty tag will be the first thing
8504      // and only tag in its 'line'.
8505      preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
8506  
8507      $totallength = core_text::strlen($ending);
8508      $truncate = '';
8509  
8510      // This array stores information about open and close tags and their position
8511      // in the truncated string. Each item in the array is an object with fields
8512      // ->open (true if open), ->tag (tag name in lower case), and ->pos
8513      // (byte position in truncated text).
8514      $tagdetails = array();
8515  
8516      foreach ($lines as $linematchings) {
8517          // If there is any html-tag in this line, handle it and add it (uncounted) to the output.
8518          if (!empty($linematchings[1])) {
8519              // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>).
8520              if (!preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $linematchings[1])) {
8521                  if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $linematchings[1], $tagmatchings)) {
8522                      // Record closing tag.
8523                      $tagdetails[] = (object) array(
8524                              'open' => false,
8525                              'tag'  => core_text::strtolower($tagmatchings[1]),
8526                              'pos'  => core_text::strlen($truncate),
8527                          );
8528  
8529                  } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $linematchings[1], $tagmatchings)) {
8530                      // Record opening tag.
8531                      $tagdetails[] = (object) array(
8532                              'open' => true,
8533                              'tag'  => core_text::strtolower($tagmatchings[1]),
8534                              'pos'  => core_text::strlen($truncate),
8535                          );
8536                  } else if (preg_match('/^<!--\[if\s.*?\]>$/s', $linematchings[1], $tagmatchings)) {
8537                      $tagdetails[] = (object) array(
8538                              'open' => true,
8539                              'tag'  => core_text::strtolower('if'),
8540                              'pos'  => core_text::strlen($truncate),
8541                      );
8542                  } else if (preg_match('/^<!--<!\[endif\]-->$/s', $linematchings[1], $tagmatchings)) {
8543                      $tagdetails[] = (object) array(
8544                              'open' => false,
8545                              'tag'  => core_text::strtolower('if'),
8546                              'pos'  => core_text::strlen($truncate),
8547                      );
8548                  }
8549              }
8550              // Add html-tag to $truncate'd text.
8551              $truncate .= $linematchings[1];
8552          }
8553  
8554          // Calculate the length of the plain text part of the line; handle entities as one character.
8555          $contentlength = core_text::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $linematchings[2]));
8556          if ($totallength + $contentlength > $ideal) {
8557              // The number of characters which are left.
8558              $left = $ideal - $totallength;
8559              $entitieslength = 0;
8560              // Search for html entities.
8561              if (preg_match_all('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', $linematchings[2], $entities, PREG_OFFSET_CAPTURE)) {
8562                  // Calculate the real length of all entities in the legal range.
8563                  foreach ($entities[0] as $entity) {
8564                      if ($entity[1]+1-$entitieslength <= $left) {
8565                          $left--;
8566                          $entitieslength += core_text::strlen($entity[0]);
8567                      } else {
8568                          // No more characters left.
8569                          break;
8570                      }
8571                  }
8572              }
8573              $breakpos = $left + $entitieslength;
8574  
8575              // If the words shouldn't be cut in the middle...
8576              if (!$exact) {
8577                  // Search the last occurence of a space.
8578                  for (; $breakpos > 0; $breakpos--) {
8579                      if ($char = core_text::substr($linematchings[2], $breakpos, 1)) {
8580                          if ($char === '.' or $char === ' ') {
8581                              $breakpos += 1;
8582                              break;
8583                          } else if (strlen($char) > 2) {
8584                              // Chinese/Japanese/Korean text can be truncated at any UTF-8 character boundary.
8585                              $breakpos += 1;
8586                              break;
8587                          }
8588                      }
8589                  }
8590              }
8591              if ($breakpos == 0) {
8592                  // This deals with the test_shorten_text_no_spaces case.
8593                  $breakpos = $left + $entitieslength;
8594              } else if ($breakpos > $left + $entitieslength) {
8595                  // This deals with the previous for loop breaking on the first char.
8596                  $breakpos = $left + $entitieslength;
8597              }
8598  
8599              $truncate .= core_text::substr($linematchings[2], 0, $breakpos);
8600              // Maximum length is reached, so get off the loop.
8601              break;
8602          } else {
8603              $truncate .= $linematchings[2];
8604              $totallength += $contentlength;
8605          }
8606  
8607          // If the maximum length is reached, get off the loop.
8608          if ($totallength >= $ideal) {
8609              break;
8610          }
8611      }
8612  
8613      // Add the defined ending to the text.
8614      $truncate .= $ending;
8615  
8616      // Now calculate the list of open html tags based on the truncate position.
8617      $opentags = array();
8618      foreach ($tagdetails as $taginfo) {
8619          if ($taginfo->open) {
8620              // Add tag to the beginning of $opentags list.
8621              array_unshift($opentags, $taginfo->tag);
8622          } else {
8623              // Can have multiple exact same open tags, close the last one.
8624              $pos = array_search($taginfo->tag, array_reverse($opentags, true));
8625              if ($pos !== false) {
8626                  unset($opentags[$pos]);
8627              }
8628          }
8629      }
8630  
8631      // Close all unclosed html-tags.
8632      foreach ($opentags as $tag) {
8633          if ($tag === 'if') {
8634              $truncate .= '<!--<![endif]-->';
8635          } else {
8636              $truncate .= '</' . $tag . '>';
8637          }
8638      }
8639  
8640      return $truncate;
8641  }
8642  
8643  /**
8644   * Shortens a given filename by removing characters positioned after the ideal string length.
8645   * When the filename is too long, the file cannot be created on the filesystem due to exceeding max byte size.
8646   * Limiting the filename to a certain size (considering multibyte characters) will prevent this.
8647   *
8648   * @param string $filename file name
8649   * @param int $length ideal string length
8650   * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8651   * @return string $shortened shortened file name
8652   */
8653  function shorten_filename($filename, $length = MAX_FILENAME_SIZE, $includehash = false) {
8654      $shortened = $filename;
8655      // Extract a part of the filename if it's char size exceeds the ideal string length.
8656      if (core_text::strlen($filename) > $length) {
8657          // Exclude extension if present in filename.
8658          $mimetypes = get_mimetypes_array();
8659          $extension = pathinfo($filename, PATHINFO_EXTENSION);
8660          if ($extension && !empty($mimetypes[$extension])) {
8661              $basename = pathinfo($filename, PATHINFO_FILENAME);
8662              $hash = empty($includehash) ? '' : ' - ' . substr(sha1($basename), 0, 10);
8663              $shortened = core_text::substr($basename, 0, $length - strlen($hash)) . $hash;
8664              $shortened .= '.' . $extension;
8665          } else {
8666              $hash = empty($includehash) ? '' : ' - ' . substr(sha1($filename), 0, 10);
8667              $shortened = core_text::substr($filename, 0, $length - strlen($hash)) . $hash;
8668          }
8669      }
8670      return $shortened;
8671  }
8672  
8673  /**
8674   * Shortens a given array of filenames by removing characters positioned after the ideal string length.
8675   *
8676   * @param array $path The paths to reduce the length.
8677   * @param int $length Ideal string length
8678   * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8679   * @return array $result Shortened paths in array.
8680   */
8681  function shorten_filenames(array $path, $length = MAX_FILENAME_SIZE, $includehash = false) {
8682      $result = null;
8683  
8684      $result = array_reduce($path, function($carry, $singlepath) use ($length, $includehash) {
8685          $carry[] = shorten_filename($singlepath, $length, $includehash);
8686          return $carry;
8687      }, []);
8688  
8689      return $result;
8690  }
8691  
8692  /**
8693   * Given dates in seconds, how many weeks is the date from startdate
8694   * The first week is 1, the second 2 etc ...
8695   *
8696   * @param int $startdate Timestamp for the start date
8697   * @param int $thedate Timestamp for the end date
8698   * @return string
8699   */
8700  function getweek ($startdate, $thedate) {
8701      if ($thedate < $startdate) {
8702          return 0;
8703      }
8704  
8705      return floor(($thedate - $startdate) / WEEKSECS) + 1;
8706  }
8707  
8708  /**
8709   * Returns a randomly generated password of length $maxlen.  inspired by
8710   *
8711   * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and
8712   * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254}
8713   *
8714   * @param int $maxlen  The maximum size of the password being generated.
8715   * @return string
8716   */
8717  function generate_password($maxlen=10) {
8718      global $CFG;
8719  
8720      if (empty($CFG->passwordpolicy)) {
8721          $fillers = PASSWORD_DIGITS;
8722          $wordlist = file($CFG->wordlist);
8723          $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8724          $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8725          $filler1 = $fillers[rand(0, strlen($fillers) - 1)];
8726          $password = $word1 . $filler1 . $word2;
8727      } else {
8728          $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0;
8729          $digits = $CFG->minpassworddigits;
8730          $lower = $CFG->minpasswordlower;
8731          $upper = $CFG->minpasswordupper;
8732          $nonalphanum = $CFG->minpasswordnonalphanum;
8733          $total = $lower + $upper + $digits + $nonalphanum;
8734          // Var minlength should be the greater one of the two ( $minlen and $total ).
8735          $minlen = $minlen < $total ? $total : $minlen;
8736          // Var maxlen can never be smaller than minlen.
8737          $maxlen = $minlen > $maxlen ? $minlen : $maxlen;
8738          $additional = $maxlen - $total;
8739  
8740          // Make sure we have enough characters to fulfill
8741          // complexity requirements.
8742          $passworddigits = PASSWORD_DIGITS;
8743          while ($digits > strlen($passworddigits)) {
8744              $passworddigits .= PASSWORD_DIGITS;
8745          }
8746          $passwordlower = PASSWORD_LOWER;
8747          while ($lower > strlen($passwordlower)) {
8748              $passwordlower .= PASSWORD_LOWER;
8749          }
8750          $passwordupper = PASSWORD_UPPER;
8751          while ($upper > strlen($passwordupper)) {
8752              $passwordupper .= PASSWORD_UPPER;
8753          }
8754          $passwordnonalphanum = PASSWORD_NONALPHANUM;
8755          while ($nonalphanum > strlen($passwordnonalphanum)) {
8756              $passwordnonalphanum .= PASSWORD_NONALPHANUM;
8757          }
8758  
8759          // Now mix and shuffle it all.
8760          $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) .
8761                                   substr(str_shuffle ($passwordupper), 0, $upper) .
8762                                   substr(str_shuffle ($passworddigits), 0, $digits) .
8763                                   substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) .
8764                                   substr(str_shuffle ($passwordlower .
8765                                                       $passwordupper .
8766                                                       $passworddigits .
8767                                                       $passwordnonalphanum), 0 , $additional));
8768      }
8769  
8770      return substr ($password, 0, $maxlen);
8771  }
8772  
8773  /**
8774   * Given a float, prints it nicely.
8775   * Localized floats must not be used in calculations!
8776   *
8777   * The stripzeros feature is intended for making numbers look nicer in small
8778   * areas where it is not necessary to indicate the degree of accuracy by showing
8779   * ending zeros. If you turn it on with $decimalpoints set to 3, for example,
8780   * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'.
8781   *
8782   * @param float $float The float to print
8783   * @param int $decimalpoints The number of decimal places to print. -1 is a special value for auto detect (full precision).
8784   * @param bool $localized use localized decimal separator
8785   * @param bool $stripzeros If true, removes final zeros after decimal point. It will be ignored and the trailing zeros after
8786   *                         the decimal point are always striped if $decimalpoints is -1.
8787   * @return string locale float
8788   */
8789  function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=false) {
8790      if (is_null($float)) {
8791          return '';
8792      }
8793      if ($localized) {
8794          $separator = get_string('decsep', 'langconfig');
8795      } else {
8796          $separator = '.';
8797      }
8798      if ($decimalpoints == -1) {
8799          // The following counts the number of decimals.
8800          // It is safe as both floatval() and round() functions have same behaviour when non-numeric values are provided.
8801          $floatval = floatval($float);
8802          for ($decimalpoints = 0; $floatval != round($float, $decimalpoints); $decimalpoints++);
8803      }
8804  
8805      $result = number_format($float, $decimalpoints, $separator, '');
8806      if ($stripzeros) {
8807          // Remove zeros and final dot if not needed.
8808          $result = preg_replace('~(' . preg_quote($separator, '~') . ')?0+$~', '', $result);
8809      }
8810      return $result;
8811  }
8812  
8813  /**
8814   * Converts locale specific floating point/comma number back to standard PHP float value
8815   * Do NOT try to do any math operations before this conversion on any user submitted floats!
8816   *
8817   * @param string $localefloat locale aware float representation
8818   * @param bool $strict If true, then check the input and return false if it is not a valid number.
8819   * @return mixed float|bool - false or the parsed float.
8820   */
8821  function unformat_float($localefloat, $strict = false) {
8822      $localefloat = trim($localefloat);
8823  
8824      if ($localefloat == '') {
8825          return null;
8826      }
8827  
8828      $localefloat = str_replace(' ', '', $localefloat); // No spaces - those might be used as thousand separators.
8829      $localefloat = str_replace(get_string('decsep', 'langconfig'), '.', $localefloat);
8830  
8831      if ($strict && !is_numeric($localefloat)) {
8832          return false;
8833      }
8834  
8835      return (float)$localefloat;
8836  }
8837  
8838  /**
8839   * Given a simple array, this shuffles it up just like shuffle()
8840   * Unlike PHP's shuffle() this function works on any machine.
8841   *
8842   * @param array $array The array to be rearranged
8843   * @return array
8844   */
8845  function swapshuffle($array) {
8846  
8847      $last = count($array) - 1;
8848      for ($i = 0; $i <= $last; $i++) {
8849          $from = rand(0, $last);
8850          $curr = $array[$i];
8851          $array[$i] = $array[$from];
8852          $array[$from] = $curr;
8853      }
8854      return $array;
8855  }
8856  
8857  /**
8858   * Like {@link swapshuffle()}, but works on associative arrays
8859   *
8860   * @param array $array The associative array to be rearranged
8861   * @return array
8862   */
8863  function swapshuffle_assoc($array) {
8864  
8865      $newarray = array();
8866      $newkeys = swapshuffle(array_keys($array));
8867  
8868      foreach ($newkeys as $newkey) {
8869          $newarray[$newkey] = $array[$newkey];
8870      }
8871      return $newarray;
8872  }
8873  
8874  /**
8875   * Given an arbitrary array, and a number of draws,
8876   * this function returns an array with that amount
8877   * of items.  The indexes are retained.
8878   *
8879   * @todo Finish documenting this function
8880   *
8881   * @param array $array
8882   * @param int $draws
8883   * @return array
8884   */
8885  function draw_rand_array($array, $draws) {
8886  
8887      $return = array();
8888  
8889      $last = count($array);
8890  
8891      if ($draws > $last) {
8892          $draws = $last;
8893      }
8894  
8895      while ($draws > 0) {
8896          $last--;
8897  
8898          $keys = array_keys($array);
8899          $rand = rand(0, $last);
8900  
8901          $return[$keys[$rand]] = $array[$keys[$rand]];
8902          unset($array[$keys[$rand]]);
8903  
8904          $draws--;
8905      }
8906  
8907      return $return;
8908  }
8909  
8910  /**
8911   * Calculate the difference between two microtimes
8912   *
8913   * @param string $a The first Microtime
8914   * @param string $b The second Microtime
8915   * @return string
8916   */
8917  function microtime_diff($a, $b) {
8918      list($adec, $asec) = explode(' ', $a);
8919      list($bdec, $bsec) = explode(' ', $b);
8920      return $bsec - $asec + $bdec - $adec;
8921  }
8922  
8923  /**
8924   * Given a list (eg a,b,c,d,e) this function returns
8925   * an array of 1->a, 2->b, 3->c etc
8926   *
8927   * @param string $list The string to explode into array bits
8928   * @param string $separator The separator used within the list string
8929   * @return array The now assembled array
8930   */
8931  function make_menu_from_list($list, $separator=',') {
8932  
8933      $array = array_reverse(explode($separator, $list), true);
8934      foreach ($array as $key => $item) {
8935          $outarray[$key+1] = trim($item);
8936      }
8937      return $outarray;
8938  }
8939  
8940  /**
8941   * Creates an array that represents all the current grades that
8942   * can be chosen using the given grading type.
8943   *
8944   * Negative numbers
8945   * are scales, zero is no grade, and positive numbers are maximum
8946   * grades.
8947   *
8948   * @todo Finish documenting this function or better deprecated this completely!
8949   *
8950   * @param int $gradingtype
8951   * @return array
8952   */
8953  function make_grades_menu($gradingtype) {
8954      global $DB;
8955  
8956      $grades = array();
8957      if ($gradingtype < 0) {
8958          if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) {
8959              return make_menu_from_list($scale->scale);
8960          }
8961      } else if ($gradingtype > 0) {
8962          for ($i=$gradingtype; $i>=0; $i--) {
8963              $grades[$i] = $i .' / '. $gradingtype;
8964          }
8965          return $grades;
8966      }
8967      return $grades;
8968  }
8969  
8970  /**
8971   * make_unique_id_code
8972   *
8973   * @todo Finish documenting this function
8974   *
8975   * @uses $_SERVER
8976   * @param string $extra Extra string to append to the end of the code
8977   * @return string
8978   */
8979  function make_unique_id_code($extra = '') {
8980  
8981      $hostname = 'unknownhost';
8982      if (!empty($_SERVER['HTTP_HOST'])) {
8983          $hostname = $_SERVER['HTTP_HOST'];
8984      } else if (!empty($_ENV['HTTP_HOST'])) {
8985          $hostname = $_ENV['HTTP_HOST'];
8986      } else if (!empty($_SERVER['SERVER_NAME'])) {
8987          $hostname = $_SERVER['SERVER_NAME'];
8988      } else if (!empty($_ENV['SERVER_NAME'])) {
8989          $hostname = $_ENV['SERVER_NAME'];
8990      }
8991  
8992      $date = gmdate("ymdHis");
8993  
8994      $random =  random_string(6);
8995  
8996      if ($extra) {
8997          return $hostname .'+'. $date .'+'. $random .'+'. $extra;
8998      } else {
8999          return $hostname .'+'. $date .'+'. $random;
9000      }
9001  }
9002  
9003  
9004  /**
9005   * Function to check the passed address is within the passed subnet
9006   *
9007   * The parameter is a comma separated string of subnet definitions.
9008   * Subnet strings can be in one of three formats:
9009   *   1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn          (number of bits in net mask)
9010   *   2: xxx.xxx.xxx.xxx-yyy or  xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx::xxxx-yyyy (a range of IP addresses in the last group)
9011   *   3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.                  (incomplete address, a bit non-technical ;-)
9012   * Code for type 1 modified from user posted comments by mediator at
9013   * {@link http://au.php.net/manual/en/function.ip2long.php}
9014   *
9015   * @param string $addr    The address you are checking
9016   * @param string $subnetstr    The string of subnet addresses
9017   * @param bool $checkallzeros    The state to whether check for 0.0.0.0
9018   * @return bool
9019   */
9020  function address_in_subnet($addr, $subnetstr, $checkallzeros = false) {
9021  
9022      if ($addr == '0.0.0.0' && !$checkallzeros) {
9023          return false;
9024      }
9025      $subnets = explode(',', $subnetstr);
9026      $found = false;
9027      $addr = trim($addr);
9028      $addr = cleanremoteaddr($addr, false); // Normalise.
9029      if ($addr === null) {
9030          return false;
9031      }
9032      $addrparts = explode(':', $addr);
9033  
9034      $ipv6 = strpos($addr, ':');
9035  
9036      foreach ($subnets as $subnet) {
9037          $subnet = trim($subnet);
9038          if ($subnet === '') {
9039              continue;
9040          }
9041  
9042          if (strpos($subnet, '/') !== false) {
9043              // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn.
9044              list($ip, $mask) = explode('/', $subnet);
9045              $mask = trim($mask);
9046              if (!is_number($mask)) {
9047                  continue; // Incorect mask number, eh?
9048              }
9049              $ip = cleanremoteaddr($ip, false); // Normalise.
9050              if ($ip === null) {
9051                  continue;
9052              }
9053              if (strpos($ip, ':') !== false) {
9054                  // IPv6.
9055                  if (!$ipv6) {
9056                      continue;
9057                  }
9058                  if ($mask > 128 or $mask < 0) {
9059                      continue; // Nonsense.
9060                  }
9061                  if ($mask == 0) {
9062                      return true; // Any address.
9063                  }
9064                  if ($mask == 128) {
9065                      if ($ip === $addr) {
9066                          return true;
9067                      }
9068                      continue;
9069                  }
9070                  $ipparts = explode(':', $ip);
9071                  $modulo  = $mask % 16;
9072                  $ipnet   = array_slice($ipparts, 0, ($mask-$modulo)/16);
9073                  $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16);
9074                  if (implode(':', $ipnet) === implode(':', $addrnet)) {
9075                      if ($modulo == 0) {
9076                          return true;
9077                      }
9078                      $pos     = ($mask-$modulo)/16;
9079                      $ipnet   = hexdec($ipparts[$pos]);
9080                      $addrnet = hexdec($addrparts[$pos]);
9081                      $mask    = 0xffff << (16 - $modulo);
9082                      if (($addrnet & $mask) == ($ipnet & $mask)) {
9083                          return true;
9084                      }
9085                  }
9086  
9087              } else {
9088                  // IPv4.
9089                  if ($ipv6) {
9090                      continue;
9091                  }
9092                  if ($mask > 32 or $mask < 0) {
9093                      continue; // Nonsense.
9094                  }
9095                  if ($mask == 0) {
9096                      return true;
9097                  }
9098                  if ($mask == 32) {
9099                      if ($ip === $addr) {
9100                          return true;
9101                      }
9102                      continue;
9103                  }
9104                  $mask = 0xffffffff << (32 - $mask);
9105                  if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
9106                      return true;
9107                  }
9108              }
9109  
9110          } else if (strpos($subnet, '-') !== false) {
9111              // 2: xxx.xxx.xxx.xxx-yyy or  xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx::xxxx-yyyy. A range of IP addresses in the last group.
9112              $parts = explode('-', $subnet);
9113              if (count($parts) != 2) {
9114                  continue;
9115              }
9116  
9117              if (strpos($subnet, ':') !== false) {
9118                  // IPv6.
9119                  if (!$ipv6) {
9120                      continue;
9121                  }
9122                  $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
9123                  if ($ipstart === null) {
9124                      continue;
9125                  }
9126                  $ipparts = explode(':', $ipstart);
9127                  $start = hexdec(array_pop($ipparts));
9128                  $ipparts[] = trim($parts[1]);
9129                  $ipend = cleanremoteaddr(implode(':', $ipparts), false); // Normalise.
9130                  if ($ipend === null) {
9131                      continue;
9132                  }
9133                  $ipparts[7] = '';
9134                  $ipnet = implode(':', $ipparts);
9135                  if (strpos($addr, $ipnet) !== 0) {
9136                      continue;
9137                  }
9138                  $ipparts = explode(':', $ipend);
9139                  $end = hexdec($ipparts[7]);
9140  
9141                  $addrend = hexdec($addrparts[7]);
9142  
9143                  if (($addrend >= $start) and ($addrend <= $end)) {
9144                      return true;
9145                  }
9146  
9147              } else {
9148                  // IPv4.
9149                  if ($ipv6) {
9150                      continue;
9151                  }
9152                  $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
9153                  if ($ipstart === null) {
9154                      continue;
9155                  }
9156                  $ipparts = explode('.', $ipstart);
9157                  $ipparts[3] = trim($parts[1]);
9158                  $ipend = cleanremoteaddr(implode('.', $ipparts), false); // Normalise.
9159                  if ($ipend === null) {
9160                      continue;
9161                  }
9162  
9163                  if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
9164                      return true;
9165                  }
9166              }
9167  
9168          } else {
9169              // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
9170              if (strpos($subnet, ':') !== false) {
9171                  // IPv6.
9172                  if (!$ipv6) {
9173                      continue;
9174                  }
9175                  $parts = explode(':', $subnet);
9176                  $count = count($parts);
9177                  if ($parts[$count-1] === '') {
9178                      unset($parts[$count-1]); // Trim trailing :'s.
9179                      $count--;
9180                      $subnet = implode('.', $parts);
9181                  }
9182                  $isip = cleanremoteaddr($subnet, false); // Normalise.
9183                  if ($isip !== null) {
9184                      if ($isip === $addr) {
9185                          return true;
9186                      }
9187                      continue;
9188                  } else if ($count > 8) {
9189                      continue;
9190                  }
9191                  $zeros = array_fill(0, 8-$count, '0');
9192                  $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16);
9193                  if (address_in_subnet($addr, $subnet)) {
9194                      return true;
9195                  }
9196  
9197              } else {
9198                  // IPv4.
9199                  if ($ipv6) {
9200                      continue;
9201                  }
9202                  $parts = explode('.', $subnet);
9203                  $count = count($parts);
9204                  if ($parts[$count-1] === '') {
9205                      unset($parts[$count-1]); // Trim trailing .
9206                      $count--;
9207                      $subnet = implode('.', $parts);
9208                  }
9209                  if ($count == 4) {
9210                      $subnet = cleanremoteaddr($subnet, false); // Normalise.
9211                      if ($subnet === $addr) {
9212                          return true;
9213                      }
9214                      continue;
9215                  } else if ($count > 4) {
9216                      continue;
9217                  }
9218                  $zeros = array_fill(0, 4-$count, '0');
9219                  $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8);
9220                  if (address_in_subnet($addr, $subnet)) {
9221                      return true;
9222                  }
9223              }
9224          }
9225      }
9226  
9227      return false;
9228  }
9229  
9230  /**
9231   * For outputting debugging info
9232   *
9233   * @param string $string The string to write
9234   * @param string $eol The end of line char(s) to use
9235   * @param string $sleep Period to make the application sleep
9236   *                      This ensures any messages have time to display before redirect
9237   */
9238  function mtrace($string, $eol="\n", $sleep=0) {
9239      global $CFG;
9240  
9241      if (isset($CFG->mtrace_wrapper) && function_exists($CFG->mtrace_wrapper)) {
9242          $fn = $CFG->mtrace_wrapper;
9243          $fn($string, $eol);
9244          return;
9245      } else if (defined('STDOUT') && !PHPUNIT_TEST && !defined('BEHAT_TEST')) {
9246          // We must explicitly call the add_line function here.
9247          // Uses of fwrite to STDOUT are not picked up by ob_start.
9248          if ($output = \core\task\logmanager::add_line("{$string}{$eol}")) {
9249              fwrite(STDOUT, $output);
9250          }
9251      } else {
9252          echo $string . $eol;
9253      }
9254  
9255      // Flush again.
9256      flush();
9257  
9258      // Delay to keep message on user's screen in case of subsequent redirect.
9259      if ($sleep) {
9260          sleep($sleep);
9261      }
9262  }
9263  
9264  /**
9265   * Replace 1 or more slashes or backslashes to 1 slash
9266   *
9267   * @param string $path The path to strip
9268   * @return string the path with double slashes removed
9269   */
9270  function cleardoubleslashes ($path) {
9271      return preg_replace('/(\/|\\\){1,}/', '/', $path);
9272  }
9273  
9274  /**
9275   * Is the current ip in a given list?
9276   *
9277   * @param string $list
9278   * @return bool
9279   */
9280  function remoteip_in_list($list) {
9281      $clientip = getremoteaddr(null);
9282  
9283      if (!$clientip) {
9284          // Ensure access on cli.
9285          return true;
9286      }
9287      return \core\ip_utils::is_ip_in_subnet_list($clientip, $list);
9288  }
9289  
9290  /**
9291   * Returns most reliable client address
9292   *
9293   * @param string $default If an address can't be determined, then return this
9294   * @return string The remote IP address
9295   */
9296  function getremoteaddr($default='0.0.0.0') {
9297      global $CFG;
9298  
9299      if (!isset($CFG->getremoteaddrconf)) {
9300          // This will happen, for example, before just after the upgrade, as the
9301          // user is redirected to the admin screen.
9302          $variablestoskip = GETREMOTEADDR_SKIP_DEFAULT;
9303      } else {
9304          $variablestoskip = $CFG->getremoteaddrconf;
9305      }
9306      if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) {
9307          if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
9308              $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']);
9309              return $address ? $address : $default;
9310          }
9311      }
9312      if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
9313          if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
9314              $forwardedaddresses = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']);
9315  
9316              $forwardedaddresses = array_filter($forwardedaddresses, function($ip) {
9317                  global $CFG;
9318                  return !\core\ip_utils::is_ip_in_subnet_list($ip, $CFG->reverseproxyignore ?? '', ',');
9319              });
9320  
9321              // Multiple proxies can append values to this header including an
9322              // untrusted original request header so we must only trust the last ip.
9323              $address = end($forwardedaddresses);
9324  
9325              if (substr_count($address, ":") > 1) {
9326                  // Remove port and brackets from IPv6.
9327                  if (preg_match("/\[(.*)\]:/", $address, $matches)) {
9328                      $address = $matches[1];
9329                  }
9330              } else {
9331                  // Remove port from IPv4.
9332                  if (substr_count($address, ":") == 1) {
9333                      $parts = explode(":", $address);
9334                      $address = $parts[0];
9335                  }
9336              }
9337  
9338              $address = cleanremoteaddr($address);
9339              return $address ? $address : $default;
9340          }
9341      }
9342      if (!empty($_SERVER['REMOTE_ADDR'])) {
9343          $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']);
9344          return $address ? $address : $default;
9345      } else {
9346          return $default;
9347      }
9348  }
9349  
9350  /**
9351   * Cleans an ip address. Internal addresses are now allowed.
9352   * (Originally local addresses were not allowed.)
9353   *
9354   * @param string $addr IPv4 or IPv6 address
9355   * @param bool $compress use IPv6 address compression
9356   * @return string normalised ip address string, null if error
9357   */
9358  function cleanremoteaddr($addr, $compress=false) {
9359      $addr = trim($addr);
9360  
9361      if (strpos($addr, ':') !== false) {
9362          // Can be only IPv6.
9363          $parts = explode(':', $addr);
9364          $count = count($parts);
9365  
9366          if (strpos($parts[$count-1], '.') !== false) {
9367              // Legacy ipv4 notation.
9368              $last = array_pop($parts);
9369              $ipv4 = cleanremoteaddr($last, true);
9370              if ($ipv4 === null) {
9371                  return null;
9372              }
9373              $bits = explode('.', $ipv4);
9374              $parts[] = dechex($bits[0]).dechex($bits[1]);
9375              $parts[] = dechex($bits[2]).dechex($bits[3]);
9376              $count = count($parts);
9377              $addr = implode(':', $parts);
9378          }
9379  
9380          if ($count < 3 or $count > 8) {
9381              return null; // Severly malformed.
9382          }
9383  
9384          if ($count != 8) {
9385              if (strpos($addr, '::') === false) {
9386                  return null; // Malformed.
9387              }
9388              // Uncompress.
9389              $insertat = array_search('', $parts, true);
9390              $missing = array_fill(0, 1 + 8 - $count, '0');
9391              array_splice($parts, $insertat, 1, $missing);
9392              foreach ($parts as $key => $part) {
9393                  if ($part === '') {
9394                      $parts[$key] = '0';
9395                  }
9396              }
9397          }
9398  
9399          $adr = implode(':', $parts);
9400          if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) {
9401              return null; // Incorrect format - sorry.
9402          }
9403  
9404          // Normalise 0s and case.
9405          $parts = array_map('hexdec', $parts);
9406          $parts = array_map('dechex', $parts);
9407  
9408          $result = implode(':', $parts);
9409  
9410          if (!$compress) {
9411              return $result;
9412          }
9413  
9414          if ($result === '0:0:0:0:0:0:0:0') {
9415              return '::'; // All addresses.
9416          }
9417  
9418          $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1);
9419          if ($compressed !== $result) {
9420              return $compressed;
9421          }
9422  
9423          $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
9424          if ($compressed !== $result) {
9425              return $compressed;
9426          }
9427  
9428          $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
9429          if ($compressed !== $result) {
9430              return $compressed;
9431          }
9432  
9433          return $result;
9434      }
9435  
9436      // First get all things that look like IPv4 addresses.
9437      $parts = array();
9438      if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
9439          return null;
9440      }
9441      unset($parts[0]);
9442  
9443      foreach ($parts as $key => $match) {
9444          if ($match > 255) {
9445              return null;
9446          }
9447          $parts[$key] = (int)$match; // Normalise 0s.
9448      }
9449  
9450      return implode('.', $parts);
9451  }
9452  
9453  
9454  /**
9455   * Is IP address a public address?
9456   *
9457   * @param string $ip The ip to check
9458   * @return bool true if the ip is public
9459   */
9460  function ip_is_public($ip) {
9461      return (bool) filter_var($ip, FILTER_VALIDATE_IP, (FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE));
9462  }
9463  
9464  /**
9465   * This function will make a complete copy of anything it's given,
9466   * regardless of whether it's an object or not.
9467   *
9468   * @param mixed $thing Something you want cloned
9469   * @return mixed What ever it is you passed it
9470   */
9471  function fullclone($thing) {
9472      return unserialize(serialize($thing));
9473  }
9474  
9475  /**
9476   * Used to make sure that $min <= $value <= $max
9477   *
9478   * Make sure that value is between min, and max
9479   *
9480   * @param int $min The minimum value
9481   * @param int $value The value to check
9482   * @param int $max The maximum value
9483   * @return int
9484   */
9485  function bounded_number($min, $value, $max) {
9486      if ($value < $min) {
9487          return $min;
9488      }
9489      if ($value > $max) {
9490          return $max;
9491      }
9492      return $value;
9493  }
9494  
9495  /**
9496   * Check if there is a nested array within the passed array
9497   *
9498   * @param array $array
9499   * @return bool true if there is a nested array false otherwise
9500   */
9501  function array_is_nested($array) {
9502      foreach ($array as $value) {
9503          if (is_array($value)) {
9504              return true;
9505          }
9506      }
9507      return false;
9508  }
9509  
9510  /**
9511   * get_performance_info() pairs up with init_performance_info()
9512   * loaded in setup.php. Returns an array with 'html' and 'txt'
9513   * values ready for use, and each of the individual stats provided
9514   * separately as well.
9515   *
9516   * @return array
9517   */
9518  function get_performance_info() {
9519      global $CFG, $PERF, $DB, $PAGE;
9520  
9521      $info = array();
9522      $info['txt']  = me() . ' '; // Holds log-friendly representation.
9523  
9524      $info['html'] = '';
9525      if (!empty($CFG->themedesignermode)) {
9526          // Attempt to avoid devs debugging peformance issues, when its caused by css building and so on.
9527          $info['html'] .= '<p><strong>Warning: Theme designer mode is enabled.</strong></p>';
9528      }
9529      $info['html'] .= '<ul class="list-unstyled row mx-md-0">';         // Holds userfriendly HTML representation.
9530  
9531      $info['realtime'] = microtime_diff($PERF->starttime, microtime());
9532  
9533      $info['html'] .= '<li class="timeused col-sm-4">'.$info['realtime'].' secs</li> ';
9534      $info['txt'] .= 'time: '.$info['realtime'].'s ';
9535  
9536      // GET/POST (or NULL if $_SERVER['REQUEST_METHOD'] is undefined) is useful for txt logged information.
9537      $info['txt'] .= 'method: ' . ($_SERVER['REQUEST_METHOD'] ?? "NULL") . ' ';
9538  
9539      if (function_exists('memory_get_usage')) {
9540          $info['memory_total'] = memory_get_usage();
9541          $info['memory_growth'] = memory_get_usage() - $PERF->startmemory;
9542          $info['html'] .= '<li class="memoryused col-sm-4">RAM: '.display_size($info['memory_total']).'</li> ';
9543          $info['txt']  .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '.
9544              $info['memory_growth'].'B ('.display_size($info['memory_growth']).') ';
9545      }
9546  
9547      if (function_exists('memory_get_peak_usage')) {
9548          $info['memory_peak'] = memory_get_peak_usage();
9549          $info['html'] .= '<li class="memoryused col-sm-4">RAM peak: '.display_size($info['memory_peak']).'</li> ';
9550          $info['txt']  .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') ';
9551      }
9552  
9553      $info['html'] .= '</ul><ul class="list-unstyled row mx-md-0">';
9554      $inc = get_included_files();
9555      $info['includecount'] = count($inc);
9556      $info['html'] .= '<li class="included col-sm-4">Included '.$info['includecount'].' files</li> ';
9557      $info['txt']  .= 'includecount: '.$info['includecount'].' ';
9558  
9559      if (!empty($CFG->early_install_lang) or empty($PAGE)) {
9560          // We can not track more performance before installation or before PAGE init, sorry.
9561          return $info;
9562      }
9563  
9564      $filtermanager = filter_manager::instance();
9565      if (method_exists($filtermanager, 'get_performance_summary')) {
9566          list($filterinfo, $nicenames) = $filtermanager->get_performance_summary();
9567          $info = array_merge($filterinfo, $info);
9568          foreach ($filterinfo as $key => $value) {
9569              $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9570              $info['txt'] .= "$key: $value ";
9571          }
9572      }
9573  
9574      $stringmanager = get_string_manager();
9575      if (method_exists($stringmanager, 'get_performance_summary')) {
9576          list($filterinfo, $nicenames) = $stringmanager->get_performance_summary();
9577          $info = array_merge($filterinfo, $info);
9578          foreach ($filterinfo as $key => $value) {
9579              $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9580              $info['txt'] .= "$key: $value ";
9581          }
9582      }
9583  
9584      if (!empty($PERF->logwrites)) {
9585          $info['logwrites'] = $PERF->logwrites;
9586          $info['html'] .= '<li class="logwrites col-sm-4">Log DB writes '.$info['logwrites'].'</li> ';
9587          $info['txt'] .= 'logwrites: '.$info['logwrites'].' ';
9588      }
9589  
9590      $info['dbqueries'] = $DB->perf_get_reads().'/'.($DB->perf_get_writes() - $PERF->logwrites);
9591      $info['html'] .= '<li class="dbqueries col-sm-4">DB reads/writes: '.$info['dbqueries'].'</li> ';
9592      $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' ';
9593  
9594      if ($DB->want_read_slave()) {
9595          $info['dbreads_slave'] = $DB->perf_get_reads_slave();
9596          $info['html'] .= '<li class="dbqueries col-sm-4">DB reads from slave: '.$info['dbreads_slave'].'</li> ';
9597          $info['txt'] .= 'db reads from slave: '.$info['dbreads_slave'].' ';
9598      }
9599  
9600      $info['dbtime'] = round($DB->perf_get_queries_time(), 5);
9601      $info['html'] .= '<li class="dbtime col-sm-4">DB queries time: '.$info['dbtime'].' secs</li> ';
9602      $info['txt'] .= 'db queries time: ' . $info['dbtime'] . 's ';
9603  
9604      if (function_exists('posix_times')) {
9605          $ptimes = posix_times();
9606          if (is_array($ptimes)) {
9607              foreach ($ptimes as $key => $val) {
9608                  $info[$key] = $ptimes[$key] -  $PERF->startposixtimes[$key];
9609              }
9610              $info['html'] .= "<li class=\"posixtimes col-sm-4\">ticks: $info[ticks] user: $info[utime]";
9611              $info['html'] .= "sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</li> ";
9612              $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] ";
9613          }
9614      }
9615  
9616      // Grab the load average for the last minute.
9617      // /proc will only work under some linux configurations
9618      // while uptime is there under MacOSX/Darwin and other unices.
9619      if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) {
9620          list($serverload) = explode(' ', $loadavg[0]);
9621          unset($loadavg);
9622      } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) {
9623          if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) {
9624              $serverload = $matches[1];
9625          } else {
9626              trigger_error('Could not parse uptime output!');
9627          }
9628      }
9629      if (!empty($serverload)) {
9630          $info['serverload'] = $serverload;
9631          $info['html'] .= '<li class="serverload col-sm-4">Load average: '.$info['serverload'].'</li> ';
9632          $info['txt'] .= "serverload: {$info['serverload']} ";
9633      }
9634  
9635      // Display size of session if session started.
9636      if ($si = \core\session\manager::get_performance_info()) {
9637          $info['sessionsize'] = $si['size'];
9638          $info['html'] .= "<li class=\"serverload col-sm-4\">" . $si['html'] . "</li>";
9639          $info['txt'] .= $si['txt'];
9640      }
9641  
9642      $info['html'] .= '</ul>';
9643      $html = '';
9644      if ($stats = cache_helper::get_stats()) {
9645  
9646          $table = new html_table();
9647          $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9648          $table->head = ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S'];
9649          $table->data = [];
9650          $table->align = ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right'];
9651  
9652          $text = 'Caches used (hits/misses/sets): ';
9653          $hits = 0;
9654          $misses = 0;
9655          $sets = 0;
9656          $maxstores = 0;
9657  
9658          // We want to align static caches into their own column.
9659          $hasstatic = false;
9660          foreach ($stats as $definition => $details) {
9661              $numstores = count($details['stores']);
9662              $first = key($details['stores']);
9663              if ($first !== cache_store::STATIC_ACCEL) {
9664                  $numstores++; // Add a blank space for the missing static store.
9665              }
9666              $maxstores = max($maxstores, $numstores);
9667          }
9668  
9669          $storec = 0;
9670  
9671          while ($storec++ < ($maxstores - 2)) {
9672              if ($storec == ($maxstores - 2)) {
9673                  $table->head[] = get_string('mappingfinal', 'cache');
9674              } else {
9675                  $table->head[] = "Store $storec";
9676              }
9677              $table->align[] = 'left';
9678              $table->align[] = 'right';
9679              $table->align[] = 'right';
9680              $table->align[] = 'right';
9681              $table->head[] = 'H';
9682              $table->head[] = 'M';
9683              $table->head[] = 'S';
9684          }
9685  
9686          ksort($stats);
9687  
9688          foreach ($stats as $definition => $details) {
9689              switch ($details['mode']) {
9690                  case cache_store::MODE_APPLICATION:
9691                      $modeclass = 'application';
9692                      $mode = ' <span title="application cache">App</span>';
9693                      break;
9694                  case cache_store::MODE_SESSION:
9695                      $modeclass = 'session';
9696                      $mode = ' <span title="session cache">Ses</span>';
9697                      break;
9698                  case cache_store::MODE_REQUEST:
9699                      $modeclass = 'request';
9700                      $mode = ' <span title="request cache">Req</span>';
9701                      break;
9702              }
9703              $row = [$mode, $definition];
9704  
9705              $text .= "$definition {";
9706  
9707              $storec = 0;
9708              foreach ($details['stores'] as $store => $data) {
9709  
9710                  if ($storec == 0 && $store !== cache_store::STATIC_ACCEL) {
9711                      $row[] = '';
9712                      $row[] = '';
9713                      $row[] = '';
9714                      $storec++;
9715                  }
9716  
9717                  $hits   += $data['hits'];
9718                  $misses += $data['misses'];
9719                  $sets   += $data['sets'];
9720                  if ($data['hits'] == 0 and $data['misses'] > 0) {
9721                      $cachestoreclass = 'nohits bg-danger';
9722                  } else if ($data['hits'] < $data['misses']) {
9723                      $cachestoreclass = 'lowhits bg-warning text-dark';
9724                  } else {
9725                      $cachestoreclass = 'hihits';
9726                  }
9727                  $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
9728                  $cell = new html_table_cell($store);
9729                  $cell->attributes = ['class' => $cachestoreclass];
9730                  $row[] = $cell;
9731                  $cell = new html_table_cell($data['hits']);
9732                  $cell->attributes = ['class' => $cachestoreclass];
9733                  $row[] = $cell;
9734                  $cell = new html_table_cell($data['misses']);
9735                  $cell->attributes = ['class' => $cachestoreclass];
9736                  $row[] = $cell;
9737  
9738                  if ($store !== cache_store::STATIC_ACCEL) {
9739                      // The static cache is never set.
9740                      $cell = new html_table_cell($data['sets']);
9741                      $cell->attributes = ['class' => $cachestoreclass];
9742                      $row[] = $cell;
9743                  }
9744                  $storec++;
9745              }
9746              while ($storec++ < $maxstores) {
9747                  $row[] = '';
9748                  $row[] = '';
9749                  $row[] = '';
9750                  $row[] = '';
9751              }
9752              $text .= '} ';
9753  
9754              $table->data[] = $row;
9755          }
9756  
9757          $html .= html_writer::table($table);
9758  
9759          // Now lets also show sub totals for each cache store.
9760          $storetotals = [];
9761          $storetotal = ['hits' => 0, 'misses' => 0, 'sets' => 0];
9762          foreach ($stats as $definition => $details) {
9763              foreach ($details['stores'] as $store => $data) {
9764                  if (!array_key_exists($store, $storetotals)) {
9765                      $storetotals[$store] = ['hits' => 0, 'misses' => 0, 'sets' => 0];
9766                  }
9767                  $storetotals[$store]['class']   = $data['class'];
9768                  $storetotals[$store]['hits']   += $data['hits'];
9769                  $storetotals[$store]['misses'] += $data['misses'];
9770                  $storetotals[$store]['sets']   += $data['sets'];
9771                  $storetotal['hits']   += $data['hits'];
9772                  $storetotal['misses'] += $data['misses'];
9773                  $storetotal['sets']   += $data['sets'];
9774              }
9775          }
9776  
9777          $table = new html_table();
9778          $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9779          $table->head = [get_string('storename', 'cache'), get_string('type_cachestore', 'plugin'), 'H', 'M', 'S'];
9780          $table->data = [];
9781          $table->align = ['left', 'left', 'right', 'right', 'right'];
9782  
9783          ksort($storetotals);
9784  
9785          foreach ($storetotals as $store => $data) {
9786              $row = [];
9787              if ($data['hits'] == 0 and $data['misses'] > 0) {
9788                  $cachestoreclass = 'nohits bg-danger';
9789              } else if ($data['hits'] < $data['misses']) {
9790                  $cachestoreclass = 'lowhits bg-warning text-dark';
9791              } else {
9792                  $cachestoreclass = 'hihits';
9793              }
9794              $cell = new html_table_cell($store);
9795              $cell->attributes = ['class' => $cachestoreclass];
9796              $row[] = $cell;
9797              $cell = new html_table_cell($data['class']);
9798              $cell->attributes = ['class' => $cachestoreclass];
9799              $row[] = $cell;
9800              $cell = new html_table_cell($data['hits']);
9801              $cell->attributes = ['class' => $cachestoreclass];
9802              $row[] = $cell;
9803              $cell = new html_table_cell($data['misses']);
9804              $cell->attributes = ['class' => $cachestoreclass];
9805              $row[] = $cell;
9806              $cell = new html_table_cell($data['sets']);
9807              $cell->attributes = ['class' => $cachestoreclass];
9808              $row[] = $cell;
9809              $table->data[] = $row;
9810          }
9811          $row = [
9812              get_string('total'),
9813              '',
9814              $storetotal['hits'],
9815              $storetotal['misses'],
9816              $storetotal['sets'],
9817          ];
9818          $table->data[] = $row;
9819  
9820          $html .= html_writer::table($table);
9821  
9822          $info['cachesused'] = "$hits / $misses / $sets";
9823          $info['html'] .= $html;
9824          $info['txt'] .= $text.'. ';
9825      } else {
9826          $info['cachesused'] = '0 / 0 / 0';
9827          $info['html'] .= '<div class="cachesused">Caches used (hits/misses/sets): 0/0/0</div>';
9828          $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 ';
9829      }
9830  
9831      $info['html'] = '<div class="performanceinfo siteinfo container-fluid px-md-0 overflow-auto mt-3">'.$info['html'].'</div>';
9832      return $info;
9833  }
9834  
9835  /**
9836   * Delete directory or only its content
9837   *
9838   * @param string $dir directory path
9839   * @param bool $contentonly
9840   * @return bool success, true also if dir does not exist
9841   */
9842  function remove_dir($dir, $contentonly=false) {
9843      if (!file_exists($dir)) {
9844          // Nothing to do.
9845          return true;
9846      }
9847      if (!$handle = opendir($dir)) {
9848          return false;
9849      }
9850      $result = true;
9851      while (false!==($item = readdir($handle))) {
9852          if ($item != '.' && $item != '..') {
9853              if (is_dir($dir.'/'.$item)) {
9854                  $result = remove_dir($dir.'/'.$item) && $result;
9855              } else {
9856                  $result = unlink($dir.'/'.$item) && $result;
9857              }
9858          }
9859      }
9860      closedir($handle);
9861      if ($contentonly) {
9862          clearstatcache(); // Make sure file stat cache is properly invalidated.
9863          return $result;
9864      }
9865      $result = rmdir($dir); // If anything left the result will be false, no need for && $result.
9866      clearstatcache(); // Make sure file stat cache is properly invalidated.
9867      return $result;
9868  }
9869  
9870  /**
9871   * Detect if an object or a class contains a given property
9872   * will take an actual object or the name of a class
9873   *
9874   * @param mix $obj Name of class or real object to test
9875   * @param string $property name of property to find
9876   * @return bool true if property exists
9877   */
9878  function object_property_exists( $obj, $property ) {
9879      if (is_string( $obj )) {
9880          $properties = get_class_vars( $obj );
9881      } else {
9882          $properties = get_object_vars( $obj );
9883      }
9884      return array_key_exists( $property, $properties );
9885  }
9886  
9887  /**
9888   * Converts an object into an associative array
9889   *
9890   * This function converts an object into an associative array by iterating
9891   * over its public properties. Because this function uses the foreach
9892   * construct, Iterators are respected. It works recursively on arrays of objects.
9893   * Arrays and simple values are returned as is.
9894   *
9895   * If class has magic properties, it can implement IteratorAggregate
9896   * and return all available properties in getIterator()
9897   *
9898   * @param mixed $var
9899   * @return array
9900   */
9901  function convert_to_array($var) {
9902      $result = array();
9903  
9904      // Loop over elements/properties.
9905      foreach ($var as $key => $value) {
9906          // Recursively convert objects.
9907          if (is_object($value) || is_array($value)) {
9908              $result[$key] = convert_to_array($value);
9909          } else {
9910              // Simple values are untouched.
9911              $result[$key] = $value;
9912          }
9913      }
9914      return $result;
9915  }
9916  
9917  /**
9918   * Detect a custom script replacement in the data directory that will
9919   * replace an existing moodle script
9920   *
9921   * @return string|bool full path name if a custom script exists, false if no custom script exists
9922   */
9923  function custom_script_path() {
9924      global $CFG, $SCRIPT;
9925  
9926      if ($SCRIPT === null) {
9927          // Probably some weird external script.
9928          return false;
9929      }
9930  
9931      $scriptpath = $CFG->customscripts . $SCRIPT;
9932  
9933      // Check the custom script exists.
9934      if (file_exists($scriptpath) and is_file($scriptpath)) {
9935          return $scriptpath;
9936      } else {
9937          return false;
9938      }
9939  }
9940  
9941  /**
9942   * Returns whether or not the user object is a remote MNET user. This function
9943   * is in moodlelib because it does not rely on loading any of the MNET code.
9944   *
9945   * @param object $user A valid user object
9946   * @return bool        True if the user is from a remote Moodle.
9947   */
9948  function is_mnet_remote_user($user) {
9949      global $CFG;
9950  
9951      if (!isset($CFG->mnet_localhost_id)) {
9952          include_once($CFG->dirroot . '/mnet/lib.php');
9953          $env = new mnet_environment();
9954          $env->init();
9955          unset($env);
9956      }
9957  
9958      return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id);
9959  }
9960  
9961  /**
9962   * This function will search for browser prefereed languages, setting Moodle
9963   * to use the best one available if $SESSION->lang is undefined
9964   */
9965  function setup_lang_from_browser() {
9966      global $CFG, $SESSION, $USER;
9967  
9968      if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) {
9969          // Lang is defined in session or user profile, nothing to do.
9970          return;
9971      }
9972  
9973      if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do.
9974          return;
9975      }
9976  
9977      // Extract and clean langs from headers.
9978      $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
9979      $rawlangs = str_replace('-', '_', $rawlangs);         // We are using underscores.
9980      $rawlangs = explode(',', $rawlangs);                  // Convert to array.
9981      $langs = array();
9982  
9983      $order = 1.0;
9984      foreach ($rawlangs as $lang) {
9985          if (strpos($lang, ';') === false) {
9986              $langs[(string)$order] = $lang;
9987              $order = $order-0.01;
9988          } else {
9989              $parts = explode(';', $lang);
9990              $pos = strpos($parts[1], '=');
9991              $langs[substr($parts[1], $pos+1)] = $parts[0];
9992          }
9993      }
9994      krsort($langs, SORT_NUMERIC);
9995  
9996      // Look for such langs under standard locations.
9997      foreach ($langs as $lang) {
9998          // Clean it properly for include.
9999          $lang = strtolower(clean_param($lang, PARAM_SAFEDIR));
10000          if (get_string_manager()->translation_exists($lang, false)) {
10001              // Lang exists, set it in session.
10002              $SESSION->lang = $lang;
10003              // We have finished. Go out.
10004              break;
10005          }
10006      }
10007      return;
10008  }
10009  
10010  /**
10011   * Check if $url matches anything in proxybypass list
10012   *
10013   * Any errors just result in the proxy being used (least bad)
10014   *
10015   * @param string $url url to check
10016   * @return boolean true if we should bypass the proxy
10017   */
10018  function is_proxybypass( $url ) {
10019      global $CFG;
10020  
10021      // Sanity check.
10022      if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) {
10023          return false;
10024      }
10025  
10026      // Get the host part out of the url.
10027      if (!$host = parse_url( $url, PHP_URL_HOST )) {
10028          return false;
10029      }
10030  
10031      // Get the possible bypass hosts into an array.
10032      $matches = explode( ',', $CFG->proxybypass );
10033  
10034      // Check for a exact match on the IP or in the domains.
10035      $isdomaininallowedlist = \core\ip_utils::is_domain_in_allowed_list($host, $matches);
10036      $isipinsubnetlist = \core\ip_utils::is_ip_in_subnet_list($host, $CFG->proxybypass, ',');
10037  
10038      if ($isdomaininallowedlist || $isipinsubnetlist) {
10039          return true;
10040      }
10041  
10042      // Nothing matched.
10043      return false;
10044  }
10045  
10046  /**
10047   * Check if the passed navigation is of the new style
10048   *
10049   * @param mixed $navigation
10050   * @return bool true for yes false for no
10051   */
10052  function is_newnav($navigation) {
10053      if (is_array($navigation) && !empty($navigation['newnav'])) {
10054          return true;
10055      } else {
10056          return false;
10057      }
10058  }
10059  
10060  /**
10061   * Checks whether the given variable name is defined as a variable within the given object.
10062   *
10063   * This will NOT work with stdClass objects, which have no class variables.
10064   *
10065   * @param string $var The variable name
10066   * @param object $object The object to check
10067   * @return boolean
10068   */
10069  function in_object_vars($var, $object) {
10070      $classvars = get_class_vars(get_class($object));
10071      $classvars = array_keys($classvars);
10072      return in_array($var, $classvars);
10073  }
10074  
10075  /**
10076   * Returns an array without repeated objects.
10077   * This function is similar to array_unique, but for arrays that have objects as values
10078   *
10079   * @param array $array
10080   * @param bool $keepkeyassoc
10081   * @return array
10082   */
10083  function object_array_unique($array, $keepkeyassoc = true) {
10084      $duplicatekeys = array();
10085      $tmp         = array();
10086  
10087      foreach ($array as $key => $val) {
10088          // Convert objects to arrays, in_array() does not support objects.
10089          if (is_object($val)) {
10090              $val = (array)$val;
10091          }
10092  
10093          if (!in_array($val, $tmp)) {
10094              $tmp[] = $val;
10095          } else {
10096              $duplicatekeys[] = $key;
10097          }
10098      }
10099  
10100      foreach ($duplicatekeys as $key) {
10101          unset($array[$key]);
10102      }
10103  
10104      return $keepkeyassoc ? $array : array_values($array);
10105  }
10106  
10107  /**
10108   * Is a userid the primary administrator?
10109   *
10110   * @param int $userid int id of user to check
10111   * @return boolean
10112   */
10113  function is_primary_admin($userid) {
10114      $primaryadmin =  get_admin();
10115  
10116      if ($userid == $primaryadmin->id) {
10117          return true;
10118      } else {
10119          return false;
10120      }
10121  }
10122  
10123  /**
10124   * Returns the site identifier
10125   *
10126   * @return string $CFG->siteidentifier, first making sure it is properly initialised.
10127   */
10128  function get_site_identifier() {
10129      global $CFG;
10130      // Check to see if it is missing. If so, initialise it.
10131      if (empty($CFG->siteidentifier)) {
10132          set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']);
10133      }
10134      // Return it.
10135      return $CFG->siteidentifier;
10136  }
10137  
10138  /**
10139   * Check whether the given password has no more than the specified
10140   * number of consecutive identical characters.
10141   *
10142   * @param string $password   password to be checked against the password policy
10143   * @param integer $maxchars  maximum number of consecutive identical characters
10144   * @return bool
10145   */
10146  function check_consecutive_identical_characters($password, $maxchars) {
10147  
10148      if ($maxchars < 1) {
10149          return true; // Zero 0 is to disable this check.
10150      }
10151      if (strlen($password) <= $maxchars) {
10152          return true; // Too short to fail this test.
10153      }
10154  
10155      $previouschar = '';
10156      $consecutivecount = 1;
10157      foreach (str_split($password) as $char) {
10158          if ($char != $previouschar) {
10159              $consecutivecount = 1;
10160          } else {
10161              $consecutivecount++;
10162              if ($consecutivecount > $maxchars) {
10163                  return false; // Check failed already.
10164              }
10165          }
10166  
10167          $previouschar = $char;
10168      }
10169  
10170      return true;
10171  }
10172  
10173  /**
10174   * Helper function to do partial function binding.
10175   * so we can use it for preg_replace_callback, for example
10176   * this works with php functions, user functions, static methods and class methods
10177   * it returns you a callback that you can pass on like so:
10178   *
10179   * $callback = partial('somefunction', $arg1, $arg2);
10180   *     or
10181   * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
10182   *     or even
10183   * $obj = new someclass();
10184   * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2);
10185   *
10186   * and then the arguments that are passed through at calltime are appended to the argument list.
10187   *
10188   * @param mixed $function a php callback
10189   * @param mixed $arg1,... $argv arguments to partially bind with
10190   * @return array Array callback
10191   */
10192  function partial() {
10193      if (!class_exists('partial')) {
10194          /**
10195           * Used to manage function binding.
10196           * @copyright  2009 Penny Leach
10197           * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10198           */
10199          class partial{
10200              /** @var array */
10201              public $values = array();
10202              /** @var string The function to call as a callback. */
10203              public $func;
10204              /**
10205               * Constructor
10206               * @param string $func
10207               * @param array $args
10208               */
10209              public function __construct($func, $args) {
10210                  $this->values = $args;
10211                  $this->func = $func;
10212              }
10213              /**
10214               * Calls the callback function.
10215               * @return mixed
10216               */
10217              public function method() {
10218                  $args = func_get_args();
10219                  return call_user_func_array($this->func, array_merge($this->values, $args));
10220              }
10221          }
10222      }
10223      $args = func_get_args();
10224      $func = array_shift($args);
10225      $p = new partial($func, $args);
10226      return array($p, 'method');
10227  }
10228  
10229  /**
10230   * helper function to load up and initialise the mnet environment
10231   * this must be called before you use mnet functions.
10232   *
10233   * @return mnet_environment the equivalent of old $MNET global
10234   */
10235  function get_mnet_environment() {
10236      global $CFG;
10237      require_once($CFG->dirroot . '/mnet/lib.php');
10238      static $instance = null;
10239      if (empty($instance)) {
10240          $instance = new mnet_environment();
10241          $instance->init();
10242      }
10243      return $instance;
10244  }
10245  
10246  /**
10247   * during xmlrpc server code execution, any code wishing to access
10248   * information about the remote peer must use this to get it.
10249   *
10250   * @return mnet_remote_client the equivalent of old $MNETREMOTE_CLIENT global
10251   */
10252  function get_mnet_remote_client() {
10253      if (!defined('MNET_SERVER')) {
10254          debugging(get_string('notinxmlrpcserver', 'mnet'));
10255          return false;
10256      }
10257      global $MNET_REMOTE_CLIENT;
10258      if (isset($MNET_REMOTE_CLIENT)) {
10259          return $MNET_REMOTE_CLIENT;
10260      }
10261      return false;
10262  }
10263  
10264  /**
10265   * during the xmlrpc server code execution, this will be called
10266   * to setup the object returned by {@link get_mnet_remote_client}
10267   *
10268   * @param mnet_remote_client $client the client to set up
10269   * @throws moodle_exception
10270   */
10271  function set_mnet_remote_client($client) {
10272      if (!defined('MNET_SERVER')) {
10273          throw new moodle_exception('notinxmlrpcserver', 'mnet');
10274      }
10275      global $MNET_REMOTE_CLIENT;
10276      $MNET_REMOTE_CLIENT = $client;
10277  }
10278  
10279  /**
10280   * return the jump url for a given remote user
10281   * this is used for rewriting forum post links in emails, etc
10282   *
10283   * @param stdclass $user the user to get the idp url for
10284   */
10285  function mnet_get_idp_jump_url($user) {
10286      global $CFG;
10287  
10288      static $mnetjumps = array();
10289      if (!array_key_exists($user->mnethostid, $mnetjumps)) {
10290          $idp = mnet_get_peer_host($user->mnethostid);
10291          $idpjumppath = mnet_get_app_jumppath($idp->applicationid);
10292          $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl=';
10293      }
10294      return $mnetjumps[$user->mnethostid];
10295  }
10296  
10297  /**
10298   * Gets the homepage to use for the current user
10299   *
10300   * @return int One of HOMEPAGE_*
10301   */
10302  function get_home_page() {
10303      global $CFG;
10304  
10305      if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) {
10306          if ($CFG->defaulthomepage == HOMEPAGE_MY) {
10307              return HOMEPAGE_MY;
10308          } else {
10309              return (int)get_user_preferences('user_home_page_preference', HOMEPAGE_MY);
10310          }
10311      }
10312      return HOMEPAGE_SITE;
10313  }
10314  
10315  /**
10316   * Gets the name of a course to be displayed when showing a list of courses.
10317   * By default this is just $course->fullname but user can configure it. The
10318   * result of this function should be passed through print_string.
10319   * @param stdClass|core_course_list_element $course Moodle course object
10320   * @return string Display name of course (either fullname or short + fullname)
10321   */
10322  function get_course_display_name_for_list($course) {
10323      global $CFG;
10324      if (!empty($CFG->courselistshortnames)) {
10325          if (!($course instanceof stdClass)) {
10326              $course = (object)convert_to_array($course);
10327          }
10328          return get_string('courseextendednamedisplay', '', $course);
10329      } else {
10330          return $course->fullname;
10331      }
10332  }
10333  
10334  /**
10335   * Safe analogue of unserialize() that can only parse arrays
10336   *
10337   * Arrays may contain only integers or strings as both keys and values. Nested arrays are allowed.
10338   *
10339   * @param string $expression
10340   * @return array|bool either parsed array or false if parsing was impossible.
10341   */
10342  function unserialize_array($expression) {
10343  
10344      // Check the expression is an array.
10345      if (!preg_match('/^a:(\d+):/', $expression)) {
10346          return false;
10347      }
10348  
10349      $values = (array) unserialize_object($expression);
10350  
10351      // Callback that returns true if the given value is an unserialized object, executes recursively.
10352      $invalidvaluecallback = static function($value) use (&$invalidvaluecallback): bool {
10353          if (is_array($value)) {
10354              return (bool) array_filter($value, $invalidvaluecallback);
10355          }
10356          return ($value instanceof stdClass) || ($value instanceof __PHP_Incomplete_Class);
10357      };
10358  
10359      // Iterate over the result to ensure there are no stray objects.
10360      if (array_filter($values, $invalidvaluecallback)) {
10361          return false;
10362      }
10363  
10364      return $values;
10365  }
10366  
10367  /**
10368   * Safe method for unserializing given input that is expected to contain only a serialized instance of an stdClass object
10369   *
10370   * If any class type other than stdClass is included in the input string, it will not be instantiated and will be cast to an
10371   * stdClass object. The initial cast to array, then back to object is to ensure we are always returning the correct type,
10372   * otherwise we would return an instances of {@see __PHP_Incomplete_class} for malformed strings
10373   *
10374   * @param string $input
10375   * @return stdClass
10376   */
10377  function unserialize_object(string $input): stdClass {
10378      $instance = (array) unserialize($input, ['allowed_classes' => [stdClass::class]]);
10379      return (object) $instance;
10380  }
10381  
10382  /**
10383   * The lang_string class
10384   *
10385   * This special class is used to create an object representation of a string request.
10386   * It is special because processing doesn't occur until the object is first used.
10387   * The class was created especially to aid performance in areas where strings were
10388   * required to be generated but were not necessarily used.
10389   * As an example the admin tree when generated uses over 1500 strings, of which
10390   * normally only 1/3 are ever actually printed at any time.
10391   * The performance advantage is achieved by not actually processing strings that
10392   * arn't being used, as such reducing the processing required for the page.
10393   *
10394   * How to use the lang_string class?
10395   *     There are two methods of using the lang_string class, first through the
10396   *     forth argument of the get_string function, and secondly directly.
10397   *     The following are examples of both.
10398   * 1. Through get_string calls e.g.
10399   *     $string = get_string($identifier, $component, $a, true);
10400   *     $string = get_string('yes', 'moodle', null, true);
10401   * 2. Direct instantiation
10402   *     $string = new lang_string($identifier, $component, $a, $lang);
10403   *     $string = new lang_string('yes');
10404   *
10405   * How do I use a lang_string object?
10406   *     The lang_string object makes use of a magic __toString method so that you
10407   *     are able to use the object exactly as you would use a string in most cases.
10408   *     This means you are able to collect it into a variable and then directly
10409   *     echo it, or concatenate it into another string, or similar.
10410   *     The other thing you can do is manually get the string by calling the
10411   *     lang_strings out method e.g.
10412   *         $string = new lang_string('yes');
10413   *         $string->out();
10414   *     Also worth noting is that the out method can take one argument, $lang which
10415   *     allows the developer to change the language on the fly.
10416   *
10417   * When should I use a lang_string object?
10418   *     The lang_string object is designed to be used in any situation where a
10419   *     string may not be needed, but needs to be generated.
10420   *     The admin tree is a good example of where lang_string objects should be
10421   *     used.
10422   *     A more practical example would be any class that requries strings that may
10423   *     not be printed (after all classes get renderer by renderers and who knows
10424   *     what they will do ;))
10425   *
10426   * When should I not use a lang_string object?
10427   *     Don't use lang_strings when you are going to use a string immediately.
10428   *     There is no need as it will be processed immediately and there will be no
10429   *     advantage, and in fact perhaps a negative hit as a class has to be
10430   *     instantiated for a lang_string object, however get_string won't require
10431   *     that.
10432   *
10433   * Limitations:
10434   * 1. You cannot use a lang_string object as an array offset. Doing so will
10435   *     result in PHP throwing an error. (You can use it as an object property!)
10436   *
10437   * @package    core
10438   * @category   string
10439   * @copyright  2011 Sam Hemelryk
10440   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10441   */
10442  class lang_string {
10443  
10444      /** @var string The strings identifier */
10445      protected $identifier;
10446      /** @var string The strings component. Default '' */
10447      protected $component = '';
10448      /** @var array|stdClass Any arguments required for the string. Default null */
10449      protected $a = null;
10450      /** @var string The language to use when processing the string. Default null */
10451      protected $lang = null;
10452  
10453      /** @var string The processed string (once processed) */
10454      protected $string = null;
10455  
10456      /**
10457       * A special boolean. If set to true then the object has been woken up and
10458       * cannot be regenerated. If this is set then $this->string MUST be used.
10459       * @var bool
10460       */
10461      protected $forcedstring = false;
10462  
10463      /**
10464       * Constructs a lang_string object
10465       *
10466       * This function should do as little processing as possible to ensure the best
10467       * performance for strings that won't be used.
10468       *
10469       * @param string $identifier The strings identifier
10470       * @param string $component The strings component
10471       * @param stdClass|array $a Any arguments the string requires
10472       * @param string $lang The language to use when processing the string.
10473       * @throws coding_exception
10474       */
10475      public function __construct($identifier, $component = '', $a = null, $lang = null) {
10476          if (empty($component)) {
10477              $component = 'moodle';
10478          }
10479  
10480          $this->identifier = $identifier;
10481          $this->component = $component;
10482          $this->lang = $lang;
10483  
10484          // We MUST duplicate $a to ensure that it if it changes by reference those
10485          // changes are not carried across.
10486          // To do this we always ensure $a or its properties/values are strings
10487          // and that any properties/values that arn't convertable are forgotten.
10488          if ($a !== null) {
10489              if (is_scalar($a)) {
10490                  $this->a = $a;
10491              } else if ($a instanceof lang_string) {
10492                  $this->a = $a->out();
10493              } else if (is_object($a) or is_array($a)) {
10494                  $a = (array)$a;
10495                  $this->a = array();
10496                  foreach ($a as $key => $value) {
10497                      // Make sure conversion errors don't get displayed (results in '').
10498                      if (is_array($value)) {
10499                          $this->a[$key] = '';
10500                      } else if (is_object($value)) {
10501                          if (method_exists($value, '__toString')) {
10502                              $this->a[$key] = $value->__toString();
10503                          } else {
10504                              $this->a[$key] = '';
10505                          }
10506                      } else {
10507                          $this->a[$key] = (string)$value;
10508                      }
10509                  }
10510              }
10511          }
10512  
10513          if (debugging(false, DEBUG_DEVELOPER)) {
10514              if (clean_param($this->identifier, PARAM_STRINGID) == '') {
10515                  throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition');
10516              }
10517              if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') {
10518                  throw new coding_exception('Invalid string compontent. Please check your string definition');
10519              }
10520              if (!get_string_manager()->string_exists($this->identifier, $this->component)) {
10521                  debugging('String does not exist. Please check your string definition for '.$this->identifier.'/'.$this->component, DEBUG_DEVELOPER);
10522              }
10523          }
10524      }
10525  
10526      /**
10527       * Processes the string.
10528       *
10529       * This function actually processes the string, stores it in the string property
10530       * and then returns it.
10531       * You will notice that this function is VERY similar to the get_string method.
10532       * That is because it is pretty much doing the same thing.
10533       * However as this function is an upgrade it isn't as tolerant to backwards
10534       * compatibility.
10535       *
10536       * @return string
10537       * @throws coding_exception
10538       */
10539      protected function get_string() {
10540          global $CFG;
10541  
10542          // Check if we need to process the string.
10543          if ($this->string === null) {
10544              // Check the quality of the identifier.
10545              if ($CFG->debugdeveloper && clean_param($this->identifier, PARAM_STRINGID) === '') {
10546                  throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition', DEBUG_DEVELOPER);
10547              }
10548  
10549              // Process the string.
10550              $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang);
10551              // Debugging feature lets you display string identifier and component.
10552              if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
10553                  $this->string .= ' {' . $this->identifier . '/' . $this->component . '}';
10554              }
10555          }
10556          // Return the string.
10557          return $this->string;
10558      }
10559  
10560      /**
10561       * Returns the string
10562       *
10563       * @param string $lang The langauge to use when processing the string
10564       * @return string
10565       */
10566      public function out($lang = null) {
10567          if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) {
10568              if ($this->forcedstring) {
10569                  debugging('lang_string objects that have been used cannot be printed in another language. ('.$this->lang.' used)', DEBUG_DEVELOPER);
10570                  return $this->get_string();
10571              }
10572              $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang);
10573              return $translatedstring->out();
10574          }
10575          return $this->get_string();
10576      }
10577  
10578      /**
10579       * Magic __toString method for printing a string
10580       *
10581       * @return string
10582       */
10583      public function __toString() {
10584          return $this->get_string();
10585      }
10586  
10587      /**
10588       * Magic __set_state method used for var_export
10589       *
10590       * @return string
10591       */
10592      public function __set_state() {
10593          return $this->get_string();
10594      }
10595  
10596      /**
10597       * Prepares the lang_string for sleep and stores only the forcedstring and
10598       * string properties... the string cannot be regenerated so we need to ensure
10599       * it is generated for this.
10600       *
10601       * @return string
10602       */
10603      public function __sleep() {
10604          $this->get_string();
10605          $this->forcedstring = true;
10606          return array('forcedstring', 'string', 'lang');
10607      }
10608  
10609      /**
10610       * Returns the identifier.
10611       *
10612       * @return string
10613       */
10614      public function get_identifier() {
10615          return $this->identifier;
10616      }
10617  
10618      /**
10619       * Returns the component.
10620       *
10621       * @return string
10622       */
10623      public function get_component() {
10624          return $this->component;
10625      }
10626  }
10627  
10628  /**
10629   * Get human readable name describing the given callable.
10630   *
10631   * This performs syntax check only to see if the given param looks like a valid function, method or closure.
10632   * It does not check if the callable actually exists.
10633   *
10634   * @param callable|string|array $callable
10635   * @return string|bool Human readable name of callable, or false if not a valid callable.
10636   */
10637  function get_callable_name($callable) {
10638  
10639      if (!is_callable($callable, true, $name)) {
10640          return false;
10641  
10642      } else {
10643          return $name;
10644      }
10645  }
10646  
10647  /**
10648   * Tries to guess if $CFG->wwwroot is publicly accessible or not.
10649   * Never put your faith on this function and rely on its accuracy as there might be false positives.
10650   * It just performs some simple checks, and mainly is used for places where we want to hide some options
10651   * such as site registration when $CFG->wwwroot is not publicly accessible.
10652   * Good thing is there is no false negative.
10653   * Note that it's possible to force the result of this check by specifying $CFG->site_is_public in config.php
10654   *
10655   * @return bool
10656   */
10657  function site_is_public() {
10658      global $CFG;
10659  
10660      // Return early if site admin has forced this setting.
10661      if (isset($CFG->site_is_public)) {
10662          return (bool)$CFG->site_is_public;
10663      }
10664  
10665      $host = parse_url($CFG->wwwroot, PHP_URL_HOST);
10666  
10667      if ($host === 'localhost' || preg_match('|^127\.\d+\.\d+\.\d+$|', $host)) {
10668          $ispublic = false;
10669      } else if (\core\ip_utils::is_ip_address($host) && !ip_is_public($host)) {
10670          $ispublic = false;
10671      } else if (($address = \core\ip_utils::get_ip_address($host)) && !ip_is_public($address)) {
10672          $ispublic = false;
10673      } else {
10674          $ispublic = true;
10675      }
10676  
10677      return $ispublic;
10678  }