Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 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 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

   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, etc.
 210   */
 211  define('PARAM_SAFEPATH',  'safepath');
 212  
 213  /**
 214   * PARAM_SEQUENCE - expects a sequence of numbers like 8 to 1,5,6,4,6,8,9.  Numbers and comma only.
 215   */
 216  define('PARAM_SEQUENCE',  'sequence');
 217  
 218  /**
 219   * PARAM_TAG - one tag (interests, blogs, etc.) - mostly international characters and space, <> not supported
 220   */
 221  define('PARAM_TAG',   'tag');
 222  
 223  /**
 224   * PARAM_TAGLIST - list of tags separated by commas (interests, blogs, etc.)
 225   */
 226  define('PARAM_TAGLIST',   'taglist');
 227  
 228  /**
 229   * PARAM_TEXT - general plain text compatible with multilang filter, no other html tags. Please note '<', or '>' are allowed here.
 230   */
 231  define('PARAM_TEXT',  'text');
 232  
 233  /**
 234   * PARAM_THEME - Checks to see if the string is a valid theme name in the current site
 235   */
 236  define('PARAM_THEME',  'theme');
 237  
 238  /**
 239   * PARAM_URL - expected properly formatted URL. Please note that domain part is required, http://localhost/ is not accepted but
 240   * http://localhost.localdomain/ is ok.
 241   */
 242  define('PARAM_URL',      'url');
 243  
 244  /**
 245   * PARAM_USERNAME - Clean username to only contains allowed characters. This is to be used ONLY when manually creating user
 246   * accounts, do NOT use when syncing with external systems!!
 247   */
 248  define('PARAM_USERNAME',    'username');
 249  
 250  /**
 251   * PARAM_STRINGID - used to check if the given string is valid string identifier for get_string()
 252   */
 253  define('PARAM_STRINGID',    'stringid');
 254  
 255  // DEPRECATED PARAM TYPES OR ALIASES - DO NOT USE FOR NEW CODE.
 256  /**
 257   * PARAM_CLEAN - obsoleted, please use a more specific type of parameter.
 258   * It was one of the first types, that is why it is abused so much ;-)
 259   * @deprecated since 2.0
 260   */
 261  define('PARAM_CLEAN',    'clean');
 262  
 263  /**
 264   * PARAM_INTEGER - deprecated alias for PARAM_INT
 265   * @deprecated since 2.0
 266   */
 267  define('PARAM_INTEGER',  'int');
 268  
 269  /**
 270   * PARAM_NUMBER - deprecated alias of PARAM_FLOAT
 271   * @deprecated since 2.0
 272   */
 273  define('PARAM_NUMBER',  'float');
 274  
 275  /**
 276   * PARAM_ACTION - deprecated alias for PARAM_ALPHANUMEXT, use for various actions in forms and urls
 277   * NOTE: originally alias for PARAM_APLHA
 278   * @deprecated since 2.0
 279   */
 280  define('PARAM_ACTION',   'alphanumext');
 281  
 282  /**
 283   * PARAM_FORMAT - deprecated alias for PARAM_ALPHANUMEXT, use for names of plugins, formats, etc.
 284   * NOTE: originally alias for PARAM_APLHA
 285   * @deprecated since 2.0
 286   */
 287  define('PARAM_FORMAT',   'alphanumext');
 288  
 289  /**
 290   * PARAM_MULTILANG - deprecated alias of PARAM_TEXT.
 291   * @deprecated since 2.0
 292   */
 293  define('PARAM_MULTILANG',  'text');
 294  
 295  /**
 296   * PARAM_TIMEZONE - expected timezone. Timezone can be int +-(0-13) or float +-(0.5-12.5) or
 297   * string separated by '/' and can have '-' &/ '_' (eg. America/North_Dakota/New_Salem
 298   * America/Port-au-Prince)
 299   */
 300  define('PARAM_TIMEZONE', 'timezone');
 301  
 302  /**
 303   * PARAM_CLEANFILE - deprecated alias of PARAM_FILE; originally was removing regional chars too
 304   */
 305  define('PARAM_CLEANFILE', 'file');
 306  
 307  /**
 308   * PARAM_COMPONENT is used for full component names (aka frankenstyle) such as 'mod_forum', 'core_rating', 'auth_ldap'.
 309   * Short legacy subsystem names and module names are accepted too ex: 'forum', 'rating', 'user'.
 310   * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
 311   * NOTE: numbers and underscores are strongly discouraged in plugin names!
 312   */
 313  define('PARAM_COMPONENT', 'component');
 314  
 315  /**
 316   * PARAM_AREA is a name of area used when addressing files, comments, ratings, etc.
 317   * It is usually used together with context id and component.
 318   * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
 319   */
 320  define('PARAM_AREA', 'area');
 321  
 322  /**
 323   * PARAM_PLUGIN is used for plugin names such as 'forum', 'glossary', 'ldap', 'paypal', 'completionstatus'.
 324   * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
 325   * NOTE: numbers and underscores are strongly discouraged in plugin names! Underscores are forbidden in module names.
 326   */
 327  define('PARAM_PLUGIN', 'plugin');
 328  
 329  
 330  // Web Services.
 331  
 332  /**
 333   * VALUE_REQUIRED - if the parameter is not supplied, there is an error
 334   */
 335  define('VALUE_REQUIRED', 1);
 336  
 337  /**
 338   * VALUE_OPTIONAL - if the parameter is not supplied, then the param has no value
 339   */
 340  define('VALUE_OPTIONAL', 2);
 341  
 342  /**
 343   * VALUE_DEFAULT - if the parameter is not supplied, then the default value is used
 344   */
 345  define('VALUE_DEFAULT', 0);
 346  
 347  /**
 348   * NULL_NOT_ALLOWED - the parameter can not be set to null in the database
 349   */
 350  define('NULL_NOT_ALLOWED', false);
 351  
 352  /**
 353   * NULL_ALLOWED - the parameter can be set to null in the database
 354   */
 355  define('NULL_ALLOWED', true);
 356  
 357  // Page types.
 358  
 359  /**
 360   * PAGE_COURSE_VIEW is a definition of a page type. For more information on the page class see moodle/lib/pagelib.php.
 361   */
 362  define('PAGE_COURSE_VIEW', 'course-view');
 363  
 364  /** Get remote addr constant */
 365  define('GETREMOTEADDR_SKIP_HTTP_CLIENT_IP', '1');
 366  /** Get remote addr constant */
 367  define('GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR', '2');
 368  /**
 369   * GETREMOTEADDR_SKIP_DEFAULT defines the default behavior remote IP address validation.
 370   */
 371  define('GETREMOTEADDR_SKIP_DEFAULT', GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR|GETREMOTEADDR_SKIP_HTTP_CLIENT_IP);
 372  
 373  // Blog access level constant declaration.
 374  define ('BLOG_USER_LEVEL', 1);
 375  define ('BLOG_GROUP_LEVEL', 2);
 376  define ('BLOG_COURSE_LEVEL', 3);
 377  define ('BLOG_SITE_LEVEL', 4);
 378  define ('BLOG_GLOBAL_LEVEL', 5);
 379  
 380  
 381  // Tag constants.
 382  /**
 383   * To prevent problems with multibytes strings,Flag updating in nav not working on the review page. this should not exceed the
 384   * length of "varchar(255) / 3 (bytes / utf-8 character) = 85".
 385   * TODO: this is not correct, varchar(255) are 255 unicode chars ;-)
 386   *
 387   * @todo define(TAG_MAX_LENGTH) this is not correct, varchar(255) are 255 unicode chars ;-)
 388   */
 389  define('TAG_MAX_LENGTH', 50);
 390  
 391  // Password policy constants.
 392  define ('PASSWORD_LOWER', 'abcdefghijklmnopqrstuvwxyz');
 393  define ('PASSWORD_UPPER', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
 394  define ('PASSWORD_DIGITS', '0123456789');
 395  define ('PASSWORD_NONALPHANUM', '.,;:!?_-+/*@#&$');
 396  
 397  // Feature constants.
 398  // Used for plugin_supports() to report features that are, or are not, supported by a module.
 399  
 400  /** True if module can provide a grade */
 401  define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade');
 402  /** True if module supports outcomes */
 403  define('FEATURE_GRADE_OUTCOMES', 'outcomes');
 404  /** True if module supports advanced grading methods */
 405  define('FEATURE_ADVANCED_GRADING', 'grade_advanced_grading');
 406  /** True if module controls the grade visibility over the gradebook */
 407  define('FEATURE_CONTROLS_GRADE_VISIBILITY', 'controlsgradevisbility');
 408  /** True if module supports plagiarism plugins */
 409  define('FEATURE_PLAGIARISM', 'plagiarism');
 410  
 411  /** True if module has code to track whether somebody viewed it */
 412  define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views');
 413  /** True if module has custom completion rules */
 414  define('FEATURE_COMPLETION_HAS_RULES', 'completion_has_rules');
 415  
 416  /** True if module has no 'view' page (like label) */
 417  define('FEATURE_NO_VIEW_LINK', 'viewlink');
 418  /** True (which is default) if the module wants support for setting the ID number for grade calculation purposes. */
 419  define('FEATURE_IDNUMBER', 'idnumber');
 420  /** True if module supports groups */
 421  define('FEATURE_GROUPS', 'groups');
 422  /** True if module supports groupings */
 423  define('FEATURE_GROUPINGS', 'groupings');
 424  /**
 425   * True if module supports groupmembersonly (which no longer exists)
 426   * @deprecated Since Moodle 2.8
 427   */
 428  define('FEATURE_GROUPMEMBERSONLY', 'groupmembersonly');
 429  
 430  /** Type of module */
 431  define('FEATURE_MOD_ARCHETYPE', 'mod_archetype');
 432  /** True if module supports intro editor */
 433  define('FEATURE_MOD_INTRO', 'mod_intro');
 434  /** True if module has default completion */
 435  define('FEATURE_MODEDIT_DEFAULT_COMPLETION', 'modedit_default_completion');
 436  
 437  define('FEATURE_COMMENT', 'comment');
 438  
 439  define('FEATURE_RATE', 'rate');
 440  /** True if module supports backup/restore of moodle2 format */
 441  define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2');
 442  
 443  /** True if module can show description on course main page */
 444  define('FEATURE_SHOW_DESCRIPTION', 'showdescription');
 445  
 446  /** True if module uses the question bank */
 447  define('FEATURE_USES_QUESTIONS', 'usesquestions');
 448  
 449  /**
 450   * Maximum filename char size
 451   */
 452  define('MAX_FILENAME_SIZE', 100);
 453  
 454  /** Unspecified module archetype */
 455  define('MOD_ARCHETYPE_OTHER', 0);
 456  /** Resource-like type module */
 457  define('MOD_ARCHETYPE_RESOURCE', 1);
 458  /** Assignment module archetype */
 459  define('MOD_ARCHETYPE_ASSIGNMENT', 2);
 460  /** System (not user-addable) module archetype */
 461  define('MOD_ARCHETYPE_SYSTEM', 3);
 462  
 463  /**
 464   * Security token used for allowing access
 465   * from external application such as web services.
 466   * Scripts do not use any session, performance is relatively
 467   * low because we need to load access info in each request.
 468   * Scripts are executed in parallel.
 469   */
 470  define('EXTERNAL_TOKEN_PERMANENT', 0);
 471  
 472  /**
 473   * Security token used for allowing access
 474   * of embedded applications, the code is executed in the
 475   * active user session. Token is invalidated after user logs out.
 476   * Scripts are executed serially - normal session locking is used.
 477   */
 478  define('EXTERNAL_TOKEN_EMBEDDED', 1);
 479  
 480  /**
 481   * The home page should be the site home
 482   */
 483  define('HOMEPAGE_SITE', 0);
 484  /**
 485   * The home page should be the users my page
 486   */
 487  define('HOMEPAGE_MY', 1);
 488  /**
 489   * The home page can be chosen by the user
 490   */
 491  define('HOMEPAGE_USER', 2);
 492  
 493  /**
 494   * URL of the Moodle sites registration portal.
 495   */
 496  defined('HUB_MOODLEORGHUBURL') || define('HUB_MOODLEORGHUBURL', 'https://stats.moodle.org');
 497  
 498  /**
 499   * Moodle mobile app service name
 500   */
 501  define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app');
 502  
 503  /**
 504   * Indicates the user has the capabilities required to ignore activity and course file size restrictions
 505   */
 506  define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1);
 507  
 508  /**
 509   * Course display settings: display all sections on one page.
 510   */
 511  define('COURSE_DISPLAY_SINGLEPAGE', 0);
 512  /**
 513   * Course display settings: split pages into a page per section.
 514   */
 515  define('COURSE_DISPLAY_MULTIPAGE', 1);
 516  
 517  /**
 518   * Authentication constant: String used in password field when password is not stored.
 519   */
 520  define('AUTH_PASSWORD_NOT_CACHED', 'not cached');
 521  
 522  /**
 523   * Email from header to never include via information.
 524   */
 525  define('EMAIL_VIA_NEVER', 0);
 526  
 527  /**
 528   * Email from header to always include via information.
 529   */
 530  define('EMAIL_VIA_ALWAYS', 1);
 531  
 532  /**
 533   * Email from header to only include via information if the address is no-reply.
 534   */
 535  define('EMAIL_VIA_NO_REPLY_ONLY', 2);
 536  
 537  // PARAMETER HANDLING.
 538  
 539  /**
 540   * Returns a particular value for the named variable, taken from
 541   * POST or GET.  If the parameter doesn't exist then an error is
 542   * thrown because we require this variable.
 543   *
 544   * This function should be used to initialise all required values
 545   * in a script that are based on parameters.  Usually it will be
 546   * used like this:
 547   *    $id = required_param('id', PARAM_INT);
 548   *
 549   * Please note the $type parameter is now required and the value can not be array.
 550   *
 551   * @param string $parname the name of the page parameter we want
 552   * @param string $type expected type of parameter
 553   * @return mixed
 554   * @throws coding_exception
 555   */
 556  function required_param($parname, $type) {
 557      if (func_num_args() != 2 or empty($parname) or empty($type)) {
 558          throw new coding_exception('required_param() requires $parname and $type to be specified (parameter: '.$parname.')');
 559      }
 560      // POST has precedence.
 561      if (isset($_POST[$parname])) {
 562          $param = $_POST[$parname];
 563      } else if (isset($_GET[$parname])) {
 564          $param = $_GET[$parname];
 565      } else {
 566          print_error('missingparam', '', '', $parname);
 567      }
 568  
 569      if (is_array($param)) {
 570          debugging('Invalid array parameter detected in required_param(): '.$parname);
 571          // TODO: switch to fatal error in Moodle 2.3.
 572          return required_param_array($parname, $type);
 573      }
 574  
 575      return clean_param($param, $type);
 576  }
 577  
 578  /**
 579   * Returns a particular array value for the named variable, taken from
 580   * POST or GET.  If the parameter doesn't exist then an error is
 581   * thrown because we require this variable.
 582   *
 583   * This function should be used to initialise all required values
 584   * in a script that are based on parameters.  Usually it will be
 585   * used like this:
 586   *    $ids = required_param_array('ids', PARAM_INT);
 587   *
 588   *  Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
 589   *
 590   * @param string $parname the name of the page parameter we want
 591   * @param string $type expected type of parameter
 592   * @return array
 593   * @throws coding_exception
 594   */
 595  function required_param_array($parname, $type) {
 596      if (func_num_args() != 2 or empty($parname) or empty($type)) {
 597          throw new coding_exception('required_param_array() requires $parname and $type to be specified (parameter: '.$parname.')');
 598      }
 599      // POST has precedence.
 600      if (isset($_POST[$parname])) {
 601          $param = $_POST[$parname];
 602      } else if (isset($_GET[$parname])) {
 603          $param = $_GET[$parname];
 604      } else {
 605          print_error('missingparam', '', '', $parname);
 606      }
 607      if (!is_array($param)) {
 608          print_error('missingparam', '', '', $parname);
 609      }
 610  
 611      $result = array();
 612      foreach ($param as $key => $value) {
 613          if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
 614              debugging('Invalid key name in required_param_array() detected: '.$key.', parameter: '.$parname);
 615              continue;
 616          }
 617          $result[$key] = clean_param($value, $type);
 618      }
 619  
 620      return $result;
 621  }
 622  
 623  /**
 624   * Returns a particular value for the named variable, taken from
 625   * POST or GET, otherwise returning a given default.
 626   *
 627   * This function should be used to initialise all optional values
 628   * in a script that are based on parameters.  Usually it will be
 629   * used like this:
 630   *    $name = optional_param('name', 'Fred', PARAM_TEXT);
 631   *
 632   * Please note the $type parameter is now required and the value can not be array.
 633   *
 634   * @param string $parname the name of the page parameter we want
 635   * @param mixed  $default the default value to return if nothing is found
 636   * @param string $type expected type of parameter
 637   * @return mixed
 638   * @throws coding_exception
 639   */
 640  function optional_param($parname, $default, $type) {
 641      if (func_num_args() != 3 or empty($parname) or empty($type)) {
 642          throw new coding_exception('optional_param requires $parname, $default + $type to be specified (parameter: '.$parname.')');
 643      }
 644  
 645      // POST has precedence.
 646      if (isset($_POST[$parname])) {
 647          $param = $_POST[$parname];
 648      } else if (isset($_GET[$parname])) {
 649          $param = $_GET[$parname];
 650      } else {
 651          return $default;
 652      }
 653  
 654      if (is_array($param)) {
 655          debugging('Invalid array parameter detected in required_param(): '.$parname);
 656          // TODO: switch to $default in Moodle 2.3.
 657          return optional_param_array($parname, $default, $type);
 658      }
 659  
 660      return clean_param($param, $type);
 661  }
 662  
 663  /**
 664   * Returns a particular array value for the named variable, taken from
 665   * POST or GET, otherwise returning a given default.
 666   *
 667   * This function should be used to initialise all optional values
 668   * in a script that are based on parameters.  Usually it will be
 669   * used like this:
 670   *    $ids = optional_param('id', array(), PARAM_INT);
 671   *
 672   * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
 673   *
 674   * @param string $parname the name of the page parameter we want
 675   * @param mixed $default the default value to return if nothing is found
 676   * @param string $type expected type of parameter
 677   * @return array
 678   * @throws coding_exception
 679   */
 680  function optional_param_array($parname, $default, $type) {
 681      if (func_num_args() != 3 or empty($parname) or empty($type)) {
 682          throw new coding_exception('optional_param_array requires $parname, $default + $type to be specified (parameter: '.$parname.')');
 683      }
 684  
 685      // POST has precedence.
 686      if (isset($_POST[$parname])) {
 687          $param = $_POST[$parname];
 688      } else if (isset($_GET[$parname])) {
 689          $param = $_GET[$parname];
 690      } else {
 691          return $default;
 692      }
 693      if (!is_array($param)) {
 694          debugging('optional_param_array() expects array parameters only: '.$parname);
 695          return $default;
 696      }
 697  
 698      $result = array();
 699      foreach ($param as $key => $value) {
 700          if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
 701              debugging('Invalid key name in optional_param_array() detected: '.$key.', parameter: '.$parname);
 702              continue;
 703          }
 704          $result[$key] = clean_param($value, $type);
 705      }
 706  
 707      return $result;
 708  }
 709  
 710  /**
 711   * Strict validation of parameter values, the values are only converted
 712   * to requested PHP type. Internally it is using clean_param, the values
 713   * before and after cleaning must be equal - otherwise
 714   * an invalid_parameter_exception is thrown.
 715   * Objects and classes are not accepted.
 716   *
 717   * @param mixed $param
 718   * @param string $type PARAM_ constant
 719   * @param bool $allownull are nulls valid value?
 720   * @param string $debuginfo optional debug information
 721   * @return mixed the $param value converted to PHP type
 722   * @throws invalid_parameter_exception if $param is not of given type
 723   */
 724  function validate_param($param, $type, $allownull=NULL_NOT_ALLOWED, $debuginfo='') {
 725      if (is_null($param)) {
 726          if ($allownull == NULL_ALLOWED) {
 727              return null;
 728          } else {
 729              throw new invalid_parameter_exception($debuginfo);
 730          }
 731      }
 732      if (is_array($param) or is_object($param)) {
 733          throw new invalid_parameter_exception($debuginfo);
 734      }
 735  
 736      $cleaned = clean_param($param, $type);
 737  
 738      if ($type == PARAM_FLOAT) {
 739          // Do not detect precision loss here.
 740          if (is_float($param) or is_int($param)) {
 741              // These always fit.
 742          } else if (!is_numeric($param) or !preg_match('/^[\+-]?[0-9]*\.?[0-9]*(e[-+]?[0-9]+)?$/i', (string)$param)) {
 743              throw new invalid_parameter_exception($debuginfo);
 744          }
 745      } else if ((string)$param !== (string)$cleaned) {
 746          // Conversion to string is usually lossless.
 747          throw new invalid_parameter_exception($debuginfo);
 748      }
 749  
 750      return $cleaned;
 751  }
 752  
 753  /**
 754   * Makes sure array contains only the allowed types, this function does not validate array key names!
 755   *
 756   * <code>
 757   * $options = clean_param($options, PARAM_INT);
 758   * </code>
 759   *
 760   * @param array $param the variable array we are cleaning
 761   * @param string $type expected format of param after cleaning.
 762   * @param bool $recursive clean recursive arrays
 763   * @return array
 764   * @throws coding_exception
 765   */
 766  function clean_param_array(array $param = null, $type, $recursive = false) {
 767      // Convert null to empty array.
 768      $param = (array)$param;
 769      foreach ($param as $key => $value) {
 770          if (is_array($value)) {
 771              if ($recursive) {
 772                  $param[$key] = clean_param_array($value, $type, true);
 773              } else {
 774                  throw new coding_exception('clean_param_array can not process multidimensional arrays when $recursive is false.');
 775              }
 776          } else {
 777              $param[$key] = clean_param($value, $type);
 778          }
 779      }
 780      return $param;
 781  }
 782  
 783  /**
 784   * Used by {@link optional_param()} and {@link required_param()} to
 785   * clean the variables and/or cast to specific types, based on
 786   * an options field.
 787   * <code>
 788   * $course->format = clean_param($course->format, PARAM_ALPHA);
 789   * $selectedgradeitem = clean_param($selectedgradeitem, PARAM_INT);
 790   * </code>
 791   *
 792   * @param mixed $param the variable we are cleaning
 793   * @param string $type expected format of param after cleaning.
 794   * @return mixed
 795   * @throws coding_exception
 796   */
 797  function clean_param($param, $type) {
 798      global $CFG;
 799  
 800      if (is_array($param)) {
 801          throw new coding_exception('clean_param() can not process arrays, please use clean_param_array() instead.');
 802      } else if (is_object($param)) {
 803          if (method_exists($param, '__toString')) {
 804              $param = $param->__toString();
 805          } else {
 806              throw new coding_exception('clean_param() can not process objects, please use clean_param_array() instead.');
 807          }
 808      }
 809  
 810      switch ($type) {
 811          case PARAM_RAW:
 812              // No cleaning at all.
 813              $param = fix_utf8($param);
 814              return $param;
 815  
 816          case PARAM_RAW_TRIMMED:
 817              // No cleaning, but strip leading and trailing whitespace.
 818              $param = fix_utf8($param);
 819              return trim($param);
 820  
 821          case PARAM_CLEAN:
 822              // General HTML cleaning, try to use more specific type if possible this is deprecated!
 823              // Please use more specific type instead.
 824              if (is_numeric($param)) {
 825                  return $param;
 826              }
 827              $param = fix_utf8($param);
 828              // Sweep for scripts, etc.
 829              return clean_text($param);
 830  
 831          case PARAM_CLEANHTML:
 832              // Clean html fragment.
 833              $param = fix_utf8($param);
 834              // Sweep for scripts, etc.
 835              $param = clean_text($param, FORMAT_HTML);
 836              return trim($param);
 837  
 838          case PARAM_INT:
 839              // Convert to integer.
 840              return (int)$param;
 841  
 842          case PARAM_FLOAT:
 843              // Convert to float.
 844              return (float)$param;
 845  
 846          case PARAM_LOCALISEDFLOAT:
 847              // Convert to float.
 848              return unformat_float($param, true);
 849  
 850          case PARAM_ALPHA:
 851              // Remove everything not `a-z`.
 852              return preg_replace('/[^a-zA-Z]/i', '', $param);
 853  
 854          case PARAM_ALPHAEXT:
 855              // Remove everything not `a-zA-Z_-` (originally allowed "/" too).
 856              return preg_replace('/[^a-zA-Z_-]/i', '', $param);
 857  
 858          case PARAM_ALPHANUM:
 859              // Remove everything not `a-zA-Z0-9`.
 860              return preg_replace('/[^A-Za-z0-9]/i', '', $param);
 861  
 862          case PARAM_ALPHANUMEXT:
 863              // Remove everything not `a-zA-Z0-9_-`.
 864              return preg_replace('/[^A-Za-z0-9_-]/i', '', $param);
 865  
 866          case PARAM_SEQUENCE:
 867              // Remove everything not `0-9,`.
 868              return preg_replace('/[^0-9,]/i', '', $param);
 869  
 870          case PARAM_BOOL:
 871              // Convert to 1 or 0.
 872              $tempstr = strtolower($param);
 873              if ($tempstr === 'on' or $tempstr === 'yes' or $tempstr === 'true') {
 874                  $param = 1;
 875              } else if ($tempstr === 'off' or $tempstr === 'no'  or $tempstr === 'false') {
 876                  $param = 0;
 877              } else {
 878                  $param = empty($param) ? 0 : 1;
 879              }
 880              return $param;
 881  
 882          case PARAM_NOTAGS:
 883              // Strip all tags.
 884              $param = fix_utf8($param);
 885              return strip_tags($param);
 886  
 887          case PARAM_TEXT:
 888              // Leave only tags needed for multilang.
 889              $param = fix_utf8($param);
 890              // If the multilang syntax is not correct we strip all tags because it would break xhtml strict which is required
 891              // for accessibility standards please note this cleaning does not strip unbalanced '>' for BC compatibility reasons.
 892              do {
 893                  if (strpos($param, '</lang>') !== false) {
 894                      // Old and future mutilang syntax.
 895                      $param = strip_tags($param, '<lang>');
 896                      if (!preg_match_all('/<.*>/suU', $param, $matches)) {
 897                          break;
 898                      }
 899                      $open = false;
 900                      foreach ($matches[0] as $match) {
 901                          if ($match === '</lang>') {
 902                              if ($open) {
 903                                  $open = false;
 904                                  continue;
 905                              } else {
 906                                  break 2;
 907                              }
 908                          }
 909                          if (!preg_match('/^<lang lang="[a-zA-Z0-9_-]+"\s*>$/u', $match)) {
 910                              break 2;
 911                          } else {
 912                              $open = true;
 913                          }
 914                      }
 915                      if ($open) {
 916                          break;
 917                      }
 918                      return $param;
 919  
 920                  } else if (strpos($param, '</span>') !== false) {
 921                      // Current problematic multilang syntax.
 922                      $param = strip_tags($param, '<span>');
 923                      if (!preg_match_all('/<.*>/suU', $param, $matches)) {
 924                          break;
 925                      }
 926                      $open = false;
 927                      foreach ($matches[0] as $match) {
 928                          if ($match === '</span>') {
 929                              if ($open) {
 930                                  $open = false;
 931                                  continue;
 932                              } else {
 933                                  break 2;
 934                              }
 935                          }
 936                          if (!preg_match('/^<span(\s+lang="[a-zA-Z0-9_-]+"|\s+class="multilang"){2}\s*>$/u', $match)) {
 937                              break 2;
 938                          } else {
 939                              $open = true;
 940                          }
 941                      }
 942                      if ($open) {
 943                          break;
 944                      }
 945                      return $param;
 946                  }
 947              } while (false);
 948              // Easy, just strip all tags, if we ever want to fix orphaned '&' we have to do that in format_string().
 949              return strip_tags($param);
 950  
 951          case PARAM_COMPONENT:
 952              // We do not want any guessing here, either the name is correct or not
 953              // please note only normalised component names are accepted.
 954              if (!preg_match('/^[a-z][a-z0-9]*(_[a-z][a-z0-9_]*)?[a-z0-9]+$/', $param)) {
 955                  return '';
 956              }
 957              if (strpos($param, '__') !== false) {
 958                  return '';
 959              }
 960              if (strpos($param, 'mod_') === 0) {
 961                  // Module names must not contain underscores because we need to differentiate them from invalid plugin types.
 962                  if (substr_count($param, '_') != 1) {
 963                      return '';
 964                  }
 965              }
 966              return $param;
 967  
 968          case PARAM_PLUGIN:
 969          case PARAM_AREA:
 970              // We do not want any guessing here, either the name is correct or not.
 971              if (!is_valid_plugin_name($param)) {
 972                  return '';
 973              }
 974              return $param;
 975  
 976          case PARAM_SAFEDIR:
 977              // Remove everything not a-zA-Z0-9_- .
 978              return preg_replace('/[^a-zA-Z0-9_-]/i', '', $param);
 979  
 980          case PARAM_SAFEPATH:
 981              // Remove everything not a-zA-Z0-9/_- .
 982              return preg_replace('/[^a-zA-Z0-9\/_-]/i', '', $param);
 983  
 984          case PARAM_FILE:
 985              // Strip all suspicious characters from filename.
 986              $param = fix_utf8($param);
 987              $param = preg_replace('~[[:cntrl:]]|[&<>"`\|\':\\\\/]~u', '', $param);
 988              if ($param === '.' || $param === '..') {
 989                  $param = '';
 990              }
 991              return $param;
 992  
 993          case PARAM_PATH:
 994              // Strip all suspicious characters from file path.
 995              $param = fix_utf8($param);
 996              $param = str_replace('\\', '/', $param);
 997  
 998              // Explode the path and clean each element using the PARAM_FILE rules.
 999              $breadcrumb = explode('/', $param);
1000              foreach ($breadcrumb as $key => $crumb) {
1001                  if ($crumb === '.' && $key === 0) {
1002                      // Special condition to allow for relative current path such as ./currentdirfile.txt.
1003                  } else {
1004                      $crumb = clean_param($crumb, PARAM_FILE);
1005                  }
1006                  $breadcrumb[$key] = $crumb;
1007              }
1008              $param = implode('/', $breadcrumb);
1009  
1010              // Remove multiple current path (./././) and multiple slashes (///).
1011              $param = preg_replace('~//+~', '/', $param);
1012              $param = preg_replace('~/(\./)+~', '/', $param);
1013              return $param;
1014  
1015          case PARAM_HOST:
1016              // Allow FQDN or IPv4 dotted quad.
1017              $param = preg_replace('/[^\.\d\w-]/', '', $param );
1018              // Match ipv4 dotted quad.
1019              if (preg_match('/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/', $param, $match)) {
1020                  // Confirm values are ok.
1021                  if ( $match[0] > 255
1022                       || $match[1] > 255
1023                       || $match[3] > 255
1024                       || $match[4] > 255 ) {
1025                      // Hmmm, what kind of dotted quad is this?
1026                      $param = '';
1027                  }
1028              } else if ( preg_match('/^[\w\d\.-]+$/', $param) // Dots, hyphens, numbers.
1029                         && !preg_match('/^[\.-]/',  $param) // No leading dots/hyphens.
1030                         && !preg_match('/[\.-]$/',  $param) // No trailing dots/hyphens.
1031                         ) {
1032                  // All is ok - $param is respected.
1033              } else {
1034                  // All is not ok...
1035                  $param='';
1036              }
1037              return $param;
1038  
1039          case PARAM_URL:
1040              // Allow safe urls.
1041              $param = fix_utf8($param);
1042              include_once($CFG->dirroot . '/lib/validateurlsyntax.php');
1043              if (!empty($param) && validateUrlSyntax($param, 's?H?S?F?E-u-P-a?I?p?f?q?r?')) {
1044                  // All is ok, param is respected.
1045              } else {
1046                  // Not really ok.
1047                  $param ='';
1048              }
1049              return $param;
1050  
1051          case PARAM_LOCALURL:
1052              // Allow http absolute, root relative and relative URLs within wwwroot.
1053              $param = clean_param($param, PARAM_URL);
1054              if (!empty($param)) {
1055  
1056                  if ($param === $CFG->wwwroot) {
1057                      // Exact match;
1058                  } else if (preg_match(':^/:', $param)) {
1059                      // Root-relative, ok!
1060                  } else if (preg_match('/^' . preg_quote($CFG->wwwroot . '/', '/') . '/i', $param)) {
1061                      // Absolute, and matches our wwwroot.
1062                  } else {
1063                      // Relative - let's make sure there are no tricks.
1064                      if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?')) {
1065                          // Looks ok.
1066                      } else {
1067                          $param = '';
1068                      }
1069                  }
1070              }
1071              return $param;
1072  
1073          case PARAM_PEM:
1074              $param = trim($param);
1075              // PEM formatted strings may contain letters/numbers and the symbols:
1076              //   forward slash: /
1077              //   plus sign:     +
1078              //   equal sign:    =
1079              //   , surrounded by BEGIN and END CERTIFICATE prefix and suffixes.
1080              if (preg_match('/^-----BEGIN CERTIFICATE-----([\s\w\/\+=]+)-----END CERTIFICATE-----$/', trim($param), $matches)) {
1081                  list($wholething, $body) = $matches;
1082                  unset($wholething, $matches);
1083                  $b64 = clean_param($body, PARAM_BASE64);
1084                  if (!empty($b64)) {
1085                      return "-----BEGIN CERTIFICATE-----\n$b64\n-----END CERTIFICATE-----\n";
1086                  } else {
1087                      return '';
1088                  }
1089              }
1090              return '';
1091  
1092          case PARAM_BASE64:
1093              if (!empty($param)) {
1094                  // PEM formatted strings may contain letters/numbers and the symbols
1095                  //   forward slash: /
1096                  //   plus sign:     +
1097                  //   equal sign:    =.
1098                  if (0 >= preg_match('/^([\s\w\/\+=]+)$/', trim($param))) {
1099                      return '';
1100                  }
1101                  $lines = preg_split('/[\s]+/', $param, -1, PREG_SPLIT_NO_EMPTY);
1102                  // Each line of base64 encoded data must be 64 characters in length, except for the last line which may be less
1103                  // than (or equal to) 64 characters long.
1104                  for ($i=0, $j=count($lines); $i < $j; $i++) {
1105                      if ($i + 1 == $j) {
1106                          if (64 < strlen($lines[$i])) {
1107                              return '';
1108                          }
1109                          continue;
1110                      }
1111  
1112                      if (64 != strlen($lines[$i])) {
1113                          return '';
1114                      }
1115                  }
1116                  return implode("\n", $lines);
1117              } else {
1118                  return '';
1119              }
1120  
1121          case PARAM_TAG:
1122              $param = fix_utf8($param);
1123              // Please note it is not safe to use the tag name directly anywhere,
1124              // it must be processed with s(), urlencode() before embedding anywhere.
1125              // Remove some nasties.
1126              $param = preg_replace('~[[:cntrl:]]|[<>`]~u', '', $param);
1127              // Convert many whitespace chars into one.
1128              $param = preg_replace('/\s+/u', ' ', $param);
1129              $param = core_text::substr(trim($param), 0, TAG_MAX_LENGTH);
1130              return $param;
1131  
1132          case PARAM_TAGLIST:
1133              $param = fix_utf8($param);
1134              $tags = explode(',', $param);
1135              $result = array();
1136              foreach ($tags as $tag) {
1137                  $res = clean_param($tag, PARAM_TAG);
1138                  if ($res !== '') {
1139                      $result[] = $res;
1140                  }
1141              }
1142              if ($result) {
1143                  return implode(',', $result);
1144              } else {
1145                  return '';
1146              }
1147  
1148          case PARAM_CAPABILITY:
1149              if (get_capability_info($param)) {
1150                  return $param;
1151              } else {
1152                  return '';
1153              }
1154  
1155          case PARAM_PERMISSION:
1156              $param = (int)$param;
1157              if (in_array($param, array(CAP_INHERIT, CAP_ALLOW, CAP_PREVENT, CAP_PROHIBIT))) {
1158                  return $param;
1159              } else {
1160                  return CAP_INHERIT;
1161              }
1162  
1163          case PARAM_AUTH:
1164              $param = clean_param($param, PARAM_PLUGIN);
1165              if (empty($param)) {
1166                  return '';
1167              } else if (exists_auth_plugin($param)) {
1168                  return $param;
1169              } else {
1170                  return '';
1171              }
1172  
1173          case PARAM_LANG:
1174              $param = clean_param($param, PARAM_SAFEDIR);
1175              if (get_string_manager()->translation_exists($param)) {
1176                  return $param;
1177              } else {
1178                  // Specified language is not installed or param malformed.
1179                  return '';
1180              }
1181  
1182          case PARAM_THEME:
1183              $param = clean_param($param, PARAM_PLUGIN);
1184              if (empty($param)) {
1185                  return '';
1186              } else if (file_exists("$CFG->dirroot/theme/$param/config.php")) {
1187                  return $param;
1188              } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$param/config.php")) {
1189                  return $param;
1190              } else {
1191                  // Specified theme is not installed.
1192                  return '';
1193              }
1194  
1195          case PARAM_USERNAME:
1196              $param = fix_utf8($param);
1197              $param = trim($param);
1198              // Convert uppercase to lowercase MDL-16919.
1199              $param = core_text::strtolower($param);
1200              if (empty($CFG->extendedusernamechars)) {
1201                  $param = str_replace(" " , "", $param);
1202                  // Regular expression, eliminate all chars EXCEPT:
1203                  // alphanum, dash (-), underscore (_), at sign (@) and period (.) characters.
1204                  $param = preg_replace('/[^-\.@_a-z0-9]/', '', $param);
1205              }
1206              return $param;
1207  
1208          case PARAM_EMAIL:
1209              $param = fix_utf8($param);
1210              if (validate_email($param)) {
1211                  return $param;
1212              } else {
1213                  return '';
1214              }
1215  
1216          case PARAM_STRINGID:
1217              if (preg_match('|^[a-zA-Z][a-zA-Z0-9\.:/_-]*$|', $param)) {
1218                  return $param;
1219              } else {
1220                  return '';
1221              }
1222  
1223          case PARAM_TIMEZONE:
1224              // Can be int, float(with .5 or .0) or string seperated by '/' and can have '-_'.
1225              $param = fix_utf8($param);
1226              $timezonepattern = '/^(([+-]?(0?[0-9](\.[5|0])?|1[0-3](\.0)?|1[0-2]\.5))|(99)|[[:alnum:]]+(\/?[[:alpha:]_-])+)$/';
1227              if (preg_match($timezonepattern, $param)) {
1228                  return $param;
1229              } else {
1230                  return '';
1231              }
1232  
1233          default:
1234              // Doh! throw error, switched parameters in optional_param or another serious problem.
1235              print_error("unknownparamtype", '', '', $type);
1236      }
1237  }
1238  
1239  /**
1240   * Whether the PARAM_* type is compatible in RTL.
1241   *
1242   * Being compatible with RTL means that the data they contain can flow
1243   * from right-to-left or left-to-right without compromising the user experience.
1244   *
1245   * Take URLs for example, they are not RTL compatible as they should always
1246   * flow from the left to the right. This also applies to numbers, email addresses,
1247   * configuration snippets, base64 strings, etc...
1248   *
1249   * This function tries to best guess which parameters can contain localised strings.
1250   *
1251   * @param string $paramtype Constant PARAM_*.
1252   * @return bool
1253   */
1254  function is_rtl_compatible($paramtype) {
1255      return $paramtype == PARAM_TEXT || $paramtype == PARAM_NOTAGS;
1256  }
1257  
1258  /**
1259   * Makes sure the data is using valid utf8, invalid characters are discarded.
1260   *
1261   * Note: this function is not intended for full objects with methods and private properties.
1262   *
1263   * @param mixed $value
1264   * @return mixed with proper utf-8 encoding
1265   */
1266  function fix_utf8($value) {
1267      if (is_null($value) or $value === '') {
1268          return $value;
1269  
1270      } else if (is_string($value)) {
1271          if ((string)(int)$value === $value) {
1272              // Shortcut.
1273              return $value;
1274          }
1275          // No null bytes expected in our data, so let's remove it.
1276          $value = str_replace("\0", '', $value);
1277  
1278          // Note: this duplicates min_fix_utf8() intentionally.
1279          static $buggyiconv = null;
1280          if ($buggyiconv === null) {
1281              $buggyiconv = (!function_exists('iconv') or @iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€');
1282          }
1283  
1284          if ($buggyiconv) {
1285              if (function_exists('mb_convert_encoding')) {
1286                  $subst = mb_substitute_character();
1287                  mb_substitute_character('none');
1288                  $result = mb_convert_encoding($value, 'utf-8', 'utf-8');
1289                  mb_substitute_character($subst);
1290  
1291              } else {
1292                  // Warn admins on admin/index.php page.
1293                  $result = $value;
1294              }
1295  
1296          } else {
1297              $result = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
1298          }
1299  
1300          return $result;
1301  
1302      } else if (is_array($value)) {
1303          foreach ($value as $k => $v) {
1304              $value[$k] = fix_utf8($v);
1305          }
1306          return $value;
1307  
1308      } else if (is_object($value)) {
1309          // Do not modify original.
1310          $value = clone($value);
1311          foreach ($value as $k => $v) {
1312              $value->$k = fix_utf8($v);
1313          }
1314          return $value;
1315  
1316      } else {
1317          // This is some other type, no utf-8 here.
1318          return $value;
1319      }
1320  }
1321  
1322  /**
1323   * Return true if given value is integer or string with integer value
1324   *
1325   * @param mixed $value String or Int
1326   * @return bool true if number, false if not
1327   */
1328  function is_number($value) {
1329      if (is_int($value)) {
1330          return true;
1331      } else if (is_string($value)) {
1332          return ((string)(int)$value) === $value;
1333      } else {
1334          return false;
1335      }
1336  }
1337  
1338  /**
1339   * Returns host part from url.
1340   *
1341   * @param string $url full url
1342   * @return string host, null if not found
1343   */
1344  function get_host_from_url($url) {
1345      preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches);
1346      if ($matches) {
1347          return $matches[1];
1348      }
1349      return null;
1350  }
1351  
1352  /**
1353   * Tests whether anything was returned by text editor
1354   *
1355   * This function is useful for testing whether something you got back from
1356   * the HTML editor actually contains anything. Sometimes the HTML editor
1357   * appear to be empty, but actually you get back a <br> tag or something.
1358   *
1359   * @param string $string a string containing HTML.
1360   * @return boolean does the string contain any actual content - that is text,
1361   * images, objects, etc.
1362   */
1363  function html_is_blank($string) {
1364      return trim(strip_tags($string, '<img><object><applet><input><select><textarea><hr>')) == '';
1365  }
1366  
1367  /**
1368   * Set a key in global configuration
1369   *
1370   * Set a key/value pair in both this session's {@link $CFG} global variable
1371   * and in the 'config' database table for future sessions.
1372   *
1373   * Can also be used to update keys for plugin-scoped configs in config_plugin table.
1374   * In that case it doesn't affect $CFG.
1375   *
1376   * A NULL value will delete the entry.
1377   *
1378   * NOTE: this function is called from lib/db/upgrade.php
1379   *
1380   * @param string $name the key to set
1381   * @param string $value the value to set (without magic quotes)
1382   * @param string $plugin (optional) the plugin scope, default null
1383   * @return bool true or exception
1384   */
1385  function set_config($name, $value, $plugin=null) {
1386      global $CFG, $DB;
1387  
1388      if (empty($plugin)) {
1389          if (!array_key_exists($name, $CFG->config_php_settings)) {
1390              // So it's defined for this invocation at least.
1391              if (is_null($value)) {
1392                  unset($CFG->$name);
1393              } else {
1394                  // Settings from db are always strings.
1395                  $CFG->$name = (string)$value;
1396              }
1397          }
1398  
1399          if ($DB->get_field('config', 'name', array('name' => $name))) {
1400              if ($value === null) {
1401                  $DB->delete_records('config', array('name' => $name));
1402              } else {
1403                  $DB->set_field('config', 'value', $value, array('name' => $name));
1404              }
1405          } else {
1406              if ($value !== null) {
1407                  $config = new stdClass();
1408                  $config->name  = $name;
1409                  $config->value = $value;
1410                  $DB->insert_record('config', $config, false);
1411              }
1412              // When setting config during a Behat test (in the CLI script, not in the web browser
1413              // requests), remember which ones are set so that we can clear them later.
1414              if (defined('BEHAT_TEST')) {
1415                  if (!property_exists($CFG, 'behat_cli_added_config')) {
1416                      $CFG->behat_cli_added_config = [];
1417                  }
1418                  $CFG->behat_cli_added_config[$name] = true;
1419              }
1420          }
1421          if ($name === 'siteidentifier') {
1422              cache_helper::update_site_identifier($value);
1423          }
1424          cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1425      } else {
1426          // Plugin scope.
1427          if ($id = $DB->get_field('config_plugins', 'id', array('name' => $name, 'plugin' => $plugin))) {
1428              if ($value===null) {
1429                  $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
1430              } else {
1431                  $DB->set_field('config_plugins', 'value', $value, array('id' => $id));
1432              }
1433          } else {
1434              if ($value !== null) {
1435                  $config = new stdClass();
1436                  $config->plugin = $plugin;
1437                  $config->name   = $name;
1438                  $config->value  = $value;
1439                  $DB->insert_record('config_plugins', $config, false);
1440              }
1441          }
1442          cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1443      }
1444  
1445      return true;
1446  }
1447  
1448  /**
1449   * Get configuration values from the global config table
1450   * or the config_plugins table.
1451   *
1452   * If called with one parameter, it will load all the config
1453   * variables for one plugin, and return them as an object.
1454   *
1455   * If called with 2 parameters it will return a string single
1456   * value or false if the value is not found.
1457   *
1458   * NOTE: this function is called from lib/db/upgrade.php
1459   *
1460   * @static string|false $siteidentifier The site identifier is not cached. We use this static cache so
1461   *     that we need only fetch it once per request.
1462   * @param string $plugin full component name
1463   * @param string $name default null
1464   * @return mixed hash-like object or single value, return false no config found
1465   * @throws dml_exception
1466   */
1467  function get_config($plugin, $name = null) {
1468      global $CFG, $DB;
1469  
1470      static $siteidentifier = null;
1471  
1472      if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) {
1473          $forced =& $CFG->config_php_settings;
1474          $iscore = true;
1475          $plugin = 'core';
1476      } else {
1477          if (array_key_exists($plugin, $CFG->forced_plugin_settings)) {
1478              $forced =& $CFG->forced_plugin_settings[$plugin];
1479          } else {
1480              $forced = array();
1481          }
1482          $iscore = false;
1483      }
1484  
1485      if ($siteidentifier === null) {
1486          try {
1487              // This may fail during installation.
1488              // If you have a look at {@link initialise_cfg()} you will see that this is how we detect the need to
1489              // install the database.
1490              $siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
1491          } catch (dml_exception $ex) {
1492              // Set siteidentifier to false. We don't want to trip this continually.
1493              $siteidentifier = false;
1494              throw $ex;
1495          }
1496      }
1497  
1498      if (!empty($name)) {
1499          if (array_key_exists($name, $forced)) {
1500              return (string)$forced[$name];
1501          } else if ($name === 'siteidentifier' && $plugin == 'core') {
1502              return $siteidentifier;
1503          }
1504      }
1505  
1506      $cache = cache::make('core', 'config');
1507      $result = $cache->get($plugin);
1508      if ($result === false) {
1509          // The user is after a recordset.
1510          if (!$iscore) {
1511              $result = $DB->get_records_menu('config_plugins', array('plugin' => $plugin), '', 'name,value');
1512          } else {
1513              // This part is not really used any more, but anyway...
1514              $result = $DB->get_records_menu('config', array(), '', 'name,value');;
1515          }
1516          $cache->set($plugin, $result);
1517      }
1518  
1519      if (!empty($name)) {
1520          if (array_key_exists($name, $result)) {
1521              return $result[$name];
1522          }
1523          return false;
1524      }
1525  
1526      if ($plugin === 'core') {
1527          $result['siteidentifier'] = $siteidentifier;
1528      }
1529  
1530      foreach ($forced as $key => $value) {
1531          if (is_null($value) or is_array($value) or is_object($value)) {
1532              // We do not want any extra mess here, just real settings that could be saved in db.
1533              unset($result[$key]);
1534          } else {
1535              // Convert to string as if it went through the DB.
1536              $result[$key] = (string)$value;
1537          }
1538      }
1539  
1540      return (object)$result;
1541  }
1542  
1543  /**
1544   * Removes a key from global configuration.
1545   *
1546   * NOTE: this function is called from lib/db/upgrade.php
1547   *
1548   * @param string $name the key to set
1549   * @param string $plugin (optional) the plugin scope
1550   * @return boolean whether the operation succeeded.
1551   */
1552  function unset_config($name, $plugin=null) {
1553      global $CFG, $DB;
1554  
1555      if (empty($plugin)) {
1556          unset($CFG->$name);
1557          $DB->delete_records('config', array('name' => $name));
1558          cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1559      } else {
1560          $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
1561          cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1562      }
1563  
1564      return true;
1565  }
1566  
1567  /**
1568   * Remove all the config variables for a given plugin.
1569   *
1570   * NOTE: this function is called from lib/db/upgrade.php
1571   *
1572   * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice';
1573   * @return boolean whether the operation succeeded.
1574   */
1575  function unset_all_config_for_plugin($plugin) {
1576      global $DB;
1577      // Delete from the obvious config_plugins first.
1578      $DB->delete_records('config_plugins', array('plugin' => $plugin));
1579      // Next delete any suspect settings from config.
1580      $like = $DB->sql_like('name', '?', true, true, false, '|');
1581      $params = array($DB->sql_like_escape($plugin.'_', '|') . '%');
1582      $DB->delete_records_select('config', $like, $params);
1583      // Finally clear both the plugin cache and the core cache (suspect settings now removed from core).
1584      cache_helper::invalidate_by_definition('core', 'config', array(), array('core', $plugin));
1585  
1586      return true;
1587  }
1588  
1589  /**
1590   * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability.
1591   *
1592   * All users are verified if they still have the necessary capability.
1593   *
1594   * @param string $value the value of the config setting.
1595   * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor.
1596   * @param bool $includeadmins include administrators.
1597   * @return array of user objects.
1598   */
1599  function get_users_from_config($value, $capability, $includeadmins = true) {
1600      if (empty($value) or $value === '$@NONE@$') {
1601          return array();
1602      }
1603  
1604      // We have to make sure that users still have the necessary capability,
1605      // it should be faster to fetch them all first and then test if they are present
1606      // instead of validating them one-by-one.
1607      $users = get_users_by_capability(context_system::instance(), $capability);
1608      if ($includeadmins) {
1609          $admins = get_admins();
1610          foreach ($admins as $admin) {
1611              $users[$admin->id] = $admin;
1612          }
1613      }
1614  
1615      if ($value === '$@ALL@$') {
1616          return $users;
1617      }
1618  
1619      $result = array(); // Result in correct order.
1620      $allowed = explode(',', $value);
1621      foreach ($allowed as $uid) {
1622          if (isset($users[$uid])) {
1623              $user = $users[$uid];
1624              $result[$user->id] = $user;
1625          }
1626      }
1627  
1628      return $result;
1629  }
1630  
1631  
1632  /**
1633   * Invalidates browser caches and cached data in temp.
1634   *
1635   * @return void
1636   */
1637  function purge_all_caches() {
1638      purge_caches();
1639  }
1640  
1641  /**
1642   * Selectively invalidate different types of cache.
1643   *
1644   * Purges the cache areas specified.  By default, this will purge all caches but can selectively purge specific
1645   * areas alone or in combination.
1646   *
1647   * @param bool[] $options Specific parts of the cache to purge. Valid options are:
1648   *        'muc'    Purge MUC caches?
1649   *        'theme'  Purge theme cache?
1650   *        'lang'   Purge language string cache?
1651   *        'js'     Purge javascript cache?
1652   *        'filter' Purge text filter cache?
1653   *        'other'  Purge all other caches?
1654   */
1655  function purge_caches($options = []) {
1656      $defaults = array_fill_keys(['muc', 'theme', 'lang', 'js', 'template', 'filter', 'other'], false);
1657      if (empty(array_filter($options))) {
1658          $options = array_fill_keys(array_keys($defaults), true); // Set all options to true.
1659      } else {
1660          $options = array_merge($defaults, array_intersect_key($options, $defaults)); // Override defaults with specified options.
1661      }
1662      if ($options['muc']) {
1663          cache_helper::purge_all();
1664      }
1665      if ($options['theme']) {
1666          theme_reset_all_caches();
1667      }
1668      if ($options['lang']) {
1669          get_string_manager()->reset_caches();
1670      }
1671      if ($options['js']) {
1672          js_reset_all_caches();
1673      }
1674      if ($options['template']) {
1675          template_reset_all_caches();
1676      }
1677      if ($options['filter']) {
1678          reset_text_filters_cache();
1679      }
1680      if ($options['other']) {
1681          purge_other_caches();
1682      }
1683  }
1684  
1685  /**
1686   * Purge all non-MUC caches not otherwise purged in purge_caches.
1687   *
1688   * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
1689   * {@link phpunit_util::reset_dataroot()}
1690   */
1691  function purge_other_caches() {
1692      global $DB, $CFG;
1693      core_text::reset_caches();
1694      if (class_exists('core_plugin_manager')) {
1695          core_plugin_manager::reset_caches();
1696      }
1697  
1698      // Bump up cacherev field for all courses.
1699      try {
1700          increment_revision_number('course', 'cacherev', '');
1701      } catch (moodle_exception $e) {
1702          // Ignore exception since this function is also called before upgrade script when field course.cacherev does not exist yet.
1703      }
1704  
1705      $DB->reset_caches();
1706  
1707      // Purge all other caches: rss, simplepie, etc.
1708      clearstatcache();
1709      remove_dir($CFG->cachedir.'', true);
1710  
1711      // Make sure cache dir is writable, throws exception if not.
1712      make_cache_directory('');
1713  
1714      // This is the only place where we purge local caches, we are only adding files there.
1715      // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes.
1716      remove_dir($CFG->localcachedir, true);
1717      set_config('localcachedirpurged', time());
1718      make_localcache_directory('', true);
1719      \core\task\manager::clear_static_caches();
1720  }
1721  
1722  /**
1723   * Get volatile flags
1724   *
1725   * @param string $type
1726   * @param int $changedsince default null
1727   * @return array records array
1728   */
1729  function get_cache_flags($type, $changedsince = null) {
1730      global $DB;
1731  
1732      $params = array('type' => $type, 'expiry' => time());
1733      $sqlwhere = "flagtype = :type AND expiry >= :expiry";
1734      if ($changedsince !== null) {
1735          $params['changedsince'] = $changedsince;
1736          $sqlwhere .= " AND timemodified > :changedsince";
1737      }
1738      $cf = array();
1739      if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) {
1740          foreach ($flags as $flag) {
1741              $cf[$flag->name] = $flag->value;
1742          }
1743      }
1744      return $cf;
1745  }
1746  
1747  /**
1748   * Get volatile flags
1749   *
1750   * @param string $type
1751   * @param string $name
1752   * @param int $changedsince default null
1753   * @return string|false The cache flag value or false
1754   */
1755  function get_cache_flag($type, $name, $changedsince=null) {
1756      global $DB;
1757  
1758      $params = array('type' => $type, 'name' => $name, 'expiry' => time());
1759  
1760      $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry";
1761      if ($changedsince !== null) {
1762          $params['changedsince'] = $changedsince;
1763          $sqlwhere .= " AND timemodified > :changedsince";
1764      }
1765  
1766      return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params);
1767  }
1768  
1769  /**
1770   * Set a volatile flag
1771   *
1772   * @param string $type the "type" namespace for the key
1773   * @param string $name the key to set
1774   * @param string $value the value to set (without magic quotes) - null will remove the flag
1775   * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs
1776   * @return bool Always returns true
1777   */
1778  function set_cache_flag($type, $name, $value, $expiry = null) {
1779      global $DB;
1780  
1781      $timemodified = time();
1782      if ($expiry === null || $expiry < $timemodified) {
1783          $expiry = $timemodified + 24 * 60 * 60;
1784      } else {
1785          $expiry = (int)$expiry;
1786      }
1787  
1788      if ($value === null) {
1789          unset_cache_flag($type, $name);
1790          return true;
1791      }
1792  
1793      if ($f = $DB->get_record('cache_flags', array('name' => $name, 'flagtype' => $type), '*', IGNORE_MULTIPLE)) {
1794          // This is a potential problem in DEBUG_DEVELOPER.
1795          if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) {
1796              return true; // No need to update.
1797          }
1798          $f->value        = $value;
1799          $f->expiry       = $expiry;
1800          $f->timemodified = $timemodified;
1801          $DB->update_record('cache_flags', $f);
1802      } else {
1803          $f = new stdClass();
1804          $f->flagtype     = $type;
1805          $f->name         = $name;
1806          $f->value        = $value;
1807          $f->expiry       = $expiry;
1808          $f->timemodified = $timemodified;
1809          $DB->insert_record('cache_flags', $f);
1810      }
1811      return true;
1812  }
1813  
1814  /**
1815   * Removes a single volatile flag
1816   *
1817   * @param string $type the "type" namespace for the key
1818   * @param string $name the key to set
1819   * @return bool
1820   */
1821  function unset_cache_flag($type, $name) {
1822      global $DB;
1823      $DB->delete_records('cache_flags', array('name' => $name, 'flagtype' => $type));
1824      return true;
1825  }
1826  
1827  /**
1828   * Garbage-collect volatile flags
1829   *
1830   * @return bool Always returns true
1831   */
1832  function gc_cache_flags() {
1833      global $DB;
1834      $DB->delete_records_select('cache_flags', 'expiry < ?', array(time()));
1835      return true;
1836  }
1837  
1838  // USER PREFERENCE API.
1839  
1840  /**
1841   * Refresh user preference cache. This is used most often for $USER
1842   * object that is stored in session, but it also helps with performance in cron script.
1843   *
1844   * Preferences for each user are loaded on first use on every page, then again after the timeout expires.
1845   *
1846   * @package  core
1847   * @category preference
1848   * @access   public
1849   * @param    stdClass         $user          User object. Preferences are preloaded into 'preference' property
1850   * @param    int              $cachelifetime Cache life time on the current page (in seconds)
1851   * @throws   coding_exception
1852   * @return   null
1853   */
1854  function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120) {
1855      global $DB;
1856      // Static cache, we need to check on each page load, not only every 2 minutes.
1857      static $loadedusers = array();
1858  
1859      if (!isset($user->id)) {
1860          throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field');
1861      }
1862  
1863      if (empty($user->id) or isguestuser($user->id)) {
1864          // No permanent storage for not-logged-in users and guest.
1865          if (!isset($user->preference)) {
1866              $user->preference = array();
1867          }
1868          return;
1869      }
1870  
1871      $timenow = time();
1872  
1873      if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) {
1874          // Already loaded at least once on this page. Are we up to date?
1875          if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) {
1876              // No need to reload - we are on the same page and we loaded prefs just a moment ago.
1877              return;
1878  
1879          } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) {
1880              // No change since the lastcheck on this page.
1881              $user->preference['_lastloaded'] = $timenow;
1882              return;
1883          }
1884      }
1885  
1886      // OK, so we have to reload all preferences.
1887      $loadedusers[$user->id] = true;
1888      $user->preference = $DB->get_records_menu('user_preferences', array('userid' => $user->id), '', 'name,value'); // All values.
1889      $user->preference['_lastloaded'] = $timenow;
1890  }
1891  
1892  /**
1893   * Called from set/unset_user_preferences, so that the prefs can be correctly reloaded in different sessions.
1894   *
1895   * NOTE: internal function, do not call from other code.
1896   *
1897   * @package core
1898   * @access private
1899   * @param integer $userid the user whose prefs were changed.
1900   */
1901  function mark_user_preferences_changed($userid) {
1902      global $CFG;
1903  
1904      if (empty($userid) or isguestuser($userid)) {
1905          // No cache flags for guest and not-logged-in users.
1906          return;
1907      }
1908  
1909      set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout);
1910  }
1911  
1912  /**
1913   * Sets a preference for the specified user.
1914   *
1915   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1916   *
1917   * When additional validation/permission check is needed it is better to use {@see useredit_update_user_preference()}
1918   *
1919   * @package  core
1920   * @category preference
1921   * @access   public
1922   * @param    string            $name  The key to set as preference for the specified user
1923   * @param    string            $value The value to set for the $name key in the specified user's
1924   *                                    record, null means delete current value.
1925   * @param    stdClass|int|null $user  A moodle user object or id, null means current user
1926   * @throws   coding_exception
1927   * @return   bool                     Always true or exception
1928   */
1929  function set_user_preference($name, $value, $user = null) {
1930      global $USER, $DB;
1931  
1932      if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1933          throw new coding_exception('Invalid preference name in set_user_preference() call');
1934      }
1935  
1936      if (is_null($value)) {
1937          // Null means delete current.
1938          return unset_user_preference($name, $user);
1939      } else if (is_object($value)) {
1940          throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed');
1941      } else if (is_array($value)) {
1942          throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed');
1943      }
1944      // Value column maximum length is 1333 characters.
1945      $value = (string)$value;
1946      if (core_text::strlen($value) > 1333) {
1947          throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column');
1948      }
1949  
1950      if (is_null($user)) {
1951          $user = $USER;
1952      } else if (isset($user->id)) {
1953          // It is a valid object.
1954      } else if (is_numeric($user)) {
1955          $user = (object)array('id' => (int)$user);
1956      } else {
1957          throw new coding_exception('Invalid $user parameter in set_user_preference() call');
1958      }
1959  
1960      check_user_preferences_loaded($user);
1961  
1962      if (empty($user->id) or isguestuser($user->id)) {
1963          // No permanent storage for not-logged-in users and guest.
1964          $user->preference[$name] = $value;
1965          return true;
1966      }
1967  
1968      if ($preference = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => $name))) {
1969          if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) {
1970              // Preference already set to this value.
1971              return true;
1972          }
1973          $DB->set_field('user_preferences', 'value', $value, array('id' => $preference->id));
1974  
1975      } else {
1976          $preference = new stdClass();
1977          $preference->userid = $user->id;
1978          $preference->name   = $name;
1979          $preference->value  = $value;
1980          $DB->insert_record('user_preferences', $preference);
1981      }
1982  
1983      // Update value in cache.
1984      $user->preference[$name] = $value;
1985      // Update the $USER in case where we've not a direct reference to $USER.
1986      if ($user !== $USER && $user->id == $USER->id) {
1987          $USER->preference[$name] = $value;
1988      }
1989  
1990      // Set reload flag for other sessions.
1991      mark_user_preferences_changed($user->id);
1992  
1993      return true;
1994  }
1995  
1996  /**
1997   * Sets a whole array of preferences for the current user
1998   *
1999   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
2000   *
2001   * @package  core
2002   * @category preference
2003   * @access   public
2004   * @param    array             $prefarray An array of key/value pairs to be set
2005   * @param    stdClass|int|null $user      A moodle user object or id, null means current user
2006   * @return   bool                         Always true or exception
2007   */
2008  function set_user_preferences(array $prefarray, $user = null) {
2009      foreach ($prefarray as $name => $value) {
2010          set_user_preference($name, $value, $user);
2011      }
2012      return true;
2013  }
2014  
2015  /**
2016   * Unsets a preference completely by deleting it from the database
2017   *
2018   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
2019   *
2020   * @package  core
2021   * @category preference
2022   * @access   public
2023   * @param    string            $name The key to unset as preference for the specified user
2024   * @param    stdClass|int|null $user A moodle user object or id, null means current user
2025   * @throws   coding_exception
2026   * @return   bool                    Always true or exception
2027   */
2028  function unset_user_preference($name, $user = null) {
2029      global $USER, $DB;
2030  
2031      if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
2032          throw new coding_exception('Invalid preference name in unset_user_preference() call');
2033      }
2034  
2035      if (is_null($user)) {
2036          $user = $USER;
2037      } else if (isset($user->id)) {
2038          // It is a valid object.
2039      } else if (is_numeric($user)) {
2040          $user = (object)array('id' => (int)$user);
2041      } else {
2042          throw new coding_exception('Invalid $user parameter in unset_user_preference() call');
2043      }
2044  
2045      check_user_preferences_loaded($user);
2046  
2047      if (empty($user->id) or isguestuser($user->id)) {
2048          // No permanent storage for not-logged-in user and guest.
2049          unset($user->preference[$name]);
2050          return true;
2051      }
2052  
2053      // Delete from DB.
2054      $DB->delete_records('user_preferences', array('userid' => $user->id, 'name' => $name));
2055  
2056      // Delete the preference from cache.
2057      unset($user->preference[$name]);
2058      // Update the $USER in case where we've not a direct reference to $USER.
2059      if ($user !== $USER && $user->id == $USER->id) {
2060          unset($USER->preference[$name]);
2061      }
2062  
2063      // Set reload flag for other sessions.
2064      mark_user_preferences_changed($user->id);
2065  
2066      return true;
2067  }
2068  
2069  /**
2070   * Used to fetch user preference(s)
2071   *
2072   * If no arguments are supplied this function will return
2073   * all of the current user preferences as an array.
2074   *
2075   * If a name is specified then this function
2076   * attempts to return that particular preference value.  If
2077   * none is found, then the optional value $default is returned,
2078   * otherwise null.
2079   *
2080   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
2081   *
2082   * @package  core
2083   * @category preference
2084   * @access   public
2085   * @param    string            $name    Name of the key to use in finding a preference value
2086   * @param    mixed|null        $default Value to be returned if the $name key is not set in the user preferences
2087   * @param    stdClass|int|null $user    A moodle user object or id, null means current user
2088   * @throws   coding_exception
2089   * @return   string|mixed|null          A string containing the value of a single preference. An
2090   *                                      array with all of the preferences or null
2091   */
2092  function get_user_preferences($name = null, $default = null, $user = null) {
2093      global $USER;
2094  
2095      if (is_null($name)) {
2096          // All prefs.
2097      } else if (is_numeric($name) or $name === '_lastloaded') {
2098          throw new coding_exception('Invalid preference name in get_user_preferences() call');
2099      }
2100  
2101      if (is_null($user)) {
2102          $user = $USER;
2103      } else if (isset($user->id)) {
2104          // Is a valid object.
2105      } else if (is_numeric($user)) {
2106          if ($USER->id == $user) {
2107              $user = $USER;
2108          } else {
2109              $user = (object)array('id' => (int)$user);
2110          }
2111      } else {
2112          throw new coding_exception('Invalid $user parameter in get_user_preferences() call');
2113      }
2114  
2115      check_user_preferences_loaded($user);
2116  
2117      if (empty($name)) {
2118          // All values.
2119          return $user->preference;
2120      } else if (isset($user->preference[$name])) {
2121          // The single string value.
2122          return $user->preference[$name];
2123      } else {
2124          // Default value (null if not specified).
2125          return $default;
2126      }
2127  }
2128  
2129  // FUNCTIONS FOR HANDLING TIME.
2130  
2131  /**
2132   * Given Gregorian date parts in user time produce a GMT timestamp.
2133   *
2134   * @package core
2135   * @category time
2136   * @param int $year The year part to create timestamp of
2137   * @param int $month The month part to create timestamp of
2138   * @param int $day The day part to create timestamp of
2139   * @param int $hour The hour part to create timestamp of
2140   * @param int $minute The minute part to create timestamp of
2141   * @param int $second The second part to create timestamp of
2142   * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset.
2143   *             if 99 then default user's timezone is used {@link http://docs.moodle.org/dev/Time_API#Timezone}
2144   * @param bool $applydst Toggle Daylight Saving Time, default true, will be
2145   *             applied only if timezone is 99 or string.
2146   * @return int GMT timestamp
2147   */
2148  function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) {
2149      $date = new DateTime('now', core_date::get_user_timezone_object($timezone));
2150      $date->setDate((int)$year, (int)$month, (int)$day);
2151      $date->setTime((int)$hour, (int)$minute, (int)$second);
2152  
2153      $time = $date->getTimestamp();
2154  
2155      if ($time === false) {
2156          throw new coding_exception('getTimestamp() returned false, please ensure you have passed correct values.'.
2157              ' This can fail if year is more than 2038 and OS is 32 bit windows');
2158      }
2159  
2160      // Moodle BC DST stuff.
2161      if (!$applydst) {
2162          $time += dst_offset_on($time, $timezone);
2163      }
2164  
2165      return $time;
2166  
2167  }
2168  
2169  /**
2170   * Format a date/time (seconds) as weeks, days, hours etc as needed
2171   *
2172   * Given an amount of time in seconds, returns string
2173   * formatted nicely as years, days, hours etc as needed
2174   *
2175   * @package core
2176   * @category time
2177   * @uses MINSECS
2178   * @uses HOURSECS
2179   * @uses DAYSECS
2180   * @uses YEARSECS
2181   * @param int $totalsecs Time in seconds
2182   * @param stdClass $str Should be a time object
2183   * @return string A nicely formatted date/time string
2184   */
2185  function format_time($totalsecs, $str = null) {
2186  
2187      $totalsecs = abs($totalsecs);
2188  
2189      if (!$str) {
2190          // Create the str structure the slow way.
2191          $str = new stdClass();
2192          $str->day   = get_string('day');
2193          $str->days  = get_string('days');
2194          $str->hour  = get_string('hour');
2195          $str->hours = get_string('hours');
2196          $str->min   = get_string('min');
2197          $str->mins  = get_string('mins');
2198          $str->sec   = get_string('sec');
2199          $str->secs  = get_string('secs');
2200          $str->year  = get_string('year');
2201          $str->years = get_string('years');
2202      }
2203  
2204      $years     = floor($totalsecs/YEARSECS);
2205      $remainder = $totalsecs - ($years*YEARSECS);
2206      $days      = floor($remainder/DAYSECS);
2207      $remainder = $totalsecs - ($days*DAYSECS);
2208      $hours     = floor($remainder/HOURSECS);
2209      $remainder = $remainder - ($hours*HOURSECS);
2210      $mins      = floor($remainder/MINSECS);
2211      $secs      = $remainder - ($mins*MINSECS);
2212  
2213      $ss = ($secs == 1)  ? $str->sec  : $str->secs;
2214      $sm = ($mins == 1)  ? $str->min  : $str->mins;
2215      $sh = ($hours == 1) ? $str->hour : $str->hours;
2216      $sd = ($days == 1)  ? $str->day  : $str->days;
2217      $sy = ($years == 1)  ? $str->year  : $str->years;
2218  
2219      $oyears = '';
2220      $odays = '';
2221      $ohours = '';
2222      $omins = '';
2223      $osecs = '';
2224  
2225      if ($years) {
2226          $oyears  = $years .' '. $sy;
2227      }
2228      if ($days) {
2229          $odays  = $days .' '. $sd;
2230      }
2231      if ($hours) {
2232          $ohours = $hours .' '. $sh;
2233      }
2234      if ($mins) {
2235          $omins  = $mins .' '. $sm;
2236      }
2237      if ($secs) {
2238          $osecs  = $secs .' '. $ss;
2239      }
2240  
2241      if ($years) {
2242          return trim($oyears .' '. $odays);
2243      }
2244      if ($days) {
2245          return trim($odays .' '. $ohours);
2246      }
2247      if ($hours) {
2248          return trim($ohours .' '. $omins);
2249      }
2250      if ($mins) {
2251          return trim($omins .' '. $osecs);
2252      }
2253      if ($secs) {
2254          return $osecs;
2255      }
2256      return get_string('now');
2257  }
2258  
2259  /**
2260   * Returns a formatted string that represents a date in user time.
2261   *
2262   * @package core
2263   * @category time
2264   * @param int $date the timestamp in UTC, as obtained from the database.
2265   * @param string $format strftime format. You should probably get this using
2266   *        get_string('strftime...', 'langconfig');
2267   * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
2268   *        not 99 then daylight saving will not be added.
2269   *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
2270   * @param bool $fixday If true (default) then the leading zero from %d is removed.
2271   *        If false then the leading zero is maintained.
2272   * @param bool $fixhour If true (default) then the leading zero from %I is removed.
2273   * @return string the formatted date/time.
2274   */
2275  function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
2276      $calendartype = \core_calendar\type_factory::get_calendar_instance();
2277      return $calendartype->timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour);
2278  }
2279  
2280  /**
2281   * Returns a html "time" tag with both the exact user date with timezone information
2282   * as a datetime attribute in the W3C format, and the user readable date and time as text.
2283   *
2284   * @package core
2285   * @category time
2286   * @param int $date the timestamp in UTC, as obtained from the database.
2287   * @param string $format strftime format. You should probably get this using
2288   *        get_string('strftime...', 'langconfig');
2289   * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
2290   *        not 99 then daylight saving will not be added.
2291   *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
2292   * @param bool $fixday If true (default) then the leading zero from %d is removed.
2293   *        If false then the leading zero is maintained.
2294   * @param bool $fixhour If true (default) then the leading zero from %I is removed.
2295   * @return string the formatted date/time.
2296   */
2297  function userdate_htmltime($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
2298      $userdatestr = userdate($date, $format, $timezone, $fixday, $fixhour);
2299      if (CLI_SCRIPT && !PHPUNIT_TEST) {
2300          return $userdatestr;
2301      }
2302      $machinedate = new DateTime();
2303      $machinedate->setTimestamp(intval($date));
2304      $machinedate->setTimezone(core_date::get_user_timezone_object());
2305  
2306      return html_writer::tag('time', $userdatestr, ['datetime' => $machinedate->format(DateTime::W3C)]);
2307  }
2308  
2309  /**
2310   * Returns a formatted date ensuring it is UTF-8.
2311   *
2312   * If we are running under Windows convert to Windows encoding and then back to UTF-8
2313   * (because it's impossible to specify UTF-8 to fetch locale info in Win32).
2314   *
2315   * @param int $date the timestamp - since Moodle 2.9 this is a real UTC timestamp
2316   * @param string $format strftime format.
2317   * @param int|float|string $tz the user timezone
2318   * @return string the formatted date/time.
2319   * @since Moodle 2.3.3
2320   */
2321  function date_format_string($date, $format, $tz = 99) {
2322      global $CFG;
2323  
2324      $localewincharset = null;
2325      // Get the calendar type user is using.
2326      if ($CFG->ostype == 'WINDOWS') {
2327          $calendartype = \core_calendar\type_factory::get_calendar_instance();
2328          $localewincharset = $calendartype->locale_win_charset();
2329      }
2330  
2331      if ($localewincharset) {
2332          $format = core_text::convert($format, 'utf-8', $localewincharset);
2333      }
2334  
2335      date_default_timezone_set(core_date::get_user_timezone($tz));
2336      $datestring = strftime($format, $date);
2337      core_date::set_default_server_timezone();
2338  
2339      if ($localewincharset) {
2340          $datestring = core_text::convert($datestring, $localewincharset, 'utf-8');
2341      }
2342  
2343      return $datestring;
2344  }
2345  
2346  /**
2347   * Given a $time timestamp in GMT (seconds since epoch),
2348   * returns an array that represents the Gregorian date in user time
2349   *
2350   * @package core
2351   * @category time
2352   * @param int $time Timestamp in GMT
2353   * @param float|int|string $timezone user timezone
2354   * @return array An array that represents the date in user time
2355   */
2356  function usergetdate($time, $timezone=99) {
2357      date_default_timezone_set(core_date::get_user_timezone($timezone));
2358      $result = getdate($time);
2359      core_date::set_default_server_timezone();
2360  
2361      return $result;
2362  }
2363  
2364  /**
2365   * Given a GMT timestamp (seconds since epoch), offsets it by
2366   * the timezone.  eg 3pm in India is 3pm GMT - 7 * 3600 seconds
2367   *
2368   * NOTE: this function does not include DST properly,
2369   *       you should use the PHP date stuff instead!
2370   *
2371   * @package core
2372   * @category time
2373   * @param int $date Timestamp in GMT
2374   * @param float|int|string $timezone user timezone
2375   * @return int
2376   */
2377  function usertime($date, $timezone=99) {
2378      $userdate = new DateTime('@' . $date);
2379      $userdate->setTimezone(core_date::get_user_timezone_object($timezone));
2380      $dst = dst_offset_on($date, $timezone);
2381  
2382      return $date - $userdate->getOffset() + $dst;
2383  }
2384  
2385  /**
2386   * Get a formatted string representation of an interval between two unix timestamps.
2387   *
2388   * E.g.
2389   * $intervalstring = get_time_interval_string(12345600, 12345660);
2390   * Will produce the string:
2391   * '0d 0h 1m'
2392   *
2393   * @param int $time1 unix timestamp
2394   * @param int $time2 unix timestamp
2395   * @param string $format string (can be lang string) containing format chars: https://www.php.net/manual/en/dateinterval.format.php.
2396   * @return string the formatted string describing the time difference, e.g. '10d 11h 45m'.
2397   */
2398  function get_time_interval_string(int $time1, int $time2, string $format = ''): string {
2399      $dtdate = new DateTime();
2400      $dtdate->setTimeStamp($time1);
2401      $dtdate2 = new DateTime();
2402      $dtdate2->setTimeStamp($time2);
2403      $interval = $dtdate2->diff($dtdate);
2404      $format = empty($format) ? get_string('dateintervaldayshoursmins', 'langconfig') : $format;
2405      return $interval->format($format);
2406  }
2407  
2408  /**
2409   * Given a time, return the GMT timestamp of the most recent midnight
2410   * for the current user.
2411   *
2412   * @package core
2413   * @category time
2414   * @param int $date Timestamp in GMT
2415   * @param float|int|string $timezone user timezone
2416   * @return int Returns a GMT timestamp
2417   */
2418  function usergetmidnight($date, $timezone=99) {
2419  
2420      $userdate = usergetdate($date, $timezone);
2421  
2422      // Time of midnight of this user's day, in GMT.
2423      return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone);
2424  
2425  }
2426  
2427  /**
2428   * Returns a string that prints the user's timezone
2429   *
2430   * @package core
2431   * @category time
2432   * @param float|int|string $timezone user timezone
2433   * @return string
2434   */
2435  function usertimezone($timezone=99) {
2436      $tz = core_date::get_user_timezone($timezone);
2437      return core_date::get_localised_timezone($tz);
2438  }
2439  
2440  /**
2441   * Returns a float or a string which denotes the user's timezone
2442   * 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)
2443   * means that for this timezone there are also DST rules to be taken into account
2444   * Checks various settings and picks the most dominant of those which have a value
2445   *
2446   * @package core
2447   * @category time
2448   * @param float|int|string $tz timezone to calculate GMT time offset before
2449   *        calculating user timezone, 99 is default user timezone
2450   *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
2451   * @return float|string
2452   */
2453  function get_user_timezone($tz = 99) {
2454      global $USER, $CFG;
2455  
2456      $timezones = array(
2457          $tz,
2458          isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99,
2459          isset($USER->timezone) ? $USER->timezone : 99,
2460          isset($CFG->timezone) ? $CFG->timezone : 99,
2461          );
2462  
2463      $tz = 99;
2464  
2465      // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array.
2466      foreach ($timezones as $nextvalue) {
2467          if ((empty($tz) && !is_numeric($tz)) || $tz == 99) {
2468              $tz = $nextvalue;
2469          }
2470      }
2471      return is_numeric($tz) ? (float) $tz : $tz;
2472  }
2473  
2474  /**
2475   * Calculates the Daylight Saving Offset for a given date/time (timestamp)
2476   * - Note: Daylight saving only works for string timezones and not for float.
2477   *
2478   * @package core
2479   * @category time
2480   * @param int $time must NOT be compensated at all, it has to be a pure timestamp
2481   * @param int|float|string $strtimezone user timezone
2482   * @return int
2483   */
2484  function dst_offset_on($time, $strtimezone = null) {
2485      $tz = core_date::get_user_timezone($strtimezone);
2486      $date = new DateTime('@' . $time);
2487      $date->setTimezone(new DateTimeZone($tz));
2488      if ($date->format('I') == '1') {
2489          if ($tz === 'Australia/Lord_Howe') {
2490              return 1800;
2491          }
2492          return 3600;
2493      }
2494      return 0;
2495  }
2496  
2497  /**
2498   * Calculates when the day appears in specific month
2499   *
2500   * @package core
2501   * @category time
2502   * @param int $startday starting day of the month
2503   * @param int $weekday The day when week starts (normally taken from user preferences)
2504   * @param int $month The month whose day is sought
2505   * @param int $year The year of the month whose day is sought
2506   * @return int
2507   */
2508  function find_day_in_month($startday, $weekday, $month, $year) {
2509      $calendartype = \core_calendar\type_factory::get_calendar_instance();
2510  
2511      $daysinmonth = days_in_month($month, $year);
2512      $daysinweek = count($calendartype->get_weekdays());
2513  
2514      if ($weekday == -1) {
2515          // Don't care about weekday, so return:
2516          //    abs($startday) if $startday != -1
2517          //    $daysinmonth otherwise.
2518          return ($startday == -1) ? $daysinmonth : abs($startday);
2519      }
2520  
2521      // From now on we 're looking for a specific weekday.
2522      // Give "end of month" its actual value, since we know it.
2523      if ($startday == -1) {
2524          $startday = -1 * $daysinmonth;
2525      }
2526  
2527      // Starting from day $startday, the sign is the direction.
2528      if ($startday < 1) {
2529          $startday = abs($startday);
2530          $lastmonthweekday = dayofweek($daysinmonth, $month, $year);
2531  
2532          // This is the last such weekday of the month.
2533          $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday;
2534          if ($lastinmonth > $daysinmonth) {
2535              $lastinmonth -= $daysinweek;
2536          }
2537  
2538          // Find the first such weekday <= $startday.
2539          while ($lastinmonth > $startday) {
2540              $lastinmonth -= $daysinweek;
2541          }
2542  
2543          return $lastinmonth;
2544      } else {
2545          $indexweekday = dayofweek($startday, $month, $year);
2546  
2547          $diff = $weekday - $indexweekday;
2548          if ($diff < 0) {
2549              $diff += $daysinweek;
2550          }
2551  
2552          // This is the first such weekday of the month equal to or after $startday.
2553          $firstfromindex = $startday + $diff;
2554  
2555          return $firstfromindex;
2556      }
2557  }
2558  
2559  /**
2560   * Calculate the number of days in a given month
2561   *
2562   * @package core
2563   * @category time
2564   * @param int $month The month whose day count is sought
2565   * @param int $year The year of the month whose day count is sought
2566   * @return int
2567   */
2568  function days_in_month($month, $year) {
2569      $calendartype = \core_calendar\type_factory::get_calendar_instance();
2570      return $calendartype->get_num_days_in_month($year, $month);
2571  }
2572  
2573  /**
2574   * Calculate the position in the week of a specific calendar day
2575   *
2576   * @package core
2577   * @category time
2578   * @param int $day The day of the date whose position in the week is sought
2579   * @param int $month The month of the date whose position in the week is sought
2580   * @param int $year The year of the date whose position in the week is sought
2581   * @return int
2582   */
2583  function dayofweek($day, $month, $year) {
2584      $calendartype = \core_calendar\type_factory::get_calendar_instance();
2585      return $calendartype->get_weekday($year, $month, $day);
2586  }
2587  
2588  // USER AUTHENTICATION AND LOGIN.
2589  
2590  /**
2591   * Returns full login url.
2592   *
2593   * Any form submissions for authentication to this URL must include username,
2594   * password as well as a logintoken generated by \core\session\manager::get_login_token().
2595   *
2596   * @return string login url
2597   */
2598  function get_login_url() {
2599      global $CFG;
2600  
2601      return "$CFG->wwwroot/login/index.php";
2602  }
2603  
2604  /**
2605   * This function checks that the current user is logged in and has the
2606   * required privileges
2607   *
2608   * This function checks that the current user is logged in, and optionally
2609   * whether they are allowed to be in a particular course and view a particular
2610   * course module.
2611   * If they are not logged in, then it redirects them to the site login unless
2612   * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which
2613   * case they are automatically logged in as guests.
2614   * If $courseid is given and the user is not enrolled in that course then the
2615   * user is redirected to the course enrolment page.
2616   * If $cm is given and the course module is hidden and the user is not a teacher
2617   * in the course then the user is redirected to the course home page.
2618   *
2619   * When $cm parameter specified, this function sets page layout to 'module'.
2620   * You need to change it manually later if some other layout needed.
2621   *
2622   * @package    core_access
2623   * @category   access
2624   *
2625   * @param mixed $courseorid id of the course or course object
2626   * @param bool $autologinguest default true
2627   * @param object $cm course module object
2628   * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2629   *             true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2630   *             in order to keep redirects working properly. MDL-14495
2631   * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2632   * @return mixed Void, exit, and die depending on path
2633   * @throws coding_exception
2634   * @throws require_login_exception
2635   * @throws moodle_exception
2636   */
2637  function require_login($courseorid = null, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
2638      global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT;
2639  
2640      // Must not redirect when byteserving already started.
2641      if (!empty($_SERVER['HTTP_RANGE'])) {
2642          $preventredirect = true;
2643      }
2644  
2645      if (AJAX_SCRIPT) {
2646          // We cannot redirect for AJAX scripts either.
2647          $preventredirect = true;
2648      }
2649  
2650      // Setup global $COURSE, themes, language and locale.
2651      if (!empty($courseorid)) {
2652          if (is_object($courseorid)) {
2653              $course = $courseorid;
2654          } else if ($courseorid == SITEID) {
2655              $course = clone($SITE);
2656          } else {
2657              $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST);
2658          }
2659          if ($cm) {
2660              if ($cm->course != $course->id) {
2661                  throw new coding_exception('course and cm parameters in require_login() call do not match!!');
2662              }
2663              // Make sure we have a $cm from get_fast_modinfo as this contains activity access details.
2664              if (!($cm instanceof cm_info)) {
2665                  // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
2666                  // db queries so this is not really a performance concern, however it is obviously
2667                  // better if you use get_fast_modinfo to get the cm before calling this.
2668                  $modinfo = get_fast_modinfo($course);
2669                  $cm = $modinfo->get_cm($cm->id);
2670              }
2671          }
2672      } else {
2673          // Do not touch global $COURSE via $PAGE->set_course(),
2674          // the reasons is we need to be able to call require_login() at any time!!
2675          $course = $SITE;
2676          if ($cm) {
2677              throw new coding_exception('cm parameter in require_login() requires valid course parameter!');
2678          }
2679      }
2680  
2681      // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false.
2682      // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future
2683      // risk leading the user back to the AJAX request URL.
2684      if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) {
2685          $setwantsurltome = false;
2686      }
2687  
2688      // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour.
2689      if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !empty($CFG->dbsessions)) {
2690          if ($preventredirect) {
2691              throw new require_login_session_timeout_exception();
2692          } else {
2693              if ($setwantsurltome) {
2694                  $SESSION->wantsurl = qualified_me();
2695              }
2696              redirect(get_login_url());
2697          }
2698      }
2699  
2700      // If the user is not even logged in yet then make sure they are.
2701      if (!isloggedin()) {
2702          if ($autologinguest and !empty($CFG->guestloginbutton) and !empty($CFG->autologinguests)) {
2703              if (!$guest = get_complete_user_data('id', $CFG->siteguest)) {
2704                  // Misconfigured site guest, just redirect to login page.
2705                  redirect(get_login_url());
2706                  exit; // Never reached.
2707              }
2708              $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang;
2709              complete_user_login($guest);
2710              $USER->autologinguest = true;
2711              $SESSION->lang = $lang;
2712          } else {
2713              // NOTE: $USER->site check was obsoleted by session test cookie, $USER->confirmed test is in login/index.php.
2714              if ($preventredirect) {
2715                  throw new require_login_exception('You are not logged in');
2716              }
2717  
2718              if ($setwantsurltome) {
2719                  $SESSION->wantsurl = qualified_me();
2720              }
2721  
2722              $referer = get_local_referer(false);
2723              if (!empty($referer)) {
2724                  $SESSION->fromurl = $referer;
2725              }
2726  
2727              // Give auth plugins an opportunity to authenticate or redirect to an external login page
2728              $authsequence = get_enabled_auth_plugins(); // Auths, in sequence.
2729              foreach($authsequence as $authname) {
2730                  $authplugin = get_auth_plugin($authname);
2731                  $authplugin->pre_loginpage_hook();
2732                  if (isloggedin()) {
2733                      if ($cm) {
2734                          $modinfo = get_fast_modinfo($course);
2735                          $cm = $modinfo->get_cm($cm->id);
2736                      }
2737                      set_access_log_user();
2738                      break;
2739                  }
2740              }
2741  
2742              // If we're still not logged in then go to the login page
2743              if (!isloggedin()) {
2744                  redirect(get_login_url());
2745                  exit; // Never reached.
2746              }
2747          }
2748      }
2749  
2750      // Loginas as redirection if needed.
2751      if ($course->id != SITEID and \core\session\manager::is_loggedinas()) {
2752          if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) {
2753              if ($USER->loginascontext->instanceid != $course->id) {
2754                  print_error('loginasonecourse', '', $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid);
2755              }
2756          }
2757      }
2758  
2759      // Check whether the user should be changing password (but only if it is REALLY them).
2760      if (get_user_preferences('auth_forcepasswordchange') && !\core\session\manager::is_loggedinas()) {
2761          $userauth = get_auth_plugin($USER->auth);
2762          if ($userauth->can_change_password() and !$preventredirect) {
2763              if ($setwantsurltome) {
2764                  $SESSION->wantsurl = qualified_me();
2765              }
2766              if ($changeurl = $userauth->change_password_url()) {
2767                  // Use plugin custom url.
2768                  redirect($changeurl);
2769              } else {
2770                  // Use moodle internal method.
2771                  redirect($CFG->wwwroot .'/login/change_password.php');
2772              }
2773          } else if ($userauth->can_change_password()) {
2774              throw new moodle_exception('forcepasswordchangenotice');
2775          } else {
2776              throw new moodle_exception('nopasswordchangeforced', 'auth');
2777          }
2778      }
2779  
2780      // Check that the user account is properly set up. If we can't redirect to
2781      // edit their profile and this is not a WS request, perform just the lax check.
2782      // It will allow them to use filepicker on the profile edit page.
2783  
2784      if ($preventredirect && !WS_SERVER) {
2785          $usernotfullysetup = user_not_fully_set_up($USER, false);
2786      } else {
2787          $usernotfullysetup = user_not_fully_set_up($USER, true);
2788      }
2789  
2790      if ($usernotfullysetup) {
2791          if ($preventredirect) {
2792              throw new moodle_exception('usernotfullysetup');
2793          }
2794          if ($setwantsurltome) {
2795              $SESSION->wantsurl = qualified_me();
2796          }
2797          redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&amp;course='. SITEID);
2798      }
2799  
2800      // Make sure the USER has a sesskey set up. Used for CSRF protection.
2801      sesskey();
2802  
2803      if (\core\session\manager::is_loggedinas()) {
2804          // During a "logged in as" session we should force all content to be cleaned because the
2805          // logged in user will be viewing potentially malicious user generated content.
2806          // See MDL-63786 for more details.
2807          $CFG->forceclean = true;
2808      }
2809  
2810      $afterlogins = get_plugins_with_function('after_require_login', 'lib.php');
2811  
2812      // Do not bother admins with any formalities, except for activities pending deletion.
2813      if (is_siteadmin() && !($cm && $cm->deletioninprogress)) {
2814          // Set the global $COURSE.
2815          if ($cm) {
2816              $PAGE->set_cm($cm, $course);
2817              $PAGE->set_pagelayout('incourse');
2818          } else if (!empty($courseorid)) {
2819              $PAGE->set_course($course);
2820          }
2821          // Set accesstime or the user will appear offline which messes up messaging.
2822          // Do not update access time for webservice or ajax requests.
2823          if (!WS_SERVER && !AJAX_SCRIPT) {
2824              user_accesstime_log($course->id);
2825          }
2826  
2827          foreach ($afterlogins as $plugintype => $plugins) {
2828              foreach ($plugins as $pluginfunction) {
2829                  $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2830              }
2831          }
2832          return;
2833      }
2834  
2835      // Scripts have a chance to declare that $USER->policyagreed should not be checked.
2836      // This is mostly for places where users are actually accepting the policies, to avoid the redirect loop.
2837      if (!defined('NO_SITEPOLICY_CHECK')) {
2838          define('NO_SITEPOLICY_CHECK', false);
2839      }
2840  
2841      // Check that the user has agreed to a site policy if there is one - do not test in case of admins.
2842      // Do not test if the script explicitly asked for skipping the site policies check.
2843      if (!$USER->policyagreed && !is_siteadmin() && !NO_SITEPOLICY_CHECK) {
2844          $manager = new \core_privacy\local\sitepolicy\manager();
2845          if ($policyurl = $manager->get_redirect_url(isguestuser())) {
2846              if ($preventredirect) {
2847                  throw new moodle_exception('sitepolicynotagreed', 'error', '', $policyurl->out());
2848              }
2849              if ($setwantsurltome) {
2850                  $SESSION->wantsurl = qualified_me();
2851              }
2852              redirect($policyurl);
2853          }
2854      }
2855  
2856      // Fetch the system context, the course context, and prefetch its child contexts.
2857      $sysctx = context_system::instance();
2858      $coursecontext = context_course::instance($course->id, MUST_EXIST);
2859      if ($cm) {
2860          $cmcontext = context_module::instance($cm->id, MUST_EXIST);
2861      } else {
2862          $cmcontext = null;
2863      }
2864  
2865      // If the site is currently under maintenance, then print a message.
2866      if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:maintenanceaccess', $sysctx)) {
2867          if ($preventredirect) {
2868              throw new require_login_exception('Maintenance in progress');
2869          }
2870          $PAGE->set_context(null);
2871          print_maintenance_message();
2872      }
2873  
2874      // Make sure the course itself is not hidden.
2875      if ($course->id == SITEID) {
2876          // Frontpage can not be hidden.
2877      } else {
2878          if (is_role_switched($course->id)) {
2879              // When switching roles ignore the hidden flag - user had to be in course to do the switch.
2880          } else {
2881              if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
2882                  // Originally there was also test of parent category visibility, BUT is was very slow in complex queries
2883                  // involving "my courses" now it is also possible to simply hide all courses user is not enrolled in :-).
2884                  if ($preventredirect) {
2885                      throw new require_login_exception('Course is hidden');
2886                  }
2887                  $PAGE->set_context(null);
2888                  // We need to override the navigation URL as the course won't have been added to the navigation and thus
2889                  // the navigation will mess up when trying to find it.
2890                  navigation_node::override_active_url(new moodle_url('/'));
2891                  notice(get_string('coursehidden'), $CFG->wwwroot .'/');
2892              }
2893          }
2894      }
2895  
2896      // Is the user enrolled?
2897      if ($course->id == SITEID) {
2898          // Everybody is enrolled on the frontpage.
2899      } else {
2900          if (\core\session\manager::is_loggedinas()) {
2901              // Make sure the REAL person can access this course first.
2902              $realuser = \core\session\manager::get_realuser();
2903              if (!is_enrolled($coursecontext, $realuser->id, '', true) and
2904                  !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) {
2905                  if ($preventredirect) {
2906                      throw new require_login_exception('Invalid course login-as access');
2907                  }
2908                  $PAGE->set_context(null);
2909                  echo $OUTPUT->header();
2910                  notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/');
2911              }
2912          }
2913  
2914          $access = false;
2915  
2916          if (is_role_switched($course->id)) {
2917              // Ok, user had to be inside this course before the switch.
2918              $access = true;
2919  
2920          } else if (is_viewing($coursecontext, $USER)) {
2921              // Ok, no need to mess with enrol.
2922              $access = true;
2923  
2924          } else {
2925              if (isset($USER->enrol['enrolled'][$course->id])) {
2926                  if ($USER->enrol['enrolled'][$course->id] > time()) {
2927                      $access = true;
2928                      if (isset($USER->enrol['tempguest'][$course->id])) {
2929                          unset($USER->enrol['tempguest'][$course->id]);
2930                          remove_temp_course_roles($coursecontext);
2931                      }
2932                  } else {
2933                      // Expired.
2934                      unset($USER->enrol['enrolled'][$course->id]);
2935                  }
2936              }
2937              if (isset($USER->enrol['tempguest'][$course->id])) {
2938                  if ($USER->enrol['tempguest'][$course->id] == 0) {
2939                      $access = true;
2940                  } else if ($USER->enrol['tempguest'][$course->id] > time()) {
2941                      $access = true;
2942                  } else {
2943                      // Expired.
2944                      unset($USER->enrol['tempguest'][$course->id]);
2945                      remove_temp_course_roles($coursecontext);
2946                  }
2947              }
2948  
2949              if (!$access) {
2950                  // Cache not ok.
2951                  $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id);
2952                  if ($until !== false) {
2953                      // Active participants may always access, a timestamp in the future, 0 (always) or false.
2954                      if ($until == 0) {
2955                          $until = ENROL_MAX_TIMESTAMP;
2956                      }
2957                      $USER->enrol['enrolled'][$course->id] = $until;
2958                      $access = true;
2959  
2960                  } else if (core_course_category::can_view_course_info($course)) {
2961                      $params = array('courseid' => $course->id, 'status' => ENROL_INSTANCE_ENABLED);
2962                      $instances = $DB->get_records('enrol', $params, 'sortorder, id ASC');
2963                      $enrols = enrol_get_plugins(true);
2964                      // First ask all enabled enrol instances in course if they want to auto enrol user.
2965                      foreach ($instances as $instance) {
2966                          if (!isset($enrols[$instance->enrol])) {
2967                              continue;
2968                          }
2969                          // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false.
2970                          $until = $enrols[$instance->enrol]->try_autoenrol($instance);
2971                          if ($until !== false) {
2972                              if ($until == 0) {
2973                                  $until = ENROL_MAX_TIMESTAMP;
2974                              }
2975                              $USER->enrol['enrolled'][$course->id] = $until;
2976                              $access = true;
2977                              break;
2978                          }
2979                      }
2980                      // If not enrolled yet try to gain temporary guest access.
2981                      if (!$access) {
2982                          foreach ($instances as $instance) {
2983                              if (!isset($enrols[$instance->enrol])) {
2984                                  continue;
2985                              }
2986                              // Get a duration for the guest access, a timestamp in the future or false.
2987                              $until = $enrols[$instance->enrol]->try_guestaccess($instance);
2988                              if ($until !== false and $until > time()) {
2989                                  $USER->enrol['tempguest'][$course->id] = $until;
2990                                  $access = true;
2991                                  break;
2992                              }
2993                          }
2994                      }
2995                  } else {
2996                      // User is not enrolled and is not allowed to browse courses here.
2997                      if ($preventredirect) {
2998                          throw new require_login_exception('Course is not available');
2999                      }
3000                      $PAGE->set_context(null);
3001                      // We need to override the navigation URL as the course won't have been added to the navigation and thus
3002                      // the navigation will mess up when trying to find it.
3003                      navigation_node::override_active_url(new moodle_url('/'));
3004                      notice(get_string('coursehidden'), $CFG->wwwroot .'/');
3005                  }
3006              }
3007          }
3008  
3009          if (!$access) {
3010              if ($preventredirect) {
3011                  throw new require_login_exception('Not enrolled');
3012              }
3013              if ($setwantsurltome) {
3014                  $SESSION->wantsurl = qualified_me();
3015              }
3016              redirect($CFG->wwwroot .'/enrol/index.php?id='. $course->id);
3017          }
3018      }
3019  
3020      // Check whether the activity has been scheduled for deletion. If so, then deny access, even for admins.
3021      if ($cm && $cm->deletioninprogress) {
3022          if ($preventredirect) {
3023              throw new moodle_exception('activityisscheduledfordeletion');
3024          }
3025          require_once($CFG->dirroot . '/course/lib.php');
3026          redirect(course_get_url($course), get_string('activityisscheduledfordeletion', 'error'));
3027      }
3028  
3029      // Check visibility of activity to current user; includes visible flag, conditional availability, etc.
3030      if ($cm && !$cm->uservisible) {
3031          if ($preventredirect) {
3032              throw new require_login_exception('Activity is hidden');
3033          }
3034          // Get the error message that activity is not available and why (if explanation can be shown to the user).
3035          $PAGE->set_course($course);
3036          $renderer = $PAGE->get_renderer('course');
3037          $message = $renderer->course_section_cm_unavailable_error_message($cm);
3038          redirect(course_get_url($course), $message, null, \core\output\notification::NOTIFY_ERROR);
3039      }
3040  
3041      // Set the global $COURSE.
3042      if ($cm) {
3043          $PAGE->set_cm($cm, $course);
3044          $PAGE->set_pagelayout('incourse');
3045      } else if (!empty($courseorid)) {
3046          $PAGE->set_course($course);
3047      }
3048  
3049      foreach ($afterlogins as $plugintype => $plugins) {
3050          foreach ($plugins as $pluginfunction) {
3051              $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3052          }
3053      }
3054  
3055      // Finally access granted, update lastaccess times.
3056      // Do not update access time for webservice or ajax requests.
3057      if (!WS_SERVER && !AJAX_SCRIPT) {
3058          user_accesstime_log($course->id);
3059      }
3060  }
3061  
3062  /**
3063   * A convenience function for where we must be logged in as admin
3064   * @return void
3065   */
3066  function require_admin() {
3067      require_login(null, false);
3068      require_capability('moodle/site:config', context_system::instance());
3069  }
3070  
3071  /**
3072   * This function just makes sure a user is logged out.
3073   *
3074   * @package    core_access
3075   * @category   access
3076   */
3077  function require_logout() {
3078      global $USER, $DB;
3079  
3080      if (!isloggedin()) {
3081          // This should not happen often, no need for hooks or events here.
3082          \core\session\manager::terminate_current();
3083          return;
3084      }
3085  
3086      // Execute hooks before action.
3087      $authplugins = array();
3088      $authsequence = get_enabled_auth_plugins();
3089      foreach ($authsequence as $authname) {
3090          $authplugins[$authname] = get_auth_plugin($authname);
3091          $authplugins[$authname]->prelogout_hook();
3092      }
3093  
3094      // Store info that gets removed during logout.
3095      $sid = session_id();
3096      $event = \core\event\user_loggedout::create(
3097          array(
3098              'userid' => $USER->id,
3099              'objectid' => $USER->id,
3100              'other' => array('sessionid' => $sid),
3101          )
3102      );
3103      if ($session = $DB->get_record('sessions', array('sid'=>$sid))) {
3104          $event->add_record_snapshot('sessions', $session);
3105      }
3106  
3107      // Clone of $USER object to be used by auth plugins.
3108      $user = fullclone($USER);
3109  
3110      // Delete session record and drop $_SESSION content.
3111      \core\session\manager::terminate_current();
3112  
3113      // Trigger event AFTER action.
3114      $event->trigger();
3115  
3116      // Hook to execute auth plugins redirection after event trigger.
3117      foreach ($authplugins as $authplugin) {
3118          $authplugin->postlogout_hook($user);
3119      }
3120  }
3121  
3122  /**
3123   * Weaker version of require_login()
3124   *
3125   * This is a weaker version of {@link require_login()} which only requires login
3126   * when called from within a course rather than the site page, unless
3127   * the forcelogin option is turned on.
3128   * @see require_login()
3129   *
3130   * @package    core_access
3131   * @category   access
3132   *
3133   * @param mixed $courseorid The course object or id in question
3134   * @param bool $autologinguest Allow autologin guests if that is wanted
3135   * @param object $cm Course activity module if known
3136   * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
3137   *             true. Used to avoid (=false) some scripts (file.php...) to set that variable,
3138   *             in order to keep redirects working properly. MDL-14495
3139   * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
3140   * @return void
3141   * @throws coding_exception
3142   */
3143  function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
3144      global $CFG, $PAGE, $SITE;
3145      $issite = ((is_object($courseorid) and $courseorid->id == SITEID)
3146            or (!is_object($courseorid) and $courseorid == SITEID));
3147      if ($issite && !empty($cm) && !($cm instanceof cm_info)) {
3148          // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
3149          // db queries so this is not really a performance concern, however it is obviously
3150          // better if you use get_fast_modinfo to get the cm before calling this.
3151          if (is_object($courseorid)) {
3152              $course = $courseorid;
3153          } else {
3154              $course = clone($SITE);
3155          }
3156          $modinfo = get_fast_modinfo($course);
3157          $cm = $modinfo->get_cm($cm->id);
3158      }
3159      if (!empty($CFG->forcelogin)) {
3160          // Login required for both SITE and courses.
3161          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3162  
3163      } else if ($issite && !empty($cm) and !$cm->uservisible) {
3164          // Always login for hidden activities.
3165          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3166  
3167      } else if (isloggedin() && !isguestuser()) {
3168          // User is already logged in. Make sure the login is complete (user is fully setup, policies agreed).
3169          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3170  
3171      } else if ($issite) {
3172          // Login for SITE not required.
3173          // We still need to instatiate PAGE vars properly so that things that rely on it like navigation function correctly.
3174          if (!empty($courseorid)) {
3175              if (is_object($courseorid)) {
3176                  $course = $courseorid;
3177              } else {
3178                  $course = clone $SITE;
3179              }
3180              if ($cm) {
3181                  if ($cm->course != $course->id) {
3182                      throw new coding_exception('course and cm parameters in require_course_login() call do not match!!');
3183                  }
3184                  $PAGE->set_cm($cm, $course);
3185                  $PAGE->set_pagelayout('incourse');
3186              } else {
3187                  $PAGE->set_course($course);
3188              }
3189          } else {
3190              // If $PAGE->course, and hence $PAGE->context, have not already been set up properly, set them up now.
3191              $PAGE->set_course($PAGE->course);
3192          }
3193          // Do not update access time for webservice or ajax requests.
3194          if (!WS_SERVER && !AJAX_SCRIPT) {
3195              user_accesstime_log(SITEID);
3196          }
3197          return;
3198  
3199      } else {
3200          // Course login always required.
3201          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3202      }
3203  }
3204  
3205  /**
3206   * Validates a user key, checking if the key exists, is not expired and the remote ip is correct.
3207   *
3208   * @param  string $keyvalue the key value
3209   * @param  string $script   unique script identifier
3210   * @param  int $instance    instance id
3211   * @return stdClass the key entry in the user_private_key table
3212   * @since Moodle 3.2
3213   * @throws moodle_exception
3214   */
3215  function validate_user_key($keyvalue, $script, $instance) {
3216      global $DB;
3217  
3218      if (!$key = $DB->get_record('user_private_key', array('script' => $script, 'value' => $keyvalue, 'instance' => $instance))) {
3219          print_error('invalidkey');
3220      }
3221  
3222      if (!empty($key->validuntil) and $key->validuntil < time()) {
3223          print_error('expiredkey');
3224      }
3225  
3226      if ($key->iprestriction) {
3227          $remoteaddr = getremoteaddr(null);
3228          if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) {
3229              print_error('ipmismatch');
3230          }
3231      }
3232      return $key;
3233  }
3234  
3235  /**
3236   * Require key login. Function terminates with error if key not found or incorrect.
3237   *
3238   * @uses NO_MOODLE_COOKIES
3239   * @uses PARAM_ALPHANUM
3240   * @param string $script unique script identifier
3241   * @param int $instance optional instance id
3242   * @param string $keyvalue The key. If not supplied, this will be fetched from the current session.
3243   * @return int Instance ID
3244   */
3245  function require_user_key_login($script, $instance = null, $keyvalue = null) {
3246      global $DB;
3247  
3248      if (!NO_MOODLE_COOKIES) {
3249          print_error('sessioncookiesdisable');
3250      }
3251  
3252      // Extra safety.
3253      \core\session\manager::write_close();
3254  
3255      if (null === $keyvalue) {
3256          $keyvalue = required_param('key', PARAM_ALPHANUM);
3257      }
3258  
3259      $key = validate_user_key($keyvalue, $script, $instance);
3260  
3261      if (!$user = $DB->get_record('user', array('id' => $key->userid))) {
3262          print_error('invaliduserid');
3263      }
3264  
3265      core_user::require_active_user($user, true, true);
3266  
3267      // Emulate normal session.
3268      enrol_check_plugins($user);
3269      \core\session\manager::set_user($user);
3270  
3271      // Note we are not using normal login.
3272      if (!defined('USER_KEY_LOGIN')) {
3273          define('USER_KEY_LOGIN', true);
3274      }
3275  
3276      // Return instance id - it might be empty.
3277      return $key->instance;
3278  }
3279  
3280  /**
3281   * Creates a new private user access key.
3282   *
3283   * @param string $script unique target identifier
3284   * @param int $userid
3285   * @param int $instance optional instance id
3286   * @param string $iprestriction optional ip restricted access
3287   * @param int $validuntil key valid only until given data
3288   * @return string access key value
3289   */
3290  function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3291      global $DB;
3292  
3293      $key = new stdClass();
3294      $key->script        = $script;
3295      $key->userid        = $userid;
3296      $key->instance      = $instance;
3297      $key->iprestriction = $iprestriction;
3298      $key->validuntil    = $validuntil;
3299      $key->timecreated   = time();
3300  
3301      // Something long and unique.
3302      $key->value         = md5($userid.'_'.time().random_string(40));
3303      while ($DB->record_exists('user_private_key', array('value' => $key->value))) {
3304          // Must be unique.
3305          $key->value     = md5($userid.'_'.time().random_string(40));
3306      }
3307      $DB->insert_record('user_private_key', $key);
3308      return $key->value;
3309  }
3310  
3311  /**
3312   * Delete the user's new private user access keys for a particular script.
3313   *
3314   * @param string $script unique target identifier
3315   * @param int $userid
3316   * @return void
3317   */
3318  function delete_user_key($script, $userid) {
3319      global $DB;
3320      $DB->delete_records('user_private_key', array('script' => $script, 'userid' => $userid));
3321  }
3322  
3323  /**
3324   * Gets a private user access key (and creates one if one doesn't exist).
3325   *
3326   * @param string $script unique target identifier
3327   * @param int $userid
3328   * @param int $instance optional instance id
3329   * @param string $iprestriction optional ip restricted access
3330   * @param int $validuntil key valid only until given date
3331   * @return string access key value
3332   */
3333  function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3334      global $DB;
3335  
3336      if ($key = $DB->get_record('user_private_key', array('script' => $script, 'userid' => $userid,
3337                                                           'instance' => $instance, 'iprestriction' => $iprestriction,
3338                                                           'validuntil' => $validuntil))) {
3339          return $key->value;
3340      } else {
3341          return create_user_key($script, $userid, $instance, $iprestriction, $validuntil);
3342      }
3343  }
3344  
3345  
3346  /**
3347   * Modify the user table by setting the currently logged in user's last login to now.
3348   *
3349   * @return bool Always returns true
3350   */
3351  function update_user_login_times() {
3352      global $USER, $DB;
3353  
3354      if (isguestuser()) {
3355          // Do not update guest access times/ips for performance.
3356          return true;
3357      }
3358  
3359      $now = time();
3360  
3361      $user = new stdClass();
3362      $user->id = $USER->id;
3363  
3364      // Make sure all users that logged in have some firstaccess.
3365      if ($USER->firstaccess == 0) {
3366          $USER->firstaccess = $user->firstaccess = $now;
3367      }
3368  
3369      // Store the previous current as lastlogin.
3370      $USER->lastlogin = $user->lastlogin = $USER->currentlogin;
3371  
3372      $USER->currentlogin = $user->currentlogin = $now;
3373  
3374      // Function user_accesstime_log() may not update immediately, better do it here.
3375      $USER->lastaccess = $user->lastaccess = $now;
3376      $USER->lastip = $user->lastip = getremoteaddr();
3377  
3378      // Note: do not call user_update_user() here because this is part of the login process,
3379      //       the login event means that these fields were updated.
3380      $DB->update_record('user', $user);
3381      return true;
3382  }
3383  
3384  /**
3385   * Determines if a user has completed setting up their account.
3386   *
3387   * The lax mode (with $strict = false) has been introduced for special cases
3388   * only where we want to skip certain checks intentionally. This is valid in
3389   * certain mnet or ajax scenarios when the user cannot / should not be
3390   * redirected to edit their profile. In most cases, you should perform the
3391   * strict check.
3392   *
3393   * @param stdClass $user A {@link $USER} object to test for the existence of a valid name and email
3394   * @param bool $strict Be more strict and assert id and custom profile fields set, too
3395   * @return bool
3396   */
3397  function user_not_fully_set_up($user, $strict = true) {
3398      global $CFG;
3399      require_once($CFG->dirroot.'/user/profile/lib.php');
3400  
3401      if (isguestuser($user)) {
3402          return false;
3403      }
3404  
3405      if (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user)) {
3406          return true;
3407      }
3408  
3409      if ($strict) {
3410          if (empty($user->id)) {
3411              // Strict mode can be used with existing accounts only.
3412              return true;
3413          }
3414          if (!profile_has_required_custom_fields_set($user->id)) {
3415              return true;
3416          }
3417      }
3418  
3419      return false;
3420  }
3421  
3422  /**
3423   * Check whether the user has exceeded the bounce threshold
3424   *
3425   * @param stdClass $user A {@link $USER} object
3426   * @return bool true => User has exceeded bounce threshold
3427   */
3428  function over_bounce_threshold($user) {
3429      global $CFG, $DB;
3430  
3431      if (empty($CFG->handlebounces)) {
3432          return false;
3433      }
3434  
3435      if (empty($user->id)) {
3436          // No real (DB) user, nothing to do here.
3437          return false;
3438      }
3439  
3440      // Set sensible defaults.
3441      if (empty($CFG->minbounces)) {
3442          $CFG->minbounces = 10;
3443      }
3444      if (empty($CFG->bounceratio)) {
3445          $CFG->bounceratio = .20;
3446      }
3447      $bouncecount = 0;
3448      $sendcount = 0;
3449      if ($bounce = $DB->get_record('user_preferences', array ('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3450          $bouncecount = $bounce->value;
3451      }
3452      if ($send = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3453          $sendcount = $send->value;
3454      }
3455      return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio);
3456  }
3457  
3458  /**
3459   * Used to increment or reset email sent count
3460   *
3461   * @param stdClass $user object containing an id
3462   * @param bool $reset will reset the count to 0
3463   * @return void
3464   */
3465  function set_send_count($user, $reset=false) {
3466      global $DB;
3467  
3468      if (empty($user->id)) {
3469          // No real (DB) user, nothing to do here.
3470          return;
3471      }
3472  
3473      if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3474          $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3475          $DB->update_record('user_preferences', $pref);
3476      } else if (!empty($reset)) {
3477          // If it's not there and we're resetting, don't bother. Make a new one.
3478          $pref = new stdClass();
3479          $pref->name   = 'email_send_count';
3480          $pref->value  = 1;
3481          $pref->userid = $user->id;
3482          $DB->insert_record('user_preferences', $pref, false);
3483      }
3484  }
3485  
3486  /**
3487   * Increment or reset user's email bounce count
3488   *
3489   * @param stdClass $user object containing an id
3490   * @param bool $reset will reset the count to 0
3491   */
3492  function set_bounce_count($user, $reset=false) {
3493      global $DB;
3494  
3495      if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3496          $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3497          $DB->update_record('user_preferences', $pref);
3498      } else if (!empty($reset)) {
3499          // If it's not there and we're resetting, don't bother. Make a new one.
3500          $pref = new stdClass();
3501          $pref->name   = 'email_bounce_count';
3502          $pref->value  = 1;
3503          $pref->userid = $user->id;
3504          $DB->insert_record('user_preferences', $pref, false);
3505      }
3506  }
3507  
3508  /**
3509   * Determines if the logged in user is currently moving an activity
3510   *
3511   * @param int $courseid The id of the course being tested
3512   * @return bool
3513   */
3514  function ismoving($courseid) {
3515      global $USER;
3516  
3517      if (!empty($USER->activitycopy)) {
3518          return ($USER->activitycopycourse == $courseid);
3519      }
3520      return false;
3521  }
3522  
3523  /**
3524   * Returns a persons full name
3525   *
3526   * Given an object containing all of the users name values, this function returns a string with the full name of the person.
3527   * The result may depend on system settings or language. 'override' will force the alternativefullnameformat to be used. In
3528   * English, fullname as well as alternativefullnameformat is set to 'firstname lastname' by default. But you could have
3529   * fullname set to 'firstname lastname' and alternativefullnameformat set to 'firstname middlename alternatename lastname'.
3530   *
3531   * @param stdClass $user A {@link $USER} object to get full name of.
3532   * @param bool $override If true then the alternativefullnameformat format rather than fullnamedisplay format will be used.
3533   * @return string
3534   */
3535  function fullname($user, $override=false) {
3536      global $CFG, $SESSION;
3537  
3538      if (!isset($user->firstname) and !isset($user->lastname)) {
3539          return '';
3540      }
3541  
3542      // Get all of the name fields.
3543      $allnames = get_all_user_name_fields();
3544      if ($CFG->debugdeveloper) {
3545          foreach ($allnames as $allname) {
3546              if (!property_exists($user, $allname)) {
3547                  // If all the user name fields are not set in the user object, then notify the programmer that it needs to be fixed.
3548                  debugging('You need to update your sql to include additional name fields in the user object.', DEBUG_DEVELOPER);
3549                  // Message has been sent, no point in sending the message multiple times.
3550                  break;
3551              }
3552          }
3553      }
3554  
3555      if (!$override) {
3556          if (!empty($CFG->forcefirstname)) {
3557              $user->firstname = $CFG->forcefirstname;
3558          }
3559          if (!empty($CFG->forcelastname)) {
3560              $user->lastname = $CFG->forcelastname;
3561          }
3562      }
3563  
3564      if (!empty($SESSION->fullnamedisplay)) {
3565          $CFG->fullnamedisplay = $SESSION->fullnamedisplay;
3566      }
3567  
3568      $template = null;
3569      // If the fullnamedisplay setting is available, set the template to that.
3570      if (isset($CFG->fullnamedisplay)) {
3571          $template = $CFG->fullnamedisplay;
3572      }
3573      // If the template is empty, or set to language, return the language string.
3574      if ((empty($template) || $template == 'language') && !$override) {
3575          return get_string('fullnamedisplay', null, $user);
3576      }
3577  
3578      // Check to see if we are displaying according to the alternative full name format.
3579      if ($override) {
3580          if (empty($CFG->alternativefullnameformat) || $CFG->alternativefullnameformat == 'language') {
3581              // Default to show just the user names according to the fullnamedisplay string.
3582              return get_string('fullnamedisplay', null, $user);
3583          } else {
3584              // If the override is true, then change the template to use the complete name.
3585              $template = $CFG->alternativefullnameformat;
3586          }
3587      }
3588  
3589      $requirednames = array();
3590      // With each name, see if it is in the display name template, and add it to the required names array if it is.
3591      foreach ($allnames as $allname) {
3592          if (strpos($template, $allname) !== false) {
3593              $requirednames[] = $allname;
3594          }
3595      }
3596  
3597      $displayname = $template;
3598      // Switch in the actual data into the template.
3599      foreach ($requirednames as $altname) {
3600          if (isset($user->$altname)) {
3601              // Using empty() on the below if statement causes breakages.
3602              if ((string)$user->$altname == '') {
3603                  $displayname = str_replace($altname, 'EMPTY', $displayname);
3604              } else {
3605                  $displayname = str_replace($altname, $user->$altname, $displayname);
3606              }
3607          } else {
3608              $displayname = str_replace($altname, 'EMPTY', $displayname);
3609          }
3610      }
3611      // Tidy up any misc. characters (Not perfect, but gets most characters).
3612      // Don't remove the "u" at the end of the first expression unless you want garbled characters when combining hiragana or
3613      // katakana and parenthesis.
3614      $patterns = array();
3615      // This regular expression replacement is to fix problems such as 'James () Kirk' Where 'Tiberius' (middlename) has not been
3616      // filled in by a user.
3617      // The special characters are Japanese brackets that are common enough to make allowances for them (not covered by :punct:).
3618      $patterns[] = '/[[:punct:]「」]*EMPTY[[:punct:]「」]*/u';
3619      // This regular expression is to remove any double spaces in the display name.
3620      $patterns[] = '/\s{2,}/u';
3621      foreach ($patterns as $pattern) {
3622          $displayname = preg_replace($pattern, ' ', $displayname);
3623      }
3624  
3625      // Trimming $displayname will help the next check to ensure that we don't have a display name with spaces.
3626      $displayname = trim($displayname);
3627      if (empty($displayname)) {
3628          // Going with just the first name if no alternate fields are filled out. May be changed later depending on what
3629          // people in general feel is a good setting to fall back on.
3630          $displayname = $user->firstname;
3631      }
3632      return $displayname;
3633  }
3634  
3635  /**
3636   * A centralised location for the all name fields. Returns an array / sql string snippet.
3637   *
3638   * @param bool $returnsql True for an sql select field snippet.
3639   * @param string $tableprefix table query prefix to use in front of each field.
3640   * @param string $prefix prefix added to the name fields e.g. authorfirstname.
3641   * @param string $fieldprefix sql field prefix e.g. id AS userid.
3642   * @param bool $order moves firstname and lastname to the top of the array / start of the string.
3643   * @return array|string All name fields.
3644   */
3645  function get_all_user_name_fields($returnsql = false, $tableprefix = null, $prefix = null, $fieldprefix = null, $order = false) {
3646      // This array is provided in this order because when called by fullname() (above) if firstname is before
3647      // firstnamephonetic str_replace() will change the wrong placeholder.
3648      $alternatenames = array('firstnamephonetic' => 'firstnamephonetic',
3649                              'lastnamephonetic' => 'lastnamephonetic',
3650                              'middlename' => 'middlename',
3651                              'alternatename' => 'alternatename',
3652                              'firstname' => 'firstname',
3653                              'lastname' => 'lastname');
3654  
3655      // Let's add a prefix to the array of user name fields if provided.
3656      if ($prefix) {
3657          foreach ($alternatenames as $key => $altname) {
3658              $alternatenames[$key] = $prefix . $altname;
3659          }
3660      }
3661  
3662      // If we want the end result to have firstname and lastname at the front / top of the result.
3663      if ($order) {
3664          // Move the last two elements (firstname, lastname) off the array and put them at the top.
3665          for ($i = 0; $i < 2; $i++) {
3666              // Get the last element.
3667              $lastelement = end($alternatenames);
3668              // Remove it from the array.
3669              unset($alternatenames[$lastelement]);
3670              // Put the element back on the top of the array.
3671              $alternatenames = array_merge(array($lastelement => $lastelement), $alternatenames);
3672          }
3673      }
3674  
3675      // Create an sql field snippet if requested.
3676      if ($returnsql) {
3677          if ($tableprefix) {
3678              if ($fieldprefix) {
3679                  foreach ($alternatenames as $key => $altname) {
3680                      $alternatenames[$key] = $tableprefix . '.' . $altname . ' AS ' . $fieldprefix . $altname;
3681                  }
3682              } else {
3683                  foreach ($alternatenames as $key => $altname) {
3684                      $alternatenames[$key] = $tableprefix . '.' . $altname;
3685                  }
3686              }
3687          }
3688          $alternatenames = implode(',', $alternatenames);
3689      }
3690      return $alternatenames;
3691  }
3692  
3693  /**
3694   * Reduces lines of duplicated code for getting user name fields.
3695   *
3696   * See also {@link user_picture::unalias()}
3697   *
3698   * @param object $addtoobject Object to add user name fields to.
3699   * @param object $secondobject Object that contains user name field information.
3700   * @param string $prefix prefix to be added to all fields (including $additionalfields) e.g. authorfirstname.
3701   * @param array $additionalfields Additional fields to be matched with data in the second object.
3702   * The key can be set to the user table field name.
3703   * @return object User name fields.
3704   */
3705  function username_load_fields_from_object($addtoobject, $secondobject, $prefix = null, $additionalfields = null) {
3706      $fields = get_all_user_name_fields(false, null, $prefix);
3707      if ($additionalfields) {
3708          // Additional fields can specify their own 'alias' such as 'id' => 'userid'. This checks to see if
3709          // the key is a number and then sets the key to the array value.
3710          foreach ($additionalfields as $key => $value) {
3711              if (is_numeric($key)) {
3712                  $additionalfields[$value] = $prefix . $value;
3713                  unset($additionalfields[$key]);
3714              } else {
3715                  $additionalfields[$key] = $prefix . $value;
3716              }
3717          }
3718          $fields = array_merge($fields, $additionalfields);
3719      }
3720      foreach ($fields as $key => $field) {
3721          // Important that we have all of the user name fields present in the object that we are sending back.
3722          $addtoobject->$key = '';
3723          if (isset($secondobject->$field)) {
3724              $addtoobject->$key = $secondobject->$field;
3725          }
3726      }
3727      return $addtoobject;
3728  }
3729  
3730  /**
3731   * Returns an array of values in order of occurance in a provided string.
3732   * The key in the result is the character postion in the string.
3733   *
3734   * @param array $values Values to be found in the string format
3735   * @param string $stringformat The string which may contain values being searched for.
3736   * @return array An array of values in order according to placement in the string format.
3737   */
3738  function order_in_string($values, $stringformat) {
3739      $valuearray = array();
3740      foreach ($values as $value) {
3741          $pattern = "/$value\b/";
3742          // Using preg_match as strpos() may match values that are similar e.g. firstname and firstnamephonetic.
3743          if (preg_match($pattern, $stringformat)) {
3744              $replacement = "thing";
3745              // Replace the value with something more unique to ensure we get the right position when using strpos().
3746              $newformat = preg_replace($pattern, $replacement, $stringformat);
3747              $position = strpos($newformat, $replacement);
3748              $valuearray[$position] = $value;
3749          }
3750      }
3751      ksort($valuearray);
3752      return $valuearray;
3753  }
3754  
3755  /**
3756   * Checks if current user is shown any extra fields when listing users.
3757   *
3758   * @param object $context Context
3759   * @param array $already Array of fields that we're going to show anyway
3760   *   so don't bother listing them
3761   * @return array Array of field names from user table, not including anything
3762   *   listed in $already
3763   */
3764  function get_extra_user_fields($context, $already = array()) {
3765      global $CFG;
3766  
3767      // Only users with permission get the extra fields.
3768      if (!has_capability('moodle/site:viewuseridentity', $context)) {
3769          return array();
3770      }
3771  
3772      // Split showuseridentity on comma (filter needed in case the showuseridentity is empty).
3773      $extra = array_filter(explode(',', $CFG->showuseridentity));
3774  
3775      foreach ($extra as $key => $field) {
3776          if (in_array($field, $already)) {
3777              unset($extra[$key]);
3778          }
3779      }
3780  
3781      // If the identity fields are also among hidden fields, make sure the user can see them.
3782      $hiddenfields = array_filter(explode(',', $CFG->hiddenuserfields));
3783      $hiddenidentifiers = array_intersect($extra, $hiddenfields);
3784  
3785      if ($hiddenidentifiers) {
3786          if ($context->get_course_context(false)) {
3787              // We are somewhere inside a course.
3788              $canviewhiddenuserfields = has_capability('moodle/course:viewhiddenuserfields', $context);
3789  
3790          } else {
3791              // We are not inside a course.
3792              $canviewhiddenuserfields = has_capability('moodle/user:viewhiddendetails', $context);
3793          }
3794  
3795          if (!$canviewhiddenuserfields) {
3796              // Remove hidden identifiers from the list.
3797              $extra = array_diff($extra, $hiddenidentifiers);
3798          }
3799      }
3800  
3801      // Re-index the entries.
3802      $extra = array_values($extra);
3803  
3804      return $extra;
3805  }
3806  
3807  /**
3808   * If the current user is to be shown extra user fields when listing or
3809   * selecting users, returns a string suitable for including in an SQL select
3810   * clause to retrieve those fields.
3811   *
3812   * @param context $context Context
3813   * @param string $alias Alias of user table, e.g. 'u' (default none)
3814   * @param string $prefix Prefix for field names using AS, e.g. 'u_' (default none)
3815   * @param array $already Array of fields that we're going to include anyway so don't list them (default none)
3816   * @return string Partial SQL select clause, beginning with comma, for example ',u.idnumber,u.department' unless it is blank
3817   */
3818  function get_extra_user_fields_sql($context, $alias='', $prefix='', $already = array()) {
3819      $fields = get_extra_user_fields($context, $already);
3820      $result = '';
3821      // Add punctuation for alias.
3822      if ($alias !== '') {
3823          $alias .= '.';
3824      }
3825      foreach ($fields as $field) {
3826          $result .= ', ' . $alias . $field;
3827          if ($prefix) {
3828              $result .= ' AS ' . $prefix . $field;
3829          }
3830      }
3831      return $result;
3832  }
3833  
3834  /**
3835   * Returns the display name of a field in the user table. Works for most fields that are commonly displayed to users.
3836   * @param string $field Field name, e.g. 'phone1'
3837   * @return string Text description taken from language file, e.g. 'Phone number'
3838   */
3839  function get_user_field_name($field) {
3840      // Some fields have language strings which are not the same as field name.
3841      switch ($field) {
3842          case 'url' : {
3843              return get_string('webpage');
3844          }
3845          case 'icq' : {
3846              return get_string('icqnumber');
3847          }
3848          case 'skype' : {
3849              return get_string('skypeid');
3850          }
3851          case 'aim' : {
3852              return get_string('aimid');
3853          }
3854          case 'yahoo' : {
3855              return get_string('yahooid');
3856          }
3857          case 'msn' : {
3858              return get_string('msnid');
3859          }
3860          case 'picture' : {
3861              return get_string('pictureofuser');
3862          }
3863      }
3864      // Otherwise just use the same lang string.
3865      return get_string($field);
3866  }
3867  
3868  /**
3869   * Returns whether a given authentication plugin exists.
3870   *
3871   * @param string $auth Form of authentication to check for. Defaults to the global setting in {@link $CFG}.
3872   * @return boolean Whether the plugin is available.
3873   */
3874  function exists_auth_plugin($auth) {
3875      global $CFG;
3876  
3877      if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) {
3878          return is_readable("{$CFG->dirroot}/auth/$auth/auth.php");
3879      }
3880      return false;
3881  }
3882  
3883  /**
3884   * Checks if a given plugin is in the list of enabled authentication plugins.
3885   *
3886   * @param string $auth Authentication plugin.
3887   * @return boolean Whether the plugin is enabled.
3888   */
3889  function is_enabled_auth($auth) {
3890      if (empty($auth)) {
3891          return false;
3892      }
3893  
3894      $enabled = get_enabled_auth_plugins();
3895  
3896      return in_array($auth, $enabled);
3897  }
3898  
3899  /**
3900   * Returns an authentication plugin instance.
3901   *
3902   * @param string $auth name of authentication plugin
3903   * @return auth_plugin_base An instance of the required authentication plugin.
3904   */
3905  function get_auth_plugin($auth) {
3906      global $CFG;
3907  
3908      // Check the plugin exists first.
3909      if (! exists_auth_plugin($auth)) {
3910          print_error('authpluginnotfound', 'debug', '', $auth);
3911      }
3912  
3913      // Return auth plugin instance.
3914      require_once("{$CFG->dirroot}/auth/$auth/auth.php");
3915      $class = "auth_plugin_$auth";
3916      return new $class;
3917  }
3918  
3919  /**
3920   * Returns array of active auth plugins.
3921   *
3922   * @param bool $fix fix $CFG->auth if needed. Only set if logged in as admin.
3923   * @return array
3924   */
3925  function get_enabled_auth_plugins($fix=false) {
3926      global $CFG;
3927  
3928      $default = array('manual', 'nologin');
3929  
3930      if (empty($CFG->auth)) {
3931          $auths = array();
3932      } else {
3933          $auths = explode(',', $CFG->auth);
3934      }
3935  
3936      $auths = array_unique($auths);
3937      $oldauthconfig = implode(',', $auths);
3938      foreach ($auths as $k => $authname) {
3939          if (in_array($authname, $default)) {
3940              // The manual and nologin plugin never need to be stored.
3941              unset($auths[$k]);
3942          } else if (!exists_auth_plugin($authname)) {
3943              debugging(get_string('authpluginnotfound', 'debug', $authname));
3944              unset($auths[$k]);
3945          }
3946      }
3947  
3948      // Ideally only explicit interaction from a human admin should trigger a
3949      // change in auth config, see MDL-70424 for details.
3950      if ($fix) {
3951          $newconfig = implode(',', $auths);
3952          if (!isset($CFG->auth) or $newconfig != $CFG->auth) {
3953              set_config('auth', $newconfig);
3954          }
3955      }
3956  
3957      return (array_merge($default, $auths));
3958  }
3959  
3960  /**
3961   * Returns true if an internal authentication method is being used.
3962   * if method not specified then, global default is assumed
3963   *
3964   * @param string $auth Form of authentication required
3965   * @return bool
3966   */
3967  function is_internal_auth($auth) {
3968      // Throws error if bad $auth.
3969      $authplugin = get_auth_plugin($auth);
3970      return $authplugin->is_internal();
3971  }
3972  
3973  /**
3974   * Returns true if the user is a 'restored' one.
3975   *
3976   * Used in the login process to inform the user and allow him/her to reset the password
3977   *
3978   * @param string $username username to be checked
3979   * @return bool
3980   */
3981  function is_restored_user($username) {
3982      global $CFG, $DB;
3983  
3984      return $DB->record_exists('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'password' => 'restored'));
3985  }
3986  
3987  /**
3988   * Returns an array of user fields
3989   *
3990   * @return array User field/column names
3991   */
3992  function get_user_fieldnames() {
3993      global $DB;
3994  
3995      $fieldarray = $DB->get_columns('user');
3996      unset($fieldarray['id']);
3997      $fieldarray = array_keys($fieldarray);
3998  
3999      return $fieldarray;
4000  }
4001  
4002  /**
4003   * Returns the string of the language for the new user.
4004   *
4005   * @return string language for the new user
4006   */
4007  function get_newuser_language() {
4008      global $CFG, $SESSION;
4009      return (!empty($CFG->autolangusercreation) && !empty($SESSION->lang)) ? $SESSION->lang : $CFG->lang;
4010  }
4011  
4012  /**
4013   * Creates a bare-bones user record
4014   *
4015   * @todo Outline auth types and provide code example
4016   *
4017   * @param string $username New user's username to add to record
4018   * @param string $password New user's password to add to record
4019   * @param string $auth Form of authentication required
4020   * @return stdClass A complete user object
4021   */
4022  function create_user_record($username, $password, $auth = 'manual') {
4023      global $CFG, $DB, $SESSION;
4024      require_once($CFG->dirroot.'/user/profile/lib.php');
4025      require_once($CFG->dirroot.'/user/lib.php');
4026  
4027      // Just in case check text case.
4028      $username = trim(core_text::strtolower($username));
4029  
4030      $authplugin = get_auth_plugin($auth);
4031      $customfields = $authplugin->get_custom_user_profile_fields();
4032      $newuser = new stdClass();
4033      if ($newinfo = $authplugin->get_userinfo($username)) {
4034          $newinfo = truncate_userinfo($newinfo);
4035          foreach ($newinfo as $key => $value) {
4036              if (in_array($key, $authplugin->userfields) || (in_array($key, $customfields))) {
4037                  $newuser->$key = $value;
4038              }
4039          }
4040      }
4041  
4042      if (!empty($newuser->email)) {
4043          if (email_is_not_allowed($newuser->email)) {
4044              unset($newuser->email);
4045          }
4046      }
4047  
4048      if (!isset($newuser->city)) {
4049          $newuser->city = '';
4050      }
4051  
4052      $newuser->auth = $auth;
4053      $newuser->username = $username;
4054  
4055      // Fix for MDL-8480
4056      // user CFG lang for user if $newuser->lang is empty
4057      // or $user->lang is not an installed language.
4058      if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) {
4059          $newuser->lang = get_newuser_language();
4060      }
4061      $newuser->confirmed = 1;
4062      $newuser->lastip = getremoteaddr();
4063      $newuser->timecreated = time();
4064      $newuser->timemodified = $newuser->timecreated;
4065      $newuser->mnethostid = $CFG->mnet_localhost_id;
4066  
4067      $newuser->id = user_create_user($newuser, false, false);
4068  
4069      // Save user profile data.
4070      profile_save_data($newuser);
4071  
4072      $user = get_complete_user_data('id', $newuser->id);
4073      if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})) {
4074          set_user_preference('auth_forcepasswordchange', 1, $user);
4075      }
4076      // Set the password.
4077      update_internal_user_password($user, $password);
4078  
4079      // Trigger event.
4080      \core\event\user_created::create_from_userid($newuser->id)->trigger();
4081  
4082      return $user;
4083  }
4084  
4085  /**
4086   * Will update a local user record from an external source (MNET users can not be updated using this method!).
4087   *
4088   * @param string $username user's username to update the record
4089   * @return stdClass A complete user object
4090   */
4091  function update_user_record($username) {
4092      global $DB, $CFG;
4093      // Just in case check text case.
4094      $username = trim(core_text::strtolower($username));
4095  
4096      $oldinfo = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id), '*', MUST_EXIST);
4097      return update_user_record_by_id($oldinfo->id);
4098  }
4099  
4100  /**
4101   * Will update a local user record from an external source (MNET users can not be updated using this method!).
4102   *
4103   * @param int $id user id
4104   * @return stdClass A complete user object
4105   */
4106  function update_user_record_by_id($id) {
4107      global $DB, $CFG;
4108      require_once($CFG->dirroot."/user/profile/lib.php");
4109      require_once($CFG->dirroot.'/user/lib.php');
4110  
4111      $params = array('mnethostid' => $CFG->mnet_localhost_id, 'id' => $id, 'deleted' => 0);
4112      $oldinfo = $DB->get_record('user', $params, '*', MUST_EXIST);
4113  
4114      $newuser = array();
4115      $userauth = get_auth_plugin($oldinfo->auth);
4116  
4117      if ($newinfo = $userauth->get_userinfo($oldinfo->username)) {
4118          $newinfo = truncate_userinfo($newinfo);
4119          $customfields = $userauth->get_custom_user_profile_fields();
4120  
4121          foreach ($newinfo as $key => $value) {
4122              $iscustom = in_array($key, $customfields);
4123              if (!$iscustom) {
4124                  $key = strtolower($key);
4125              }
4126              if ((!property_exists($oldinfo, $key) && !$iscustom) or $key === 'username' or $key === 'id'
4127                      or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') {
4128                  // Unknown or must not be changed.
4129                  continue;
4130              }
4131              if (empty($userauth->config->{'field_updatelocal_' . $key}) || empty($userauth->config->{'field_lock_' . $key})) {
4132                  continue;
4133              }
4134              $confval = $userauth->config->{'field_updatelocal_' . $key};
4135              $lockval = $userauth->config->{'field_lock_' . $key};
4136              if ($confval === 'onlogin') {
4137                  // MDL-4207 Don't overwrite modified user profile values with
4138                  // empty LDAP values when 'unlocked if empty' is set. The purpose
4139                  // of the setting 'unlocked if empty' is to allow the user to fill
4140                  // in a value for the selected field _if LDAP is giving
4141                  // nothing_ for this field. Thus it makes sense to let this value
4142                  // stand in until LDAP is giving a value for this field.
4143                  if (!(empty($value) && $lockval === 'unlockedifempty')) {
4144                      if ($iscustom || (in_array($key, $userauth->userfields) &&
4145                              ((string)$oldinfo->$key !== (string)$value))) {
4146                          $newuser[$key] = (string)$value;
4147                      }
4148                  }
4149              }
4150          }
4151          if ($newuser) {
4152              $newuser['id'] = $oldinfo->id;
4153              $newuser['timemodified'] = time();
4154              user_update_user((object) $newuser, false, false);
4155  
4156              // Save user profile data.
4157              profile_save_data((object) $newuser);
4158  
4159              // Trigger event.
4160              \core\event\user_updated::create_from_userid($newuser['id'])->trigger();
4161          }
4162      }
4163  
4164      return get_complete_user_data('id', $oldinfo->id);
4165  }
4166  
4167  /**
4168   * Will truncate userinfo as it comes from auth_get_userinfo (from external auth) which may have large fields.
4169   *
4170   * @param array $info Array of user properties to truncate if needed
4171   * @return array The now truncated information that was passed in
4172   */
4173  function truncate_userinfo(array $info) {
4174      // Define the limits.
4175      $limit = array(
4176          'username'    => 100,
4177          'idnumber'    => 255,
4178          'firstname'   => 100,
4179          'lastname'    => 100,
4180          'email'       => 100,
4181          'icq'         =>  15,
4182          'phone1'      =>  20,
4183          'phone2'      =>  20,
4184          'institution' => 255,
4185          'department'  => 255,
4186          'address'     => 255,
4187          'city'        => 120,
4188          'country'     =>   2,
4189          'url'         => 255,
4190      );
4191  
4192      // Apply where needed.
4193      foreach (array_keys($info) as $key) {
4194          if (!empty($limit[$key])) {
4195              $info[$key] = trim(core_text::substr($info[$key], 0, $limit[$key]));
4196          }
4197      }
4198  
4199      return $info;
4200  }
4201  
4202  /**
4203   * Marks user deleted in internal user database and notifies the auth plugin.
4204   * Also unenrols user from all roles and does other cleanup.
4205   *
4206   * Any plugin that needs to purge user data should register the 'user_deleted' event.
4207   *
4208   * @param stdClass $user full user object before delete
4209   * @return boolean success
4210   * @throws coding_exception if invalid $user parameter detected
4211   */
4212  function delete_user(stdClass $user) {
4213      global $CFG, $DB, $SESSION;
4214      require_once($CFG->libdir.'/grouplib.php');
4215      require_once($CFG->libdir.'/gradelib.php');
4216      require_once($CFG->dirroot.'/message/lib.php');
4217      require_once($CFG->dirroot.'/user/lib.php');
4218  
4219      // Make sure nobody sends bogus record type as parameter.
4220      if (!property_exists($user, 'id') or !property_exists($user, 'username')) {
4221          throw new coding_exception('Invalid $user parameter in delete_user() detected');
4222      }
4223  
4224      // Better not trust the parameter and fetch the latest info this will be very expensive anyway.
4225      if (!$user = $DB->get_record('user', array('id' => $user->id))) {
4226          debugging('Attempt to delete unknown user account.');
4227          return false;
4228      }
4229  
4230      // There must be always exactly one guest record, originally the guest account was identified by username only,
4231      // now we use $CFG->siteguest for performance reasons.
4232      if ($user->username === 'guest' or isguestuser($user)) {
4233          debugging('Guest user account can not be deleted.');
4234          return false;
4235      }
4236  
4237      // Admin can be theoretically from different auth plugin, but we want to prevent deletion of internal accoutns only,
4238      // if anything goes wrong ppl may force somebody to be admin via config.php setting $CFG->siteadmins.
4239      if ($user->auth === 'manual' and is_siteadmin($user)) {
4240          debugging('Local administrator accounts can not be deleted.');
4241          return false;
4242      }
4243  
4244      // Allow plugins to use this user object before we completely delete it.
4245      if ($pluginsfunction = get_plugins_with_function('pre_user_delete')) {
4246          foreach ($pluginsfunction as $plugintype => $plugins) {
4247              foreach ($plugins as $pluginfunction) {
4248                  $pluginfunction($user);
4249              }
4250          }
4251      }
4252  
4253      // Keep user record before updating it, as we have to pass this to user_deleted event.
4254      $olduser = clone $user;
4255  
4256      // Keep a copy of user context, we need it for event.
4257      $usercontext = context_user::instance($user->id);
4258  
4259      // Delete all grades - backup is kept in grade_grades_history table.
4260      grade_user_delete($user->id);
4261  
4262      // TODO: remove from cohorts using standard API here.
4263  
4264      // Remove user tags.
4265      core_tag_tag::remove_all_item_tags('core', 'user', $user->id);
4266  
4267      // Unconditionally unenrol from all courses.
4268      enrol_user_delete($user);
4269  
4270      // Unenrol from all roles in all contexts.
4271      // This might be slow but it is really needed - modules might do some extra cleanup!
4272      role_unassign_all(array('userid' => $user->id));
4273  
4274      // Notify the competency subsystem.
4275      \core_competency\api::hook_user_deleted($user->id);
4276  
4277      // Now do a brute force cleanup.
4278  
4279      // Delete all user events and subscription events.
4280      $DB->delete_records_select('event', 'userid = :userid AND subscriptionid IS NOT NULL', ['userid' => $user->id]);
4281  
4282      // Now, delete all calendar subscription from the user.
4283      $DB->delete_records('event_subscriptions', ['userid' => $user->id]);
4284  
4285      // Remove from all cohorts.
4286      $DB->delete_records('cohort_members', array('userid' => $user->id));
4287  
4288      // Remove from all groups.
4289      $DB->delete_records('groups_members', array('userid' => $user->id));
4290  
4291      // Brute force unenrol from all courses.
4292      $DB->delete_records('user_enrolments', array('userid' => $user->id));
4293  
4294      // Purge user preferences.
4295      $DB->delete_records('user_preferences', array('userid' => $user->id));
4296  
4297      // Purge user extra profile info.
4298      $DB->delete_records('user_info_data', array('userid' => $user->id));
4299  
4300      // Purge log of previous password hashes.
4301      $DB->delete_records('user_password_history', array('userid' => $user->id));
4302  
4303      // Last course access not necessary either.
4304      $DB->delete_records('user_lastaccess', array('userid' => $user->id));
4305      // Remove all user tokens.
4306      $DB->delete_records('external_tokens', array('userid' => $user->id));
4307  
4308      // Unauthorise the user for all services.
4309      $DB->delete_records('external_services_users', array('userid' => $user->id));
4310  
4311      // Remove users private keys.
4312      $DB->delete_records('user_private_key', array('userid' => $user->id));
4313  
4314      // Remove users customised pages.
4315      $DB->delete_records('my_pages', array('userid' => $user->id, 'private' => 1));
4316  
4317      // Remove user's oauth2 refresh tokens, if present.
4318      $DB->delete_records('oauth2_refresh_token', array('userid' => $user->id));
4319  
4320      // Delete user from $SESSION->bulk_users.
4321      if (isset($SESSION->bulk_users[$user->id])) {
4322          unset($SESSION->bulk_users[$user->id]);
4323      }
4324  
4325      // Force logout - may fail if file based sessions used, sorry.
4326      \core\session\manager::kill_user_sessions($user->id);
4327  
4328      // Generate username from email address, or a fake email.
4329      $delemail = !empty($user->email) ? $user->email : $user->username . '.' . $user->id . '@unknownemail.invalid';
4330  
4331      $deltime = time();
4332      $deltimelength = core_text::strlen((string) $deltime);
4333  
4334      // Max username length is 100 chars. Select up to limit - (length of current time + 1 [period character]) from users email.
4335      $delname = clean_param($delemail, PARAM_USERNAME);
4336      $delname = core_text::substr($delname, 0, 100 - ($deltimelength + 1)) . ".{$deltime}";
4337  
4338      // Workaround for bulk deletes of users with the same email address.
4339      while ($DB->record_exists('user', array('username' => $delname))) { // No need to use mnethostid here.
4340          $delname++;
4341      }
4342  
4343      // Mark internal user record as "deleted".
4344      $updateuser = new stdClass();
4345      $updateuser->id           = $user->id;
4346      $updateuser->deleted      = 1;
4347      $updateuser->username     = $delname;            // Remember it just in case.
4348      $updateuser->email        = md5($user->username);// Store hash of username, useful importing/restoring users.
4349      $updateuser->idnumber     = '';                  // Clear this field to free it up.
4350      $updateuser->picture      = 0;
4351      $updateuser->timemodified = $deltime;
4352  
4353      // Don't trigger update event, as user is being deleted.
4354      user_update_user($updateuser, false, false);
4355  
4356      // Delete all content associated with the user context, but not the context itself.
4357      $usercontext->delete_content();
4358  
4359      // Delete any search data.
4360      \core_search\manager::context_deleted($usercontext);
4361  
4362      // Any plugin that needs to cleanup should register this event.
4363      // Trigger event.
4364      $event = \core\event\user_deleted::create(
4365              array(
4366                  'objectid' => $user->id,
4367                  'relateduserid' => $user->id,
4368                  'context' => $usercontext,
4369                  'other' => array(
4370                      'username' => $user->username,
4371                      'email' => $user->email,
4372                      'idnumber' => $user->idnumber,
4373                      'picture' => $user->picture,
4374                      'mnethostid' => $user->mnethostid
4375                      )
4376                  )
4377              );
4378      $event->add_record_snapshot('user', $olduser);
4379      $event->trigger();
4380  
4381      // We will update the user's timemodified, as it will be passed to the user_deleted event, which
4382      // should know about this updated property persisted to the user's table.
4383      $user->timemodified = $updateuser->timemodified;
4384  
4385      // Notify auth plugin - do not block the delete even when plugin fails.
4386      $authplugin = get_auth_plugin($user->auth);
4387      $authplugin->user_delete($user);
4388  
4389      return true;
4390  }
4391  
4392  /**
4393   * Retrieve the guest user object.
4394   *
4395   * @return stdClass A {@link $USER} object
4396   */
4397  function guest_user() {
4398      global $CFG, $DB;
4399  
4400      if ($newuser = $DB->get_record('user', array('id' => $CFG->siteguest))) {
4401          $newuser->confirmed = 1;
4402          $newuser->lang = get_newuser_language();
4403          $newuser->lastip = getremoteaddr();
4404      }
4405  
4406      return $newuser;
4407  }
4408  
4409  /**
4410   * Authenticates a user against the chosen authentication mechanism
4411   *
4412   * Given a username and password, this function looks them
4413   * up using the currently selected authentication mechanism,
4414   * and if the authentication is successful, it returns a
4415   * valid $user object from the 'user' table.
4416   *
4417   * Uses auth_ functions from the currently active auth module
4418   *
4419   * After authenticate_user_login() returns success, you will need to
4420   * log that the user has logged in, and call complete_user_login() to set
4421   * the session up.
4422   *
4423   * Note: this function works only with non-mnet accounts!
4424   *
4425   * @param string $username  User's username (or also email if $CFG->authloginviaemail enabled)
4426   * @param string $password  User's password
4427   * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO
4428   * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists)
4429   * @param mixed logintoken If this is set to a string it is validated against the login token for the session.
4430   * @return stdClass|false A {@link $USER} object or false if error
4431   */
4432  function authenticate_user_login($username, $password, $ignorelockout=false, &$failurereason=null, $logintoken=false) {
4433      global $CFG, $DB, $PAGE;
4434      require_once("$CFG->libdir/authlib.php");
4435  
4436      if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) {
4437          // we have found the user
4438  
4439      } else if (!empty($CFG->authloginviaemail)) {
4440          if ($email = clean_param($username, PARAM_EMAIL)) {
4441              $select = "mnethostid = :mnethostid AND LOWER(email) = LOWER(:email) AND deleted = 0";
4442              $params = array('mnethostid' => $CFG->mnet_localhost_id, 'email' => $email);
4443              $users = $DB->get_records_select('user', $select, $params, 'id', 'id', 0, 2);
4444              if (count($users) === 1) {
4445                  // Use email for login only if unique.
4446                  $user = reset($users);
4447                  $user = get_complete_user_data('id', $user->id);
4448                  $username = $user->username;
4449              }
4450              unset($users);
4451          }
4452      }
4453  
4454      // Make sure this request came from the login form.
4455      if (!\core\session\manager::validate_login_token($logintoken)) {
4456          $failurereason = AUTH_LOGIN_FAILED;
4457  
4458          // Trigger login failed event (specifying the ID of the found user, if available).
4459          \core\event\user_login_failed::create([
4460              'userid' => ($user->id ?? 0),
4461              'other' => [
4462                  'username' => $username,
4463                  'reason' => $failurereason,
4464              ],
4465          ])->trigger();
4466  
4467          error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Invalid Login Token:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4468          return false;
4469      }
4470  
4471      $authsenabled = get_enabled_auth_plugins();
4472  
4473      if ($user) {
4474          // Use manual if auth not set.
4475          $auth = empty($user->auth) ? 'manual' : $user->auth;
4476  
4477          if (in_array($user->auth, $authsenabled)) {
4478              $authplugin = get_auth_plugin($user->auth);
4479              $authplugin->pre_user_login_hook($user);
4480          }
4481  
4482          if (!empty($user->suspended)) {
4483              $failurereason = AUTH_LOGIN_SUSPENDED;
4484  
4485              // Trigger login failed event.
4486              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4487                      'other' => array('username' => $username, 'reason' => $failurereason)));
4488              $event->trigger();
4489              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4490              return false;
4491          }
4492          if ($auth=='nologin' or !is_enabled_auth($auth)) {
4493              // Legacy way to suspend user.
4494              $failurereason = AUTH_LOGIN_SUSPENDED;
4495  
4496              // Trigger login failed event.
4497              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4498                      'other' => array('username' => $username, 'reason' => $failurereason)));
4499              $event->trigger();
4500              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Disabled Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4501              return false;
4502          }
4503          $auths = array($auth);
4504  
4505      } else {
4506          // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user().
4507          if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id,  'deleted' => 1))) {
4508              $failurereason = AUTH_LOGIN_NOUSER;
4509  
4510              // Trigger login failed event.
4511              $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4512                      'reason' => $failurereason)));
4513              $event->trigger();
4514              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Deleted Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4515              return false;
4516          }
4517  
4518          // User does not exist.
4519          $auths = $authsenabled;
4520          $user = new stdClass();
4521          $user->id = 0;
4522      }
4523  
4524      if ($ignorelockout) {
4525          // Some other mechanism protects against brute force password guessing, for example login form might include reCAPTCHA
4526          // or this function is called from a SSO script.
4527      } else if ($user->id) {
4528          // Verify login lockout after other ways that may prevent user login.
4529          if (login_is_lockedout($user)) {
4530              $failurereason = AUTH_LOGIN_LOCKOUT;
4531  
4532              // Trigger login failed event.
4533              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4534                      'other' => array('username' => $username, 'reason' => $failurereason)));
4535              $event->trigger();
4536  
4537              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Login lockout:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4538              return false;
4539          }
4540      } else {
4541          // We can not lockout non-existing accounts.
4542      }
4543  
4544      foreach ($auths as $auth) {
4545          $authplugin = get_auth_plugin($auth);
4546  
4547          // On auth fail fall through to the next plugin.
4548          if (!$authplugin->user_login($username, $password)) {
4549              continue;
4550          }
4551  
4552          // Before performing login actions, check if user still passes password policy, if admin setting is enabled.
4553          if (!empty($CFG->passwordpolicycheckonlogin)) {
4554              $errmsg = '';
4555              $passed = check_password_policy($password, $errmsg, $user);
4556              if (!$passed) {
4557                  // First trigger event for failure.
4558                  $failedevent = \core\event\user_password_policy_failed::create_from_user($user);
4559                  $failedevent->trigger();
4560  
4561                  // If able to change password, set flag and move on.
4562                  if ($authplugin->can_change_password()) {
4563                      // Check if we are on internal change password page, or service is external, don't show notification.
4564                      $internalchangeurl = new moodle_url('/login/change_password.php');
4565                      if (!($PAGE->has_set_url() && $internalchangeurl->compare($PAGE->url)) && $authplugin->is_internal()) {
4566                          \core\notification::error(get_string('passwordpolicynomatch', '', $errmsg));
4567                      }
4568                      set_user_preference('auth_forcepasswordchange', 1, $user);
4569                  } else if ($authplugin->can_reset_password()) {
4570                      // Else force a reset if possible.
4571                      \core\notification::error(get_string('forcepasswordresetnotice', '', $errmsg));
4572                      redirect(new moodle_url('/login/forgot_password.php'));
4573                  } else {
4574                      $notifymsg = get_string('forcepasswordresetfailurenotice', '', $errmsg);
4575                      // If support page is set, add link for help.
4576                      if (!empty($CFG->supportpage)) {
4577                          $link = \html_writer::link($CFG->supportpage, $CFG->supportpage);
4578                          $link = \html_writer::tag('p', $link);
4579                          $notifymsg .= $link;
4580                      }
4581  
4582                      // If no change or reset is possible, add a notification for user.
4583                      \core\notification::error($notifymsg);
4584                  }
4585              }
4586          }
4587  
4588          // Successful authentication.
4589          if ($user->id) {
4590              // User already exists in database.
4591              if (empty($user->auth)) {
4592                  // For some reason auth isn't set yet.
4593                  $DB->set_field('user', 'auth', $auth, array('id' => $user->id));
4594                  $user->auth = $auth;
4595              }
4596  
4597              // If the existing hash is using an out-of-date algorithm (or the legacy md5 algorithm), then we should update to
4598              // the current hash algorithm while we have access to the user's password.
4599              update_internal_user_password($user, $password);
4600  
4601              if ($authplugin->is_synchronised_with_external()) {
4602                  // Update user record from external DB.
4603                  $user = update_user_record_by_id($user->id);
4604              }
4605          } else {
4606              // The user is authenticated but user creation may be disabled.
4607              if (!empty($CFG->authpreventaccountcreation)) {
4608                  $failurereason = AUTH_LOGIN_UNAUTHORISED;
4609  
4610                  // Trigger login failed event.
4611                  $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4612                          'reason' => $failurereason)));
4613                  $event->trigger();
4614  
4615                  error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Unknown user, can not create new accounts:  $username  ".
4616                          $_SERVER['HTTP_USER_AGENT']);
4617                  return false;
4618              } else {
4619                  $user = create_user_record($username, $password, $auth);
4620              }
4621          }
4622  
4623          $authplugin->sync_roles($user);
4624  
4625          foreach ($authsenabled as $hau) {
4626              $hauth = get_auth_plugin($hau);
4627              $hauth->user_authenticated_hook($user, $username, $password);
4628          }
4629  
4630          if (empty($user->id)) {
4631              $failurereason = AUTH_LOGIN_NOUSER;
4632              // Trigger login failed event.
4633              $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4634                      'reason' => $failurereason)));
4635              $event->trigger();
4636              return false;
4637          }
4638  
4639          if (!empty($user->suspended)) {
4640              // Just in case some auth plugin suspended account.
4641              $failurereason = AUTH_LOGIN_SUSPENDED;
4642              // Trigger login failed event.
4643              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4644                      'other' => array('username' => $username, 'reason' => $failurereason)));
4645              $event->trigger();
4646              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4647              return false;
4648          }
4649  
4650          login_attempt_valid($user);
4651          $failurereason = AUTH_LOGIN_OK;
4652          return $user;
4653      }
4654  
4655      // Failed if all the plugins have failed.
4656      if (debugging('', DEBUG_ALL)) {
4657          error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Failed Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4658      }
4659  
4660      if ($user->id) {
4661          login_attempt_failed($user);
4662          $failurereason = AUTH_LOGIN_FAILED;
4663          // Trigger login failed event.
4664          $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4665                  'other' => array('username' => $username, 'reason' => $failurereason)));
4666          $event->trigger();
4667      } else {
4668          $failurereason = AUTH_LOGIN_NOUSER;
4669          // Trigger login failed event.
4670          $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4671                  'reason' => $failurereason)));
4672          $event->trigger();
4673      }
4674  
4675      return false;
4676  }
4677  
4678  /**
4679   * Call to complete the user login process after authenticate_user_login()
4680   * has succeeded. It will setup the $USER variable and other required bits
4681   * and pieces.
4682   *
4683   * NOTE:
4684   * - It will NOT log anything -- up to the caller to decide what to log.
4685   * - this function does not set any cookies any more!
4686   *
4687   * @param stdClass $user
4688   * @return stdClass A {@link $USER} object - BC only, do not use
4689   */
4690  function complete_user_login($user) {
4691      global $CFG, $DB, $USER, $SESSION;
4692  
4693      \core\session\manager::login_user($user);
4694  
4695      // Reload preferences from DB.
4696      unset($USER->preference);
4697      check_user_preferences_loaded($USER);
4698  
4699      // Update login times.
4700      update_user_login_times();
4701  
4702      // Extra session prefs init.
4703      set_login_session_preferences();
4704  
4705      // Trigger login event.
4706      $event = \core\event\user_loggedin::create(
4707          array(
4708              'userid' => $USER->id,
4709              'objectid' => $USER->id,
4710              'other' => array('username' => $USER->username),
4711          )
4712      );
4713      $event->trigger();
4714  
4715      // Queue migrating the messaging data, if we need to.
4716      if (!get_user_preferences('core_message_migrate_data', false, $USER->id)) {
4717          // Check if there are any legacy messages to migrate.
4718          if (\core_message\helper::legacy_messages_exist($USER->id)) {
4719              \core_message\task\migrate_message_data::queue_task($USER->id);
4720          } else {
4721              set_user_preference('core_message_migrate_data', true, $USER->id);
4722          }
4723      }
4724  
4725      if (isguestuser()) {
4726          // No need to continue when user is THE guest.
4727          return $USER;
4728      }
4729  
4730      if (CLI_SCRIPT) {
4731          // We can redirect to password change URL only in browser.
4732          return $USER;
4733      }
4734  
4735      // Select password change url.
4736      $userauth = get_auth_plugin($USER->auth);
4737  
4738      // Check whether the user should be changing password.
4739      if (get_user_preferences('auth_forcepasswordchange', false)) {
4740          if ($userauth->can_change_password()) {
4741              if ($changeurl = $userauth->change_password_url()) {
4742                  redirect($changeurl);
4743              } else {
4744                  require_once($CFG->dirroot . '/login/lib.php');
4745                  $SESSION->wantsurl = core_login_get_return_url();
4746                  redirect($CFG->wwwroot.'/login/change_password.php');
4747              }
4748          } else {
4749              print_error('nopasswordchangeforced', 'auth');
4750          }
4751      }
4752      return $USER;
4753  }
4754  
4755  /**
4756   * Check a password hash to see if it was hashed using the legacy hash algorithm (md5).
4757   *
4758   * @param string $password String to check.
4759   * @return boolean True if the $password matches the format of an md5 sum.
4760   */
4761  function password_is_legacy_hash($password) {
4762      return (bool) preg_match('/^[0-9a-f]{32}$/', $password);
4763  }
4764  
4765  /**
4766   * Compare password against hash stored in user object to determine if it is valid.
4767   *
4768   * If necessary it also updates the stored hash to the current format.
4769   *
4770   * @param stdClass $user (Password property may be updated).
4771   * @param string $password Plain text password.
4772   * @return bool True if password is valid.
4773   */
4774  function validate_internal_user_password($user, $password) {
4775      global $CFG;
4776  
4777      if ($user->password === AUTH_PASSWORD_NOT_CACHED) {
4778          // Internal password is not used at all, it can not validate.
4779          return false;
4780      }
4781  
4782      // If hash isn't a legacy (md5) hash, validate using the library function.
4783      if (!password_is_legacy_hash($user->password)) {
4784          return password_verify($password, $user->password);
4785      }
4786  
4787      // Otherwise we need to check for a legacy (md5) hash instead. If the hash
4788      // is valid we can then update it to the new algorithm.
4789  
4790      $sitesalt = isset($CFG->passwordsaltmain) ? $CFG->passwordsaltmain : '';
4791      $validated = false;
4792  
4793      if ($user->password === md5($password.$sitesalt)
4794              or $user->password === md5($password)
4795              or $user->password === md5(addslashes($password).$sitesalt)
4796              or $user->password === md5(addslashes($password))) {
4797          // Note: we are intentionally using the addslashes() here because we
4798          //       need to accept old password hashes of passwords with magic quotes.
4799          $validated = true;
4800  
4801      } else {
4802          for ($i=1; $i<=20; $i++) { // 20 alternative salts should be enough, right?
4803              $alt = 'passwordsaltalt'.$i;
4804              if (!empty($CFG->$alt)) {
4805                  if ($user->password === md5($password.$CFG->$alt) or $user->password === md5(addslashes($password).$CFG->$alt)) {
4806                      $validated = true;
4807                      break;
4808                  }
4809              }
4810          }
4811      }
4812  
4813      if ($validated) {
4814          // If the password matches the existing md5 hash, update to the
4815          // current hash algorithm while we have access to the user's password.
4816          update_internal_user_password($user, $password);
4817      }
4818  
4819      return $validated;
4820  }
4821  
4822  /**
4823   * Calculate hash for a plain text password.
4824   *
4825   * @param string $password Plain text password to be hashed.
4826   * @param bool $fasthash If true, use a low cost factor when generating the hash
4827   *                       This is much faster to generate but makes the hash
4828   *                       less secure. It is used when lots of hashes need to
4829   *                       be generated quickly.
4830   * @return string The hashed password.
4831   *
4832   * @throws moodle_exception If a problem occurs while generating the hash.
4833   */
4834  function hash_internal_user_password($password, $fasthash = false) {
4835      global $CFG;
4836  
4837      // Set the cost factor to 4 for fast hashing, otherwise use default cost.
4838      $options = ($fasthash) ? array('cost' => 4) : array();
4839  
4840      $generatedhash = password_hash($password, PASSWORD_DEFAULT, $options);
4841  
4842      if ($generatedhash === false || $generatedhash === null) {
4843          throw new moodle_exception('Failed to generate password hash.');
4844      }
4845  
4846      return $generatedhash;
4847  }
4848  
4849  /**
4850   * Update password hash in user object (if necessary).
4851   *
4852   * The password is updated if:
4853   * 1. The password has changed (the hash of $user->password is different
4854   *    to the hash of $password).
4855   * 2. The existing hash is using an out-of-date algorithm (or the legacy
4856   *    md5 algorithm).
4857   *
4858   * Updating the password will modify the $user object and the database
4859   * record to use the current hashing algorithm.
4860   * It will remove Web Services user tokens too.
4861   *
4862   * @param stdClass $user User object (password property may be updated).
4863   * @param string $password Plain text password.
4864   * @param bool $fasthash If true, use a low cost factor when generating the hash
4865   *                       This is much faster to generate but makes the hash
4866   *                       less secure. It is used when lots of hashes need to
4867   *                       be generated quickly.
4868   * @return bool Always returns true.
4869   */
4870  function update_internal_user_password($user, $password, $fasthash = false) {
4871      global $CFG, $DB;
4872  
4873      // Figure out what the hashed password should be.
4874      if (!isset($user->auth)) {
4875          debugging('User record in update_internal_user_password() must include field auth',
4876                  DEBUG_DEVELOPER);
4877          $user->auth = $DB->get_field('user', 'auth', array('id' => $user->id));
4878      }
4879      $authplugin = get_auth_plugin($user->auth);
4880      if ($authplugin->prevent_local_passwords()) {
4881          $hashedpassword = AUTH_PASSWORD_NOT_CACHED;
4882      } else {
4883          $hashedpassword = hash_internal_user_password($password, $fasthash);
4884      }
4885  
4886      $algorithmchanged = false;
4887  
4888      if ($hashedpassword === AUTH_PASSWORD_NOT_CACHED) {
4889          // Password is not cached, update it if not set to AUTH_PASSWORD_NOT_CACHED.
4890          $passwordchanged = ($user->password !== $hashedpassword);
4891  
4892      } else if (isset($user->password)) {
4893          // If verification fails then it means the password has changed.
4894          $passwordchanged = !password_verify($password, $user->password);
4895          $algorithmchanged = password_needs_rehash($user->password, PASSWORD_DEFAULT);
4896      } else {
4897          // While creating new user, password in unset in $user object, to avoid
4898          // saving it with user_create()
4899          $passwordchanged = true;
4900      }
4901  
4902      if ($passwordchanged || $algorithmchanged) {
4903          $DB->set_field('user', 'password',  $hashedpassword, array('id' => $user->id));
4904          $user->password = $hashedpassword;
4905  
4906          // Trigger event.
4907          $user = $DB->get_record('user', array('id' => $user->id));
4908          \core\event\user_password_updated::create_from_user($user)->trigger();
4909  
4910          // Remove WS user tokens.
4911          if (!empty($CFG->passwordchangetokendeletion)) {
4912              require_once($CFG->dirroot.'/webservice/lib.php');
4913              webservice::delete_user_ws_tokens($user->id);
4914          }
4915      }
4916  
4917      return true;
4918  }
4919  
4920  /**
4921   * Get a complete user record, which includes all the info in the user record.
4922   *
4923   * Intended for setting as $USER session variable
4924   *
4925   * @param string $field The user field to be checked for a given value.
4926   * @param string $value The value to match for $field.
4927   * @param int $mnethostid
4928   * @param bool $throwexception If true, it will throw an exception when there's no record found or when there are multiple records
4929   *                              found. Otherwise, it will just return false.
4930   * @return mixed False, or A {@link $USER} object.
4931   */
4932  function get_complete_user_data($field, $value, $mnethostid = null, $throwexception = false) {
4933      global $CFG, $DB;
4934  
4935      if (!$field || !$value) {
4936          return false;
4937      }
4938  
4939      // Change the field to lowercase.
4940      $field = core_text::strtolower($field);
4941  
4942      // List of case insensitive fields.
4943      $caseinsensitivefields = ['email'];
4944  
4945      // Username input is forced to lowercase and should be case sensitive.
4946      if ($field == 'username') {
4947          $value = core_text::strtolower($value);
4948      }
4949  
4950      // Build the WHERE clause for an SQL query.
4951      $params = array('fieldval' => $value);
4952  
4953      // Do a case-insensitive query, if necessary. These are generally very expensive. The performance can be improved on some DBs
4954      // such as MySQL by pre-filtering users with accent-insensitive subselect.
4955      if (in_array($field, $caseinsensitivefields)) {
4956          $fieldselect = $DB->sql_equal($field, ':fieldval', false);
4957          $idsubselect = $DB->sql_equal($field, ':fieldval2', false, false);
4958          $params['fieldval2'] = $value;
4959      } else {
4960          $fieldselect = "$field = :fieldval";
4961          $idsubselect = '';
4962      }
4963      $constraints = "$fieldselect AND deleted <> 1";
4964  
4965      // If we are loading user data based on anything other than id,
4966      // we must also restrict our search based on mnet host.
4967      if ($field != 'id') {
4968          if (empty($mnethostid)) {
4969              // If empty, we restrict to local users.
4970              $mnethostid = $CFG->mnet_localhost_id;
4971          }
4972      }
4973      if (!empty($mnethostid)) {
4974          $params['mnethostid'] = $mnethostid;
4975          $constraints .= " AND mnethostid = :mnethostid";
4976      }
4977  
4978      if ($idsubselect) {
4979          $constraints .= " AND id IN (SELECT id FROM {user} WHERE {$idsubselect})";
4980      }
4981  
4982      // Get all the basic user data.
4983      try {
4984          // Make sure that there's only a single record that matches our query.
4985          // For example, when fetching by email, multiple records might match the query as there's no guarantee that email addresses
4986          // are unique. Therefore we can't reliably tell whether the user profile data that we're fetching is the correct one.
4987          $user = $DB->get_record_select('user', $constraints, $params, '*', MUST_EXIST);
4988      } catch (dml_exception $exception) {
4989          if ($throwexception) {
4990              throw $exception;
4991          } else {
4992              // Return false when no records or multiple records were found.
4993              return false;
4994          }
4995      }
4996  
4997      // Get various settings and preferences.
4998  
4999      // Preload preference cache.
5000      check_user_preferences_loaded($user);
5001  
5002      // Load course enrolment related stuff.
5003      $user->lastcourseaccess    = array(); // During last session.
5004      $user->currentcourseaccess = array(); // During current session.
5005      if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid' => $user->id))) {
5006          foreach ($lastaccesses as $lastaccess) {
5007              $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess;
5008          }
5009      }
5010  
5011      $sql = "SELECT g.id, g.courseid
5012                FROM {groups} g, {groups_members} gm
5013               WHERE gm.groupid=g.id AND gm.userid=?";
5014  
5015      // This is a special hack to speedup calendar display.
5016      $user->groupmember = array();
5017      if (!isguestuser($user)) {
5018          if ($groups = $DB->get_records_sql($sql, array($user->id))) {
5019              foreach ($groups as $group) {
5020                  if (!array_key_exists($group->courseid, $user->groupmember)) {
5021                      $user->groupmember[$group->courseid] = array();
5022                  }
5023                  $user->groupmember[$group->courseid][$group->id] = $group->id;
5024              }
5025          }
5026      }
5027  
5028      // Add cohort theme.
5029      if (!empty($CFG->allowcohortthemes)) {
5030          require_once($CFG->dirroot . '/cohort/lib.php');
5031          if ($cohorttheme = cohort_get_user_cohort_theme($user->id)) {
5032              $user->cohorttheme = $cohorttheme;
5033          }
5034      }
5035  
5036      // Add the custom profile fields to the user record.
5037      $user->profile = array();
5038      if (!isguestuser($user)) {
5039          require_once($CFG->dirroot.'/user/profile/lib.php');
5040          profile_load_custom_fields($user);
5041      }
5042  
5043      // Rewrite some variables if necessary.
5044      if (!empty($user->description)) {
5045          // No need to cart all of it around.
5046          $user->description = true;
5047      }
5048      if (isguestuser($user)) {
5049          // Guest language always same as site.
5050          $user->lang = get_newuser_language();
5051          // Name always in current language.
5052          $user->firstname = get_string('guestuser');
5053          $user->lastname = ' ';
5054      }
5055  
5056      return $user;
5057  }
5058  
5059  /**
5060   * Validate a password against the configured password policy
5061   *
5062   * @param string $password the password to be checked against the password policy
5063   * @param string $errmsg the error message to display when the password doesn't comply with the policy.
5064   * @param stdClass $user the user object to perform password validation against. Defaults to null if not provided.
5065   *
5066   * @return bool true if the password is valid according to the policy. false otherwise.
5067   */
5068  function check_password_policy($password, &$errmsg, $user = null) {
5069      global $CFG;
5070  
5071      if (!empty($CFG->passwordpolicy)) {
5072          $errmsg = '';
5073          if (core_text::strlen($password) < $CFG->minpasswordlength) {
5074              $errmsg .= '<div>'. get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength) .'</div>';
5075          }
5076          if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) {
5077              $errmsg .= '<div>'. get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits) .'</div>';
5078          }
5079          if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) {
5080              $errmsg .= '<div>'. get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower) .'</div>';
5081          }
5082          if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) {
5083              $errmsg .= '<div>'. get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper) .'</div>';
5084          }
5085          if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) {
5086              $errmsg .= '<div>'. get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum) .'</div>';
5087          }
5088          if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) {
5089              $errmsg .= '<div>'. get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars) .'</div>';
5090          }
5091  
5092          // Fire any additional password policy functions from plugins.
5093          // Plugin functions should output an error message string or empty string for success.
5094          $pluginsfunction = get_plugins_with_function('check_password_policy');
5095          foreach ($pluginsfunction as $plugintype => $plugins) {
5096              foreach ($plugins as $pluginfunction) {
5097                  $pluginerr = $pluginfunction($password, $user);
5098                  if ($pluginerr) {
5099                      $errmsg .= '<div>'. $pluginerr .'</div>';
5100                  }
5101              }
5102          }
5103      }
5104  
5105      if ($errmsg == '') {
5106          return true;
5107      } else {
5108          return false;
5109      }
5110  }
5111  
5112  
5113  /**
5114   * When logging in, this function is run to set certain preferences for the current SESSION.
5115   */
5116  function set_login_session_preferences() {
5117      global $SESSION;
5118  
5119      $SESSION->justloggedin = true;
5120  
5121      unset($SESSION->lang);
5122      unset($SESSION->forcelang);
5123      unset($SESSION->load_navigation_admin);
5124  }
5125  
5126  
5127  /**
5128   * Delete a course, including all related data from the database, and any associated files.
5129   *
5130   * @param mixed $courseorid The id of the course or course object to delete.
5131   * @param bool $showfeedback Whether to display notifications of each action the function performs.
5132   * @return bool true if all the removals succeeded. false if there were any failures. If this
5133   *             method returns false, some of the removals will probably have succeeded, and others
5134   *             failed, but you have no way of knowing which.
5135   */
5136  function delete_course($courseorid, $showfeedback = true) {
5137      global $DB;
5138  
5139      if (is_object($courseorid)) {
5140          $courseid = $courseorid->id;
5141          $course   = $courseorid;
5142      } else {
5143          $courseid = $courseorid;
5144          if (!$course = $DB->get_record('course', array('id' => $courseid))) {
5145              return false;
5146          }
5147      }
5148      $context = context_course::instance($courseid);
5149  
5150      // Frontpage course can not be deleted!!
5151      if ($courseid == SITEID) {
5152          return false;
5153      }
5154  
5155      // Allow plugins to use this course before we completely delete it.
5156      if ($pluginsfunction = get_plugins_with_function('pre_course_delete')) {
5157          foreach ($pluginsfunction as $plugintype => $plugins) {
5158              foreach ($plugins as $pluginfunction) {
5159                  $pluginfunction($course);
5160              }
5161          }
5162      }
5163  
5164      // Tell the search manager we are about to delete a course. This prevents us sending updates
5165      // for each individual context being deleted.
5166      \core_search\manager::course_deleting_start($courseid);
5167  
5168      $handler = core_course\customfield\course_handler::create();
5169      $handler->delete_instance($courseid);
5170  
5171      // Make the course completely empty.
5172      remove_course_contents($courseid, $showfeedback);
5173  
5174      // Delete the course and related context instance.
5175      context_helper::delete_instance(CONTEXT_COURSE, $courseid);
5176  
5177      $DB->delete_records("course", array("id" => $courseid));
5178      $DB->delete_records("course_format_options", array("courseid" => $courseid));
5179  
5180      // Reset all course related caches here.
5181      if (class_exists('format_base', false)) {
5182          format_base::reset_course_cache($courseid);
5183      }
5184  
5185      // Tell search that we have deleted the course so it can delete course data from the index.
5186      \core_search\manager::course_deleting_finish($courseid);
5187  
5188      // Trigger a course deleted event.
5189      $event = \core\event\course_deleted::create(array(
5190          'objectid' => $course->id,
5191          'context' => $context,
5192          'other' => array(
5193              'shortname' => $course->shortname,
5194              'fullname' => $course->fullname,
5195              'idnumber' => $course->idnumber
5196              )
5197      ));
5198      $event->add_record_snapshot('course', $course);
5199      $event->trigger();
5200  
5201      return true;
5202  }
5203  
5204  /**
5205   * Clear a course out completely, deleting all content but don't delete the course itself.
5206   *
5207   * This function does not verify any permissions.
5208   *
5209   * Please note this function also deletes all user enrolments,
5210   * enrolment instances and role assignments by default.
5211   *
5212   * $options:
5213   *  - 'keep_roles_and_enrolments' - false by default
5214   *  - 'keep_groups_and_groupings' - false by default
5215   *
5216   * @param int $courseid The id of the course that is being deleted
5217   * @param bool $showfeedback Whether to display notifications of each action the function performs.
5218   * @param array $options extra options
5219   * @return bool true if all the removals succeeded. false if there were any failures. If this
5220   *             method returns false, some of the removals will probably have succeeded, and others
5221   *             failed, but you have no way of knowing which.
5222   */
5223  function remove_course_contents($courseid, $showfeedback = true, array $options = null) {
5224      global $CFG, $DB, $OUTPUT;
5225  
5226      require_once($CFG->libdir.'/badgeslib.php');
5227      require_once($CFG->libdir.'/completionlib.php');
5228      require_once($CFG->libdir.'/questionlib.php');
5229      require_once($CFG->libdir.'/gradelib.php');
5230      require_once($CFG->dirroot.'/group/lib.php');
5231      require_once($CFG->dirroot.'/comment/lib.php');
5232      require_once($CFG->dirroot.'/rating/lib.php');
5233      require_once($CFG->dirroot.'/notes/lib.php');
5234  
5235      // Handle course badges.
5236      badges_handle_course_deletion($courseid);
5237  
5238      // NOTE: these concatenated strings are suboptimal, but it is just extra info...
5239      $strdeleted = get_string('deleted').' - ';
5240  
5241      // Some crazy wishlist of stuff we should skip during purging of course content.
5242      $options = (array)$options;
5243  
5244      $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
5245      $coursecontext = context_course::instance($courseid);
5246      $fs = get_file_storage();
5247  
5248      // Delete course completion information, this has to be done before grades and enrols.
5249      $cc = new completion_info($course);
5250      $cc->clear_criteria();
5251      if ($showfeedback) {
5252          echo $OUTPUT->notification($strdeleted.get_string('completion', 'completion'), 'notifysuccess');
5253      }
5254  
5255      // Remove all data from gradebook - this needs to be done before course modules
5256      // because while deleting this information, the system may need to reference
5257      // the course modules that own the grades.
5258      remove_course_grades($courseid, $showfeedback);
5259      remove_grade_letters($coursecontext, $showfeedback);
5260  
5261      // Delete course blocks in any all child contexts,
5262      // they may depend on modules so delete them first.
5263      $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
5264      foreach ($childcontexts as $childcontext) {
5265          blocks_delete_all_for_context($childcontext->id);
5266      }
5267      unset($childcontexts);
5268      blocks_delete_all_for_context($coursecontext->id);
5269      if ($showfeedback) {
5270          echo $OUTPUT->notification($strdeleted.get_string('type_block_plural', 'plugin'), 'notifysuccess');
5271      }
5272  
5273      $DB->set_field('course_modules', 'deletioninprogress', '1', ['course' => $courseid]);
5274      rebuild_course_cache($courseid, true);
5275  
5276      // Get the list of all modules that are properly installed.
5277      $allmodules = $DB->get_records_menu('modules', array(), '', 'name, id');
5278  
5279      // Delete every instance of every module,
5280      // this has to be done before deleting of course level stuff.
5281      $locations = core_component::get_plugin_list('mod');
5282      foreach ($locations as $modname => $moddir) {
5283          if ($modname === 'NEWMODULE') {
5284              continue;
5285          }
5286          if (array_key_exists($modname, $allmodules)) {
5287              $sql = "SELECT cm.*, m.id AS modinstance, m.name, '$modname' AS modname
5288                FROM {".$modname."} m
5289                     LEFT JOIN {course_modules} cm ON cm.instance = m.id AND cm.module = :moduleid
5290               WHERE m.course = :courseid";
5291              $instances = $DB->get_records_sql($sql, array('courseid' => $course->id,
5292                  'modulename' => $modname, 'moduleid' => $allmodules[$modname]));
5293  
5294              include_once("$moddir/lib.php");                 // Shows php warning only if plugin defective.
5295              $moddelete = $modname .'_delete_instance';       // Delete everything connected to an instance.
5296  
5297              if ($instances) {
5298                  foreach ($instances as $cm) {
5299                      if ($cm->id) {
5300                          // Delete activity context questions and question categories.
5301                          question_delete_activity($cm);
5302                          // Notify the competency subsystem.
5303                          \core_competency\api::hook_course_module_deleted($cm);
5304                      }
5305                      if (function_exists($moddelete)) {
5306                          // This purges all module data in related tables, extra user prefs, settings, etc.
5307                          $moddelete($cm->modinstance);
5308                      } else {
5309                          // NOTE: we should not allow installation of modules with missing delete support!
5310                          debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!");
5311                          $DB->delete_records($modname, array('id' => $cm->modinstance));
5312                      }
5313  
5314                      if ($cm->id) {
5315                          // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition.
5316                          context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
5317                          $DB->delete_records('course_modules_completion', ['coursemoduleid' => $cm->id]);
5318                          $DB->delete_records('course_modules', array('id' => $cm->id));
5319                          rebuild_course_cache($cm->course, true);
5320                      }
5321                  }
5322              }
5323              if ($instances and $showfeedback) {
5324                  echo $OUTPUT->notification($strdeleted.get_string('pluginname', $modname), 'notifysuccess');
5325              }
5326          } else {
5327              // Ooops, this module is not properly installed, force-delete it in the next block.
5328          }
5329      }
5330  
5331      // We have tried to delete everything the nice way - now let's force-delete any remaining module data.
5332  
5333      // Delete completion defaults.
5334      $DB->delete_records("course_completion_defaults", array("course" => $courseid));
5335  
5336      // Remove all data from availability and completion tables that is associated
5337      // with course-modules belonging to this course. Note this is done even if the
5338      // features are not enabled now, in case they were enabled previously.
5339      $DB->delete_records_subquery('course_modules_completion', 'coursemoduleid', 'id',
5340              'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
5341  
5342      // Remove course-module data that has not been removed in modules' _delete_instance callbacks.
5343      $cms = $DB->get_records('course_modules', array('course' => $course->id));
5344      $allmodulesbyid = array_flip($allmodules);
5345      foreach ($cms as $cm) {
5346          if (array_key_exists($cm->module, $allmodulesbyid)) {
5347              try {
5348                  $DB->delete_records($allmodulesbyid[$cm->module], array('id' => $cm->instance));
5349              } catch (Exception $e) {
5350                  // Ignore weird or missing table problems.
5351              }
5352          }
5353          context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
5354          $DB->delete_records('course_modules', array('id' => $cm->id));
5355          rebuild_course_cache($cm->course, true);
5356      }
5357  
5358      if ($showfeedback) {
5359          echo $OUTPUT->notification($strdeleted.get_string('type_mod_plural', 'plugin'), 'notifysuccess');
5360      }
5361  
5362      // Delete questions and question categories.
5363      question_delete_course($course);
5364      if ($showfeedback) {
5365          echo $OUTPUT->notification($strdeleted.get_string('questions', 'question'), 'notifysuccess');
5366      }
5367  
5368      // Delete content bank contents.
5369      $cb = new \core_contentbank\contentbank();
5370      $cbdeleted = $cb->delete_contents($coursecontext);
5371      if ($showfeedback && $cbdeleted) {
5372          echo $OUTPUT->notification($strdeleted.get_string('contentbank', 'contentbank'), 'notifysuccess');
5373      }
5374  
5375      // Make sure there are no subcontexts left - all valid blocks and modules should be already gone.
5376      $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
5377      foreach ($childcontexts as $childcontext) {
5378          $childcontext->delete();
5379      }
5380      unset($childcontexts);
5381  
5382      // Remove roles and enrolments by default.
5383      if (empty($options['keep_roles_and_enrolments'])) {
5384          // This hack is used in restore when deleting contents of existing course.
5385          // During restore, we should remove only enrolment related data that the user performing the restore has a
5386          // permission to remove.
5387          $userid = $options['userid'] ?? null;
5388          enrol_course_delete($course, $userid);
5389          role_unassign_all(array('contextid' => $coursecontext->id, 'component' => ''), true);
5390          if ($showfeedback) {
5391              echo $OUTPUT->notification($strdeleted.get_string('type_enrol_plural', 'plugin'), 'notifysuccess');
5392          }
5393      }
5394  
5395      // Delete any groups, removing members and grouping/course links first.
5396      if (empty($options['keep_groups_and_groupings'])) {
5397          groups_delete_groupings($course->id, $showfeedback);
5398          groups_delete_groups($course->id, $showfeedback);
5399      }
5400  
5401      // Filters be gone!
5402      filter_delete_all_for_context($coursecontext->id);
5403  
5404      // Notes, you shall not pass!
5405      note_delete_all($course->id);
5406  
5407      // Die comments!
5408      comment::delete_comments($coursecontext->id);
5409  
5410      // Ratings are history too.
5411      $delopt = new stdclass();
5412      $delopt->contextid = $coursecontext->id;
5413      $rm = new rating_manager();
5414      $rm->delete_ratings($delopt);
5415  
5416      // Delete course tags.
5417      core_tag_tag::remove_all_item_tags('core', 'course', $course->id);
5418  
5419      // Notify the competency subsystem.
5420      \core_competency\api::hook_course_deleted($course);
5421  
5422      // Delete calendar events.
5423      $DB->delete_records('event', array('courseid' => $course->id));
5424      $fs->delete_area_files($coursecontext->id, 'calendar');
5425  
5426      // Delete all related records in other core tables that may have a courseid
5427      // This array stores the tables that need to be cleared, as
5428      // table_name => column_name that contains the course id.
5429      $tablestoclear = array(
5430          'backup_courses' => 'courseid',  // Scheduled backup stuff.
5431          'user_lastaccess' => 'courseid', // User access info.
5432      );
5433      foreach ($tablestoclear as $table => $col) {
5434          $DB->delete_records($table, array($col => $course->id));
5435      }
5436  
5437      // Delete all course backup files.
5438      $fs->delete_area_files($coursecontext->id, 'backup');
5439  
5440      // Cleanup course record - remove links to deleted stuff.
5441      $oldcourse = new stdClass();
5442      $oldcourse->id               = $course->id;
5443      $oldcourse->summary          = '';
5444      $oldcourse->cacherev         = 0;
5445      $oldcourse->legacyfiles      = 0;
5446      if (!empty($options['keep_groups_and_groupings'])) {
5447          $oldcourse->defaultgroupingid = 0;
5448      }
5449      $DB->update_record('course', $oldcourse);
5450  
5451      // Delete course sections.
5452      $DB->delete_records('course_sections', array('course' => $course->id));
5453  
5454      // Delete legacy, section and any other course files.
5455      $fs->delete_area_files($coursecontext->id, 'course'); // Files from summary and section.
5456  
5457      // Delete all remaining stuff linked to context such as files, comments, ratings, etc.
5458      if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) {
5459          // Easy, do not delete the context itself...
5460          $coursecontext->delete_content();
5461      } else {
5462          // Hack alert!!!!
5463          // We can not drop all context stuff because it would bork enrolments and roles,
5464          // there might be also files used by enrol plugins...
5465      }
5466  
5467      // Delete legacy files - just in case some files are still left there after conversion to new file api,
5468      // also some non-standard unsupported plugins may try to store something there.
5469      fulldelete($CFG->dataroot.'/'.$course->id);
5470  
5471      // Delete from cache to reduce the cache size especially makes sense in case of bulk course deletion.
5472      $cachemodinfo = cache::make('core', 'coursemodinfo');
5473      $cachemodinfo->delete($courseid);
5474  
5475      // Trigger a course content deleted event.
5476      $event = \core\event\course_content_deleted::create(array(
5477          'objectid' => $course->id,
5478          'context' => $coursecontext,
5479          'other' => array('shortname' => $course->shortname,
5480                           'fullname' => $course->fullname,
5481                           'options' => $options) // Passing this for legacy reasons.
5482      ));
5483      $event->add_record_snapshot('course', $course);
5484      $event->trigger();
5485  
5486      return true;
5487  }
5488  
5489  /**
5490   * Change dates in module - used from course reset.
5491   *
5492   * @param string $modname forum, assignment, etc
5493   * @param array $fields array of date fields from mod table
5494   * @param int $timeshift time difference
5495   * @param int $courseid
5496   * @param int $modid (Optional) passed if specific mod instance in course needs to be updated.
5497   * @return bool success
5498   */
5499  function shift_course_mod_dates($modname, $fields, $timeshift, $courseid, $modid = 0) {
5500      global $CFG, $DB;
5501      include_once($CFG->dirroot.'/mod/'.$modname.'/lib.php');
5502  
5503      $return = true;
5504      $params = array($timeshift, $courseid);
5505      foreach ($fields as $field) {
5506          $updatesql = "UPDATE {".$modname."}
5507                            SET $field = $field + ?
5508                          WHERE course=? AND $field<>0";
5509          if ($modid) {
5510              $updatesql .= ' AND id=?';
5511              $params[] = $modid;
5512          }
5513          $return = $DB->execute($updatesql, $params) && $return;
5514      }
5515  
5516      return $return;
5517  }
5518  
5519  /**
5520   * This function will empty a course of user data.
5521   * It will retain the activities and the structure of the course.
5522   *
5523   * @param object $data an object containing all the settings including courseid (without magic quotes)
5524   * @return array status array of array component, item, error
5525   */
5526  function reset_course_userdata($data) {
5527      global $CFG, $DB;
5528      require_once($CFG->libdir.'/gradelib.php');
5529      require_once($CFG->libdir.'/completionlib.php');
5530      require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php');
5531      require_once($CFG->dirroot.'/group/lib.php');
5532  
5533      $data->courseid = $data->id;
5534      $context = context_course::instance($data->courseid);
5535  
5536      $eventparams = array(
5537          'context' => $context,
5538          'courseid' => $data->id,
5539          'other' => array(
5540              'reset_options' => (array) $data
5541          )
5542      );
5543      $event = \core\event\course_reset_started::create($eventparams);
5544      $event->trigger();
5545  
5546      // Calculate the time shift of dates.
5547      if (!empty($data->reset_start_date)) {
5548          // Time part of course startdate should be zero.
5549          $data->timeshift = $data->reset_start_date - usergetmidnight($data->reset_start_date_old);
5550      } else {
5551          $data->timeshift = 0;
5552      }
5553  
5554      // Result array: component, item, error.
5555      $status = array();
5556  
5557      // Start the resetting.
5558      $componentstr = get_string('general');
5559  
5560      // Move the course start time.
5561      if (!empty($data->reset_start_date) and $data->timeshift) {
5562          // Change course start data.
5563          $DB->set_field('course', 'startdate', $data->reset_start_date, array('id' => $data->courseid));
5564          // Update all course and group events - do not move activity events.
5565          $updatesql = "UPDATE {event}
5566                           SET timestart = timestart + ?
5567                         WHERE courseid=? AND instance=0";
5568          $DB->execute($updatesql, array($data->timeshift, $data->courseid));
5569  
5570          // Update any date activity restrictions.
5571          if ($CFG->enableavailability) {
5572              \availability_date\condition::update_all_dates($data->courseid, $data->timeshift);
5573          }
5574  
5575          // Update completion expected dates.
5576          if ($CFG->enablecompletion) {
5577              $modinfo = get_fast_modinfo($data->courseid);
5578              $changed = false;
5579              foreach ($modinfo->get_cms() as $cm) {
5580                  if ($cm->completion && !empty($cm->completionexpected)) {
5581                      $DB->set_field('course_modules', 'completionexpected', $cm->completionexpected + $data->timeshift,
5582                          array('id' => $cm->id));
5583                      $changed = true;
5584                  }
5585              }
5586  
5587              // Clear course cache if changes made.
5588              if ($changed) {
5589                  rebuild_course_cache($data->courseid, true);
5590              }
5591  
5592              // Update course date completion criteria.
5593              \completion_criteria_date::update_date($data->courseid, $data->timeshift);
5594          }
5595  
5596          $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false);
5597      }
5598  
5599      if (!empty($data->reset_end_date)) {
5600          // If the user set a end date value respect it.
5601          $DB->set_field('course', 'enddate', $data->reset_end_date, array('id' => $data->courseid));
5602      } else if ($data->timeshift > 0 && $data->reset_end_date_old) {
5603          // If there is a time shift apply it to the end date as well.
5604          $enddate = $data->reset_end_date_old + $data->timeshift;
5605          $DB->set_field('course', 'enddate', $enddate, array('id' => $data->courseid));
5606      }
5607  
5608      if (!empty($data->reset_events)) {
5609          $DB->delete_records('event', array('courseid' => $data->courseid));
5610          $status[] = array('component' => $componentstr, 'item' => get_string('deleteevents', 'calendar'), 'error' => false);
5611      }
5612  
5613      if (!empty($data->reset_notes)) {
5614          require_once($CFG->dirroot.'/notes/lib.php');
5615          note_delete_all($data->courseid);
5616          $status[] = array('component' => $componentstr, 'item' => get_string('deletenotes', 'notes'), 'error' => false);
5617      }
5618  
5619      if (!empty($data->delete_blog_associations)) {
5620          require_once($CFG->dirroot.'/blog/lib.php');
5621          blog_remove_associations_for_course($data->courseid);
5622          $status[] = array('component' => $componentstr, 'item' => get_string('deleteblogassociations', 'blog'), 'error' => false);
5623      }
5624  
5625      if (!empty($data->reset_completion)) {
5626          // Delete course and activity completion information.
5627          $course = $DB->get_record('course', array('id' => $data->courseid));
5628          $cc = new completion_info($course);
5629          $cc->delete_all_completion_data();
5630          $status[] = array('component' => $componentstr,
5631                  'item' => get_string('deletecompletiondata', 'completion'), 'error' => false);
5632      }
5633  
5634      if (!empty($data->reset_competency_ratings)) {
5635          \core_competency\api::hook_course_reset_competency_ratings($data->courseid);
5636          $status[] = array('component' => $componentstr,
5637              'item' => get_string('deletecompetencyratings', 'core_competency'), 'error' => false);
5638      }
5639  
5640      $componentstr = get_string('roles');
5641  
5642      if (!empty($data->reset_roles_overrides)) {
5643          $children = $context->get_child_contexts();
5644          foreach ($children as $child) {
5645              $child->delete_capabilities();
5646          }
5647          $context->delete_capabilities();
5648          $status[] = array('component' => $componentstr, 'item' => get_string('deletecourseoverrides', 'role'), 'error' => false);
5649      }
5650  
5651      if (!empty($data->reset_roles_local)) {
5652          $children = $context->get_child_contexts();
5653          foreach ($children as $child) {
5654              role_unassign_all(array('contextid' => $child->id));
5655          }
5656          $status[] = array('component' => $componentstr, 'item' => get_string('deletelocalroles', 'role'), 'error' => false);
5657      }
5658  
5659      // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc.
5660      $data->unenrolled = array();
5661      if (!empty($data->unenrol_users)) {
5662          $plugins = enrol_get_plugins(true);
5663          $instances = enrol_get_instances($data->courseid, true);
5664          foreach ($instances as $key => $instance) {
5665              if (!isset($plugins[$instance->enrol])) {
5666                  unset($instances[$key]);
5667                  continue;
5668              }
5669          }
5670  
5671          $usersroles = enrol_get_course_users_roles($data->courseid);
5672          foreach ($data->unenrol_users as $withroleid) {
5673              if ($withroleid) {
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                            JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)";
5679                  $params = array('courseid' => $data->courseid, 'roleid' => $withroleid, 'courselevel' => CONTEXT_COURSE);
5680  
5681              } else {
5682                  // Without any role assigned at course context.
5683                  $sql = "SELECT ue.*
5684                            FROM {user_enrolments} ue
5685                            JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5686                            JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5687                       LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid)
5688                           WHERE ra.id IS null";
5689                  $params = array('courseid' => $data->courseid, 'courselevel' => CONTEXT_COURSE);
5690              }
5691  
5692              $rs = $DB->get_recordset_sql($sql, $params);
5693              foreach ($rs as $ue) {
5694                  if (!isset($instances[$ue->enrolid])) {
5695                      continue;
5696                  }
5697                  $instance = $instances[$ue->enrolid];
5698                  $plugin = $plugins[$instance->enrol];
5699                  if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) {
5700                      continue;
5701                  }
5702  
5703                  if ($withroleid && count($usersroles[$ue->userid]) > 1) {
5704                      // If we don't remove all roles and user has more than one role, just remove this role.
5705                      role_unassign($withroleid, $ue->userid, $context->id);
5706  
5707                      unset($usersroles[$ue->userid][$withroleid]);
5708                  } else {
5709                      // If we remove all roles or user has only one role, unenrol user from course.
5710                      $plugin->unenrol_user($instance, $ue->userid);
5711                  }
5712                  $data->unenrolled[$ue->userid] = $ue->userid;
5713              }
5714              $rs->close();
5715          }
5716      }
5717      if (!empty($data->unenrolled)) {
5718          $status[] = array(
5719              'component' => $componentstr,
5720              'item' => get_string('unenrol', 'enrol').' ('.count($data->unenrolled).')',
5721              'error' => false
5722          );
5723      }
5724  
5725      $componentstr = get_string('groups');
5726  
5727      // Remove all group members.
5728      if (!empty($data->reset_groups_members)) {
5729          groups_delete_group_members($data->courseid);
5730          $status[] = array('component' => $componentstr, 'item' => get_string('removegroupsmembers', 'group'), 'error' => false);
5731      }
5732  
5733      // Remove all groups.
5734      if (!empty($data->reset_groups_remove)) {
5735          groups_delete_groups($data->courseid, false);
5736          $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroups', 'group'), 'error' => false);
5737      }
5738  
5739      // Remove all grouping members.
5740      if (!empty($data->reset_groupings_members)) {
5741          groups_delete_groupings_groups($data->courseid, false);
5742          $status[] = array('component' => $componentstr, 'item' => get_string('removegroupingsmembers', 'group'), 'error' => false);
5743      }
5744  
5745      // Remove all groupings.
5746      if (!empty($data->reset_groupings_remove)) {
5747          groups_delete_groupings($data->courseid, false);
5748          $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroupings', 'group'), 'error' => false);
5749      }
5750  
5751      // Look in every instance of every module for data to delete.
5752      $unsupportedmods = array();
5753      if ($allmods = $DB->get_records('modules') ) {
5754          foreach ($allmods as $mod) {
5755              $modname = $mod->name;
5756              $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php';
5757              $moddeleteuserdata = $modname.'_reset_userdata';   // Function to delete user data.
5758              if (file_exists($modfile)) {
5759                  if (!$DB->count_records($modname, array('course' => $data->courseid))) {
5760                      continue; // Skip mods with no instances.
5761                  }
5762                  include_once($modfile);
5763                  if (function_exists($moddeleteuserdata)) {
5764                      $modstatus = $moddeleteuserdata($data);
5765                      if (is_array($modstatus)) {
5766                          $status = array_merge($status, $modstatus);
5767                      } else {
5768                          debugging('Module '.$modname.' returned incorrect staus - must be an array!');
5769                      }
5770                  } else {
5771                      $unsupportedmods[] = $mod;
5772                  }
5773              } else {
5774                  debugging('Missing lib.php in '.$modname.' module!');
5775              }
5776              // Update calendar events for all modules.
5777              course_module_bulk_update_calendar_events($modname, $data->courseid);
5778          }
5779      }
5780  
5781      // Mention unsupported mods.
5782      if (!empty($unsupportedmods)) {
5783          foreach ($unsupportedmods as $mod) {
5784              $status[] = array(
5785                  'component' => get_string('modulenameplural', $mod->name),
5786                  'item' => '',
5787                  'error' => get_string('resetnotimplemented')
5788              );
5789          }
5790      }
5791  
5792      $componentstr = get_string('gradebook', 'grades');
5793      // Reset gradebook,.
5794      if (!empty($data->reset_gradebook_items)) {
5795          remove_course_grades($data->courseid, false);
5796          grade_grab_course_grades($data->courseid);
5797          grade_regrade_final_grades($data->courseid);
5798          $status[] = array('component' => $componentstr, 'item' => get_string('removeallcourseitems', 'grades'), 'error' => false);
5799  
5800      } else if (!empty($data->reset_gradebook_grades)) {
5801          grade_course_reset($data->courseid);
5802          $status[] = array('component' => $componentstr, 'item' => get_string('removeallcoursegrades', 'grades'), 'error' => false);
5803      }
5804      // Reset comments.
5805      if (!empty($data->reset_comments)) {
5806          require_once($CFG->dirroot.'/comment/lib.php');
5807          comment::reset_course_page_comments($context);
5808      }
5809  
5810      $event = \core\event\course_reset_ended::create($eventparams);
5811      $event->trigger();
5812  
5813      return $status;
5814  }
5815  
5816  /**
5817   * Generate an email processing address.
5818   *
5819   * @param int $modid
5820   * @param string $modargs
5821   * @return string Returns email processing address
5822   */
5823  function generate_email_processing_address($modid, $modargs) {
5824      global $CFG;
5825  
5826      $header = $CFG->mailprefix . substr(base64_encode(pack('C', $modid)), 0, 2).$modargs;
5827      return $header . substr(md5($header.get_site_identifier()), 0, 16).'@'.$CFG->maildomain;
5828  }
5829  
5830  /**
5831   * ?
5832   *
5833   * @todo Finish documenting this function
5834   *
5835   * @param string $modargs
5836   * @param string $body Currently unused
5837   */
5838  function moodle_process_email($modargs, $body) {
5839      global $DB;
5840  
5841      // The first char should be an unencoded letter. We'll take this as an action.
5842      switch ($modargs[0]) {
5843          case 'B': { // Bounce.
5844              list(, $userid) = unpack('V', base64_decode(substr($modargs, 1, 8)));
5845              if ($user = $DB->get_record("user", array('id' => $userid), "id,email")) {
5846                  // Check the half md5 of their email.
5847                  $md5check = substr(md5($user->email), 0, 16);
5848                  if ($md5check == substr($modargs, -16)) {
5849                      set_bounce_count($user);
5850                  }
5851                  // Else maybe they've already changed it?
5852              }
5853          }
5854          break;
5855          // Maybe more later?
5856      }
5857  }
5858  
5859  // CORRESPONDENCE.
5860  
5861  /**
5862   * Get mailer instance, enable buffering, flush buffer or disable buffering.
5863   *
5864   * @param string $action 'get', 'buffer', 'close' or 'flush'
5865   * @return moodle_phpmailer|null mailer instance if 'get' used or nothing
5866   */
5867  function get_mailer($action='get') {
5868      global $CFG;
5869  
5870      /** @var moodle_phpmailer $mailer */
5871      static $mailer  = null;
5872      static $counter = 0;
5873  
5874      if (!isset($CFG->smtpmaxbulk)) {
5875          $CFG->smtpmaxbulk = 1;
5876      }
5877  
5878      if ($action == 'get') {
5879          $prevkeepalive = false;
5880  
5881          if (isset($mailer) and $mailer->Mailer == 'smtp') {
5882              if ($counter < $CFG->smtpmaxbulk and !$mailer->isError()) {
5883                  $counter++;
5884                  // Reset the mailer.
5885                  $mailer->Priority         = 3;
5886                  $mailer->CharSet          = 'UTF-8'; // Our default.
5887                  $mailer->ContentType      = "text/plain";
5888                  $mailer->Encoding         = "8bit";
5889                  $mailer->From             = "root@localhost";
5890                  $mailer->FromName         = "Root User";
5891                  $mailer->Sender           = "";
5892                  $mailer->Subject          = "";
5893                  $mailer->Body             = "";
5894                  $mailer->AltBody          = "";
5895                  $mailer->ConfirmReadingTo = "";
5896  
5897                  $mailer->clearAllRecipients();
5898                  $mailer->clearReplyTos();
5899                  $mailer->clearAttachments();
5900                  $mailer->clearCustomHeaders();
5901                  return $mailer;
5902              }
5903  
5904              $prevkeepalive = $mailer->SMTPKeepAlive;
5905              get_mailer('flush');
5906          }
5907  
5908          require_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php');
5909          $mailer = new moodle_phpmailer();
5910  
5911          $counter = 1;
5912  
5913          if ($CFG->smtphosts == 'qmail') {
5914              // Use Qmail system.
5915              $mailer->isQmail();
5916  
5917          } else if (empty($CFG->smtphosts)) {
5918              // Use PHP mail() = sendmail.
5919              $mailer->isMail();
5920  
5921          } else {
5922              // Use SMTP directly.
5923              $mailer->isSMTP();
5924              if (!empty($CFG->debugsmtp) && (!empty($CFG->debugdeveloper))) {
5925                  $mailer->SMTPDebug = 3;
5926              }
5927              // Specify main and backup servers.
5928              $mailer->Host          = $CFG->smtphosts;
5929              // Specify secure connection protocol.
5930              $mailer->SMTPSecure    = $CFG->smtpsecure;
5931              // Use previous keepalive.
5932              $mailer->SMTPKeepAlive = $prevkeepalive;
5933  
5934              if ($CFG->smtpuser) {
5935                  // Use SMTP authentication.
5936                  $mailer->SMTPAuth = true;
5937                  $mailer->Username = $CFG->smtpuser;
5938                  $mailer->Password = $CFG->smtppass;
5939              }
5940          }
5941  
5942          return $mailer;
5943      }
5944  
5945      $nothing = null;
5946  
5947      // Keep smtp session open after sending.
5948      if ($action == 'buffer') {
5949          if (!empty($CFG->smtpmaxbulk)) {
5950              get_mailer('flush');
5951              $m = get_mailer();
5952              if ($m->Mailer == 'smtp') {
5953                  $m->SMTPKeepAlive = true;
5954              }
5955          }
5956          return $nothing;
5957      }
5958  
5959      // Close smtp session, but continue buffering.
5960      if ($action == 'flush') {
5961          if (isset($mailer) and $mailer->Mailer == 'smtp') {
5962              if (!empty($mailer->SMTPDebug)) {
5963                  echo '<pre>'."\n";
5964              }
5965              $mailer->SmtpClose();
5966              if (!empty($mailer->SMTPDebug)) {
5967                  echo '</pre>';
5968              }
5969          }
5970          return $nothing;
5971      }
5972  
5973      // Close smtp session, do not buffer anymore.
5974      if ($action == 'close') {
5975          if (isset($mailer) and $mailer->Mailer == 'smtp') {
5976              get_mailer('flush');
5977              $mailer->SMTPKeepAlive = false;
5978          }
5979          $mailer = null; // Better force new instance.
5980          return $nothing;
5981      }
5982  }
5983  
5984  /**
5985   * A helper function to test for email diversion
5986   *
5987   * @param string $email
5988   * @return bool Returns true if the email should be diverted
5989   */
5990  function email_should_be_diverted($email) {
5991      global $CFG;
5992  
5993      if (empty($CFG->divertallemailsto)) {
5994          return false;
5995      }
5996  
5997      if (empty($CFG->divertallemailsexcept)) {
5998          return true;
5999      }
6000  
6001      $patterns = array_map('trim', preg_split("/[\s,]+/", $CFG->divertallemailsexcept));
6002      foreach ($patterns as $pattern) {
6003          if (preg_match("/$pattern/", $email)) {
6004              return false;
6005          }
6006      }
6007  
6008      return true;
6009  }
6010  
6011  /**
6012   * Generate a unique email Message-ID using the moodle domain and install path
6013   *
6014   * @param string $localpart An optional unique message id prefix.
6015   * @return string The formatted ID ready for appending to the email headers.
6016   */
6017  function generate_email_messageid($localpart = null) {
6018      global $CFG;
6019  
6020      $urlinfo = parse_url($CFG->wwwroot);
6021      $base = '@' . $urlinfo['host'];
6022  
6023      // If multiple moodles are on the same domain we want to tell them
6024      // apart so we add the install path to the local part. This means
6025      // that the id local part should never contain a / character so
6026      // we can correctly parse the id to reassemble the wwwroot.
6027      if (isset($urlinfo['path'])) {
6028          $base = $urlinfo['path'] . $base;
6029      }
6030  
6031      if (empty($localpart)) {
6032          $localpart = uniqid('', true);
6033      }
6034  
6035      // Because we may have an option /installpath suffix to the local part
6036      // of the id we need to escape any / chars which are in the $localpart.
6037      $localpart = str_replace('/', '%2F', $localpart);
6038  
6039      return '<' . $localpart . $base . '>';
6040  }
6041  
6042  /**
6043   * Send an email to a specified user
6044   *
6045   * @param stdClass $user  A {@link $USER} object
6046   * @param stdClass $from A {@link $USER} object
6047   * @param string $subject plain text subject line of the email
6048   * @param string $messagetext plain text version of the message
6049   * @param string $messagehtml complete html version of the message (optional)
6050   * @param string $attachment a file on the filesystem, either relative to $CFG->dataroot or a full path to a file in one of
6051   *          the following directories: $CFG->cachedir, $CFG->dataroot, $CFG->dirroot, $CFG->localcachedir, $CFG->tempdir
6052   * @param string $attachname the name of the file (extension indicates MIME)
6053   * @param bool $usetrueaddress determines whether $from email address should
6054   *          be sent out. Will be overruled by user profile setting for maildisplay
6055   * @param string $replyto Email address to reply to
6056   * @param string $replytoname Name of reply to recipient
6057   * @param int $wordwrapwidth custom word wrap width, default 79
6058   * @return bool Returns true if mail was sent OK and false if there was an error.
6059   */
6060  function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '', $attachment = '', $attachname = '',
6061                         $usetrueaddress = true, $replyto = '', $replytoname = '', $wordwrapwidth = 79) {
6062  
6063      global $CFG, $PAGE, $SITE;
6064  
6065      if (empty($user) or empty($user->id)) {
6066          debugging('Can not send email to null user', DEBUG_DEVELOPER);
6067          return false;
6068      }
6069  
6070      if (empty($user->email)) {
6071          debugging('Can not send email to user without email: '.$user->id, DEBUG_DEVELOPER);
6072          return false;
6073      }
6074  
6075      if (!empty($user->deleted)) {
6076          debugging('Can not send email to deleted user: '.$user->id, DEBUG_DEVELOPER);
6077          return false;
6078      }
6079  
6080      if (defined('BEHAT_SITE_RUNNING')) {
6081          // Fake email sending in behat.
6082          return true;
6083      }
6084  
6085      if (!empty($CFG->noemailever)) {
6086          // Hidden setting for development sites, set in config.php if needed.
6087          debugging('Not sending email due to $CFG->noemailever config setting', DEBUG_NORMAL);
6088          return true;
6089      }
6090  
6091      if (email_should_be_diverted($user->email)) {
6092          $subject = "[DIVERTED {$user->email}] $subject";
6093          $user = clone($user);
6094          $user->email = $CFG->divertallemailsto;
6095      }
6096  
6097      // Skip mail to suspended users.
6098      if ((isset($user->auth) && $user->auth=='nologin') or (isset($user->suspended) && $user->suspended)) {
6099          return true;
6100      }
6101  
6102      if (!validate_email($user->email)) {
6103          // We can not send emails to invalid addresses - it might create security issue or confuse the mailer.
6104          debugging("email_to_user: User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending.");
6105          return false;
6106      }
6107  
6108      if (over_bounce_threshold($user)) {
6109          debugging("email_to_user: User $user->id (".fullname($user).") is over bounce threshold! Not sending.");
6110          return false;
6111      }
6112  
6113      // TLD .invalid  is specifically reserved for invalid domain names.
6114      // For More information, see {@link http://tools.ietf.org/html/rfc2606#section-2}.
6115      if (substr($user->email, -8) == '.invalid') {
6116          debugging("email_to_user: User $user->id (".fullname($user).") email domain ($user->email) is invalid! Not sending.");
6117          return true; // This is not an error.
6118      }
6119  
6120      // If the user is a remote mnet user, parse the email text for URL to the
6121      // wwwroot and modify the url to direct the user's browser to login at their
6122      // home site (identity provider - idp) before hitting the link itself.
6123      if (is_mnet_remote_user($user)) {
6124          require_once($CFG->dirroot.'/mnet/lib.php');
6125  
6126          $jumpurl = mnet_get_idp_jump_url($user);
6127          $callback = partial('mnet_sso_apply_indirection', $jumpurl);
6128  
6129          $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%",
6130                  $callback,
6131                  $messagetext);
6132          $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%",
6133                  $callback,
6134                  $messagehtml);
6135      }
6136      $mail = get_mailer();
6137  
6138      if (!empty($mail->SMTPDebug)) {
6139          echo '<pre>' . "\n";
6140      }
6141  
6142      $temprecipients = array();
6143      $tempreplyto = array();
6144  
6145      // Make sure that we fall back onto some reasonable no-reply address.
6146      $noreplyaddressdefault = 'noreply@' . get_host_from_url($CFG->wwwroot);
6147      $noreplyaddress = empty($CFG->noreplyaddress) ? $noreplyaddressdefault : $CFG->noreplyaddress;
6148  
6149      if (!validate_email($noreplyaddress)) {
6150          debugging('email_to_user: Invalid noreply-email '.s($noreplyaddress));
6151          $noreplyaddress = $noreplyaddressdefault;
6152      }
6153  
6154      // Make up an email address for handling bounces.
6155      if (!empty($CFG->handlebounces)) {
6156          $modargs = 'B'.base64_encode(pack('V', $user->id)).substr(md5($user->email), 0, 16);
6157          $mail->Sender = generate_email_processing_address(0, $modargs);
6158      } else {
6159          $mail->Sender = $noreplyaddress;
6160      }
6161  
6162      // Make sure that the explicit replyto is valid, fall back to the implicit one.
6163      if (!empty($replyto) && !validate_email($replyto)) {
6164          debugging('email_to_user: Invalid replyto-email '.s($replyto));
6165          $replyto = $noreplyaddress;
6166      }
6167  
6168      if (is_string($from)) { // So we can pass whatever we want if there is need.
6169          $mail->From     = $noreplyaddress;
6170          $mail->FromName = $from;
6171      // Check if using the true address is true, and the email is in the list of allowed domains for sending email,
6172      // and that the senders email setting is either displayed to everyone, or display to only other users that are enrolled
6173      // in a course with the sender.
6174      } else if ($usetrueaddress && can_send_from_real_email_address($from, $user)) {
6175          if (!validate_email($from->email)) {
6176              debugging('email_to_user: Invalid from-email '.s($from->email).' - not sending');
6177              // Better not to use $noreplyaddress in this case.
6178              return false;
6179          }
6180          $mail->From = $from->email;
6181          $fromdetails = new stdClass();
6182          $fromdetails->name = fullname($from);
6183          $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
6184          $fromdetails->siteshortname = format_string($SITE->shortname);
6185          $fromstring = $fromdetails->name;
6186          if ($CFG->emailfromvia == EMAIL_VIA_ALWAYS) {
6187              $fromstring = get_string('emailvia', 'core', $fromdetails);
6188          }
6189          $mail->FromName = $fromstring;
6190          if (empty($replyto)) {
6191              $tempreplyto[] = array($from->email, fullname($from));
6192          }
6193      } else {
6194          $mail->From = $noreplyaddress;
6195          $fromdetails = new stdClass();
6196          $fromdetails->name = fullname($from);
6197          $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
6198          $fromdetails->siteshortname = format_string($SITE->shortname);
6199          $fromstring = $fromdetails->name;
6200          if ($CFG->emailfromvia != EMAIL_VIA_NEVER) {
6201              $fromstring = get_string('emailvia', 'core', $fromdetails);
6202          }
6203          $mail->FromName = $fromstring;
6204          if (empty($replyto)) {
6205              $tempreplyto[] = array($noreplyaddress, get_string('noreplyname'));
6206          }
6207      }
6208  
6209      if (!empty($replyto)) {
6210          $tempreplyto[] = array($replyto, $replytoname);
6211      }
6212  
6213      $temprecipients[] = array($user->email, fullname($user));
6214  
6215      // Set word wrap.
6216      $mail->WordWrap = $wordwrapwidth;
6217  
6218      if (!empty($from->customheaders)) {
6219          // Add custom headers.
6220          if (is_array($from->customheaders)) {
6221              foreach ($from->customheaders as $customheader) {
6222                  $mail->addCustomHeader($customheader);
6223              }
6224          } else {
6225              $mail->addCustomHeader($from->customheaders);
6226          }
6227      }
6228  
6229      // If the X-PHP-Originating-Script email header is on then also add an additional
6230      // header with details of where exactly in moodle the email was triggered from,
6231      // either a call to message_send() or to email_to_user().
6232      if (ini_get('mail.add_x_header')) {
6233  
6234          $stack = debug_backtrace(false);
6235          $origin = $stack[0];
6236  
6237          foreach ($stack as $depth => $call) {
6238              if ($call['function'] == 'message_send') {
6239                  $origin = $call;
6240              }
6241          }
6242  
6243          $originheader = $CFG->wwwroot . ' => ' . gethostname() . ':'
6244               . str_replace($CFG->dirroot . '/', '', $origin['file']) . ':' . $origin['line'];
6245          $mail->addCustomHeader('X-Moodle-Originating-Script: ' . $originheader);
6246      }
6247  
6248      if (!empty($CFG->emailheaders)) {
6249          $headers = array_map('trim', explode("\n", $CFG->emailheaders));
6250          foreach ($headers as $header) {
6251              if (!empty($header)) {
6252                  $mail->addCustomHeader($header);
6253              }
6254          }
6255      }
6256  
6257      if (!empty($from->priority)) {
6258          $mail->Priority = $from->priority;
6259      }
6260  
6261      $renderer = $PAGE->get_renderer('core');
6262      $context = array(
6263          'sitefullname' => $SITE->fullname,
6264          'siteshortname' => $SITE->shortname,
6265          'sitewwwroot' => $CFG->wwwroot,
6266          'subject' => $subject,
6267          'prefix' => $CFG->emailsubjectprefix,
6268          'to' => $user->email,
6269          'toname' => fullname($user),
6270          'from' => $mail->From,
6271          'fromname' => $mail->FromName,
6272      );
6273      if (!empty($tempreplyto[0])) {
6274          $context['replyto'] = $tempreplyto[0][0];
6275          $context['replytoname'] = $tempreplyto[0][1];
6276      }
6277      if ($user->id > 0) {
6278          $context['touserid'] = $user->id;
6279          $context['tousername'] = $user->username;
6280      }
6281  
6282      if (!empty($user->mailformat) && $user->mailformat == 1) {
6283          // Only process html templates if the user preferences allow html email.
6284  
6285          if (!$messagehtml) {
6286              // If no html has been given, BUT there is an html wrapping template then
6287              // auto convert the text to html and then wrap it.
6288              $messagehtml = trim(text_to_html($messagetext));
6289          }
6290          $context['body'] = $messagehtml;
6291          $messagehtml = $renderer->render_from_template('core/email_html', $context);
6292      }
6293  
6294      $context['body'] = html_to_text(nl2br($messagetext));
6295      $mail->Subject = $renderer->render_from_template('core/email_subject', $context);
6296      $mail->FromName = $renderer->render_from_template('core/email_fromname', $context);
6297      $messagetext = $renderer->render_from_template('core/email_text', $context);
6298  
6299      // Autogenerate a MessageID if it's missing.
6300      if (empty($mail->MessageID)) {
6301          $mail->MessageID = generate_email_messageid();
6302      }
6303  
6304      if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) {
6305          // Don't ever send HTML to users who don't want it.
6306          $mail->isHTML(true);
6307          $mail->Encoding = 'quoted-printable';
6308          $mail->Body    =  $messagehtml;
6309          $mail->AltBody =  "\n$messagetext\n";
6310      } else {
6311          $mail->IsHTML(false);
6312          $mail->Body =  "\n$messagetext\n";
6313      }
6314  
6315      if ($attachment && $attachname) {
6316          if (preg_match( "~\\.\\.~" , $attachment )) {
6317              // Security check for ".." in dir path.
6318              $supportuser = core_user::get_support_user();
6319              $temprecipients[] = array($supportuser->email, fullname($supportuser, true));
6320              $mail->addStringAttachment('Error in attachment.  User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain');
6321          } else {
6322              require_once($CFG->libdir.'/filelib.php');
6323              $mimetype = mimeinfo('type', $attachname);
6324  
6325              // Before doing the comparison, make sure that the paths are correct (Windows uses slashes in the other direction).
6326              // The absolute (real) path is also fetched to ensure that comparisons to allowed paths are compared equally.
6327              $attachpath = str_replace('\\', '/', realpath($attachment));
6328  
6329              // Build an array of all filepaths from which attachments can be added (normalised slashes, absolute/real path).
6330              $allowedpaths = array_map(function(string $path): string {
6331                  return str_replace('\\', '/', realpath($path));
6332              }, [
6333                  $CFG->cachedir,
6334                  $CFG->dataroot,
6335                  $CFG->dirroot,
6336                  $CFG->localcachedir,
6337                  $CFG->tempdir,
6338                  $CFG->localrequestdir,
6339              ]);
6340  
6341              // Set addpath to true.
6342              $addpath = true;
6343  
6344              // Check if attachment includes one of the allowed paths.
6345              foreach (array_filter($allowedpaths) as $allowedpath) {
6346                  // Set addpath to false if the attachment includes one of the allowed paths.
6347                  if (strpos($attachpath, $allowedpath) === 0) {
6348                      $addpath = false;
6349                      break;
6350                  }
6351              }
6352  
6353              // If the attachment is a full path to a file in the multiple allowed paths, use it as is,
6354              // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons).
6355              if ($addpath == true) {
6356                  $attachment = $CFG->dataroot . '/' . $attachment;
6357              }
6358  
6359              $mail->addAttachment($attachment, $attachname, 'base64', $mimetype);
6360          }
6361      }
6362  
6363      // Check if the email should be sent in an other charset then the default UTF-8.
6364      if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) {
6365  
6366          // Use the defined site mail charset or eventually the one preferred by the recipient.
6367          $charset = $CFG->sitemailcharset;
6368          if (!empty($CFG->allowusermailcharset)) {
6369              if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) {
6370                  $charset = $useremailcharset;
6371              }
6372          }
6373  
6374          // Convert all the necessary strings if the charset is supported.
6375          $charsets = get_list_of_charsets();
6376          unset($charsets['UTF-8']);
6377          if (in_array($charset, $charsets)) {
6378              $mail->CharSet  = $charset;
6379              $mail->FromName = core_text::convert($mail->FromName, 'utf-8', strtolower($charset));
6380              $mail->Subject  = core_text::convert($mail->Subject, 'utf-8', strtolower($charset));
6381              $mail->Body     = core_text::convert($mail->Body, 'utf-8', strtolower($charset));
6382              $mail->AltBody  = core_text::convert($mail->AltBody, 'utf-8', strtolower($charset));
6383  
6384              foreach ($temprecipients as $key => $values) {
6385                  $temprecipients[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
6386              }
6387              foreach ($tempreplyto as $key => $values) {
6388                  $tempreplyto[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
6389              }
6390          }
6391      }
6392  
6393      foreach ($temprecipients as $values) {
6394          $mail->addAddress($values[0], $values[1]);
6395      }
6396      foreach ($tempreplyto as $values) {
6397          $mail->addReplyTo($values[0], $values[1]);
6398      }
6399  
6400      if (!empty($CFG->emaildkimselector)) {
6401          $domain = substr(strrchr($mail->From, "@"), 1);
6402          $pempath = "{$CFG->dataroot}/dkim/{$domain}/{$CFG->emaildkimselector}.private";
6403          if (file_exists($pempath)) {
6404              $mail->DKIM_domain      = $domain;
6405              $mail->DKIM_private     = $pempath;
6406              $mail->DKIM_selector    = $CFG->emaildkimselector;
6407              $mail->DKIM_identity    = $mail->From;
6408          } else {
6409              debugging("Email DKIM selector chosen due to {$mail->From} but no certificate found at $pempath", DEBUG_DEVELOPER);
6410          }
6411      }
6412  
6413      if ($mail->send()) {
6414          set_send_count($user);
6415          if (!empty($mail->SMTPDebug)) {
6416              echo '</pre>';
6417          }
6418          return true;
6419      } else {
6420          // Trigger event for failing to send email.
6421          $event = \core\event\email_failed::create(array(
6422              'context' => context_system::instance(),
6423              'userid' => $from->id,
6424              'relateduserid' => $user->id,
6425              'other' => array(
6426                  'subject' => $subject,
6427                  'message' => $messagetext,
6428                  'errorinfo' => $mail->ErrorInfo
6429              )
6430          ));
6431          $event->trigger();
6432          if (CLI_SCRIPT) {
6433              mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo);
6434          }
6435          if (!empty($mail->SMTPDebug)) {
6436              echo '</pre>';
6437          }
6438          return false;
6439      }
6440  }
6441  
6442  /**
6443   * Check to see if a user's real email address should be used for the "From" field.
6444   *
6445   * @param  object $from The user object for the user we are sending the email from.
6446   * @param  object $user The user object that we are sending the email to.
6447   * @param  array $unused No longer used.
6448   * @return bool Returns true if we can use the from user's email adress in the "From" field.
6449   */
6450  function can_send_from_real_email_address($from, $user, $unused = null) {
6451      global $CFG;
6452      if (!isset($CFG->allowedemaildomains) || empty(trim($CFG->allowedemaildomains))) {
6453          return false;
6454      }
6455      $alloweddomains = array_map('trim', explode("\n", $CFG->allowedemaildomains));
6456      // Email is in the list of allowed domains for sending email,
6457      // and the senders email setting is either displayed to everyone, or display to only other users that are enrolled
6458      // in a course with the sender.
6459      if (\core\ip_utils::is_domain_in_allowed_list(substr($from->email, strpos($from->email, '@') + 1), $alloweddomains)
6460                  && ($from->maildisplay == core_user::MAILDISPLAY_EVERYONE
6461                  || ($from->maildisplay == core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY
6462                  && enrol_get_shared_courses($user, $from, false, true)))) {
6463          return true;
6464      }
6465      return false;
6466  }
6467  
6468  /**
6469   * Generate a signoff for emails based on support settings
6470   *
6471   * @return string
6472   */
6473  function generate_email_signoff() {
6474      global $CFG;
6475  
6476      $signoff = "\n";
6477      if (!empty($CFG->supportname)) {
6478          $signoff .= $CFG->supportname."\n";
6479      }
6480      if (!empty($CFG->supportemail)) {
6481          $signoff .= $CFG->supportemail."\n";
6482      }
6483      if (!empty($CFG->supportpage)) {
6484          $signoff .= $CFG->supportpage."\n";
6485      }
6486      return $signoff;
6487  }
6488  
6489  /**
6490   * Sets specified user's password and send the new password to the user via email.
6491   *
6492   * @param stdClass $user A {@link $USER} object
6493   * @param bool $fasthash If true, use a low cost factor when generating the hash for speed.
6494   * @return bool|string Returns "true" if mail was sent OK and "false" if there was an error
6495   */
6496  function setnew_password_and_mail($user, $fasthash = false) {
6497      global $CFG, $DB;
6498  
6499      // We try to send the mail in language the user understands,
6500      // unfortunately the filter_string() does not support alternative langs yet
6501      // so multilang will not work properly for site->fullname.
6502      $lang = empty($user->lang) ? get_newuser_language() : $user->lang;
6503  
6504      $site  = get_site();
6505  
6506      $supportuser = core_user::get_support_user();
6507  
6508      $newpassword = generate_password();
6509  
6510      update_internal_user_password($user, $newpassword, $fasthash);
6511  
6512      $a = new stdClass();
6513      $a->firstname   = fullname($user, true);
6514      $a->sitename    = format_string($site->fullname);
6515      $a->username    = $user->username;
6516      $a->newpassword = $newpassword;
6517      $a->link        = $CFG->wwwroot .'/login/?lang='.$lang;
6518      $a->signoff     = generate_email_signoff();
6519  
6520      $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang);
6521  
6522      $subject = format_string($site->fullname) .': '. (string)new lang_string('newusernewpasswordsubj', '', $a, $lang);
6523  
6524      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6525      return email_to_user($user, $supportuser, $subject, $message);
6526  
6527  }
6528  
6529  /**
6530   * Resets specified user's password and send the new password to the user via email.
6531   *
6532   * @param stdClass $user A {@link $USER} object
6533   * @return bool Returns true if mail was sent OK and false if there was an error.
6534   */
6535  function reset_password_and_mail($user) {
6536      global $CFG;
6537  
6538      $site  = get_site();
6539      $supportuser = core_user::get_support_user();
6540  
6541      $userauth = get_auth_plugin($user->auth);
6542      if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) {
6543          trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth.");
6544          return false;
6545      }
6546  
6547      $newpassword = generate_password();
6548  
6549      if (!$userauth->user_update_password($user, $newpassword)) {
6550          print_error("cannotsetpassword");
6551      }
6552  
6553      $a = new stdClass();
6554      $a->firstname   = $user->firstname;
6555      $a->lastname    = $user->lastname;
6556      $a->sitename    = format_string($site->fullname);
6557      $a->username    = $user->username;
6558      $a->newpassword = $newpassword;
6559      $a->link        = $CFG->wwwroot .'/login/change_password.php';
6560      $a->signoff     = generate_email_signoff();
6561  
6562      $message = get_string('newpasswordtext', '', $a);
6563  
6564      $subject  = format_string($site->fullname) .': '. get_string('changedpassword');
6565  
6566      unset_user_preference('create_password', $user); // Prevent cron from generating the password.
6567  
6568      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6569      return email_to_user($user, $supportuser, $subject, $message);
6570  }
6571  
6572  /**
6573   * Send email to specified user with confirmation text and activation link.
6574   *
6575   * @param stdClass $user A {@link $USER} object
6576   * @param string $confirmationurl user confirmation URL
6577   * @return bool Returns true if mail was sent OK and false if there was an error.
6578   */
6579  function send_confirmation_email($user, $confirmationurl = null) {
6580      global $CFG;
6581  
6582      $site = get_site();
6583      $supportuser = core_user::get_support_user();
6584  
6585      $data = new stdClass();
6586      $data->sitename  = format_string($site->fullname);
6587      $data->admin     = generate_email_signoff();
6588  
6589      $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname));
6590  
6591      if (empty($confirmationurl)) {
6592          $confirmationurl = '/login/confirm.php';
6593      }
6594  
6595      $confirmationurl = new moodle_url($confirmationurl);
6596      // Remove data parameter just in case it was included in the confirmation so we can add it manually later.
6597      $confirmationurl->remove_params('data');
6598      $confirmationpath = $confirmationurl->out(false);
6599  
6600      // We need to custom encode the username to include trailing dots in the link.
6601      // Because of this custom encoding we can't use moodle_url directly.
6602      // Determine if a query string is present in the confirmation url.
6603      $hasquerystring = strpos($confirmationpath, '?') !== false;
6604      // Perform normal url encoding of the username first.
6605      $username = urlencode($user->username);
6606      // Prevent problems with trailing dots not being included as part of link in some mail clients.
6607      $username = str_replace('.', '%2E', $username);
6608  
6609      $data->link = $confirmationpath . ( $hasquerystring ? '&' : '?') . 'data='. $user->secret .'/'. $username;
6610  
6611      $message     = get_string('emailconfirmation', '', $data);
6612      $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true);
6613  
6614      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6615      return email_to_user($user, $supportuser, $subject, $message, $messagehtml);
6616  }
6617  
6618  /**
6619   * Sends a password change confirmation email.
6620   *
6621   * @param stdClass $user A {@link $USER} object
6622   * @param stdClass $resetrecord An object tracking metadata regarding password reset request
6623   * @return bool Returns true if mail was sent OK and false if there was an error.
6624   */
6625  function send_password_change_confirmation_email($user, $resetrecord) {
6626      global $CFG;
6627  
6628      $site = get_site();
6629      $supportuser = core_user::get_support_user();
6630      $pwresetmins = isset($CFG->pwresettime) ? floor($CFG->pwresettime / MINSECS) : 30;
6631  
6632      $data = new stdClass();
6633      $data->firstname = $user->firstname;
6634      $data->lastname  = $user->lastname;
6635      $data->username  = $user->username;
6636      $data->sitename  = format_string($site->fullname);
6637      $data->link      = $CFG->wwwroot .'/login/forgot_password.php?token='. $resetrecord->token;
6638      $data->admin     = generate_email_signoff();
6639      $data->resetminutes = $pwresetmins;
6640  
6641      $message = get_string('emailresetconfirmation', '', $data);
6642      $subject = get_string('emailresetconfirmationsubject', '', format_string($site->fullname));
6643  
6644      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6645      return email_to_user($user, $supportuser, $subject, $message);
6646  
6647  }
6648  
6649  /**
6650   * Sends an email containing information on how to change your password.
6651   *
6652   * @param stdClass $user A {@link $USER} object
6653   * @return bool Returns true if mail was sent OK and false if there was an error.
6654   */
6655  function send_password_change_info($user) {
6656      $site = get_site();
6657      $supportuser = core_user::get_support_user();
6658  
6659      $data = new stdClass();
6660      $data->firstname = $user->firstname;
6661      $data->lastname  = $user->lastname;
6662      $data->username  = $user->username;
6663      $data->sitename  = format_string($site->fullname);
6664      $data->admin     = generate_email_signoff();
6665  
6666      if (!is_enabled_auth($user->auth)) {
6667          $message = get_string('emailpasswordchangeinfodisabled', '', $data);
6668          $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
6669          // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6670          return email_to_user($user, $supportuser, $subject, $message);
6671      }
6672  
6673      $userauth = get_auth_plugin($user->auth);
6674      ['subject' => $subject, 'message' => $message] = $userauth->get_password_change_info($user);
6675  
6676      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6677      return email_to_user($user, $supportuser, $subject, $message);
6678  }
6679  
6680  /**
6681   * Check that an email is allowed.  It returns an error message if there was a problem.
6682   *
6683   * @param string $email Content of email
6684   * @return string|false
6685   */
6686  function email_is_not_allowed($email) {
6687      global $CFG;
6688  
6689      // Comparing lowercase domains.
6690      $email = strtolower($email);
6691      if (!empty($CFG->allowemailaddresses)) {
6692          $allowed = explode(' ', strtolower($CFG->allowemailaddresses));
6693          foreach ($allowed as $allowedpattern) {
6694              $allowedpattern = trim($allowedpattern);
6695              if (!$allowedpattern) {
6696                  continue;
6697              }
6698              if (strpos($allowedpattern, '.') === 0) {
6699                  if (strpos(strrev($email), strrev($allowedpattern)) === 0) {
6700                      // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6701                      return false;
6702                  }
6703  
6704              } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) {
6705                  return false;
6706              }
6707          }
6708          return get_string('emailonlyallowed', '', $CFG->allowemailaddresses);
6709  
6710      } else if (!empty($CFG->denyemailaddresses)) {
6711          $denied = explode(' ', strtolower($CFG->denyemailaddresses));
6712          foreach ($denied as $deniedpattern) {
6713              $deniedpattern = trim($deniedpattern);
6714              if (!$deniedpattern) {
6715                  continue;
6716              }
6717              if (strpos($deniedpattern, '.') === 0) {
6718                  if (strpos(strrev($email), strrev($deniedpattern)) === 0) {
6719                      // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6720                      return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6721                  }
6722  
6723              } else if (strpos(strrev($email), strrev('@'.$deniedpattern)) === 0) {
6724                  return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6725              }
6726          }
6727      }
6728  
6729      return false;
6730  }
6731  
6732  // FILE HANDLING.
6733  
6734  /**
6735   * Returns local file storage instance
6736   *
6737   * @return file_storage
6738   */
6739  function get_file_storage($reset = false) {
6740      global $CFG;
6741  
6742      static $fs = null;
6743  
6744      if ($reset) {
6745          $fs = null;
6746          return;
6747      }
6748  
6749      if ($fs) {
6750          return $fs;
6751      }
6752  
6753      require_once("$CFG->libdir/filelib.php");
6754  
6755      $fs = new file_storage();
6756  
6757      return $fs;
6758  }
6759  
6760  /**
6761   * Returns local file storage instance
6762   *
6763   * @return file_browser
6764   */
6765  function get_file_browser() {
6766      global $CFG;
6767  
6768      static $fb = null;
6769  
6770      if ($fb) {
6771          return $fb;
6772      }
6773  
6774      require_once("$CFG->libdir/filelib.php");
6775  
6776      $fb = new file_browser();
6777  
6778      return $fb;
6779  }
6780  
6781  /**
6782   * Returns file packer
6783   *
6784   * @param string $mimetype default application/zip
6785   * @return file_packer
6786   */
6787  function get_file_packer($mimetype='application/zip') {
6788      global $CFG;
6789  
6790      static $fp = array();
6791  
6792      if (isset($fp[$mimetype])) {
6793          return $fp[$mimetype];
6794      }
6795  
6796      switch ($mimetype) {
6797          case 'application/zip':
6798          case 'application/vnd.moodle.profiling':
6799              $classname = 'zip_packer';
6800              break;
6801  
6802          case 'application/x-gzip' :
6803              $classname = 'tgz_packer';
6804              break;
6805  
6806          case 'application/vnd.moodle.backup':
6807              $classname = 'mbz_packer';
6808              break;
6809  
6810          default:
6811              return false;
6812      }
6813  
6814      require_once("$CFG->libdir/filestorage/$classname.php");
6815      $fp[$mimetype] = new $classname();
6816  
6817      return $fp[$mimetype];
6818  }
6819  
6820  /**
6821   * Returns current name of file on disk if it exists.
6822   *
6823   * @param string $newfile File to be verified
6824   * @return string Current name of file on disk if true
6825   */
6826  function valid_uploaded_file($newfile) {
6827      if (empty($newfile)) {
6828          return '';
6829      }
6830      if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) {
6831          return $newfile['tmp_name'];
6832      } else {
6833          return '';
6834      }
6835  }
6836  
6837  /**
6838   * Returns the maximum size for uploading files.
6839   *
6840   * There are seven possible upload limits:
6841   * 1. in Apache using LimitRequestBody (no way of checking or changing this)
6842   * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
6843   * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
6844   * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
6845   * 5. by the Moodle admin in $CFG->maxbytes
6846   * 6. by the teacher in the current course $course->maxbytes
6847   * 7. by the teacher for the current module, eg $assignment->maxbytes
6848   *
6849   * These last two are passed to this function as arguments (in bytes).
6850   * Anything defined as 0 is ignored.
6851   * The smallest of all the non-zero numbers is returned.
6852   *
6853   * @todo Finish documenting this function
6854   *
6855   * @param int $sitebytes Set maximum size
6856   * @param int $coursebytes Current course $course->maxbytes (in bytes)
6857   * @param int $modulebytes Current module ->maxbytes (in bytes)
6858   * @param bool $unused This parameter has been deprecated and is not used any more.
6859   * @return int The maximum size for uploading files.
6860   */
6861  function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0, $unused = false) {
6862  
6863      if (! $filesize = ini_get('upload_max_filesize')) {
6864          $filesize = '5M';
6865      }
6866      $minimumsize = get_real_size($filesize);
6867  
6868      if ($postsize = ini_get('post_max_size')) {
6869          $postsize = get_real_size($postsize);
6870          if ($postsize < $minimumsize) {
6871              $minimumsize = $postsize;
6872          }
6873      }
6874  
6875      if (($sitebytes > 0) and ($sitebytes < $minimumsize)) {
6876          $minimumsize = $sitebytes;
6877      }
6878  
6879      if (($coursebytes > 0) and ($coursebytes < $minimumsize)) {
6880          $minimumsize = $coursebytes;
6881      }
6882  
6883      if (($modulebytes > 0) and ($modulebytes < $minimumsize)) {
6884          $minimumsize = $modulebytes;
6885      }
6886  
6887      return $minimumsize;
6888  }
6889  
6890  /**
6891   * Returns the maximum size for uploading files for the current user
6892   *
6893   * This function takes in account {@link get_max_upload_file_size()} the user's capabilities
6894   *
6895   * @param context $context The context in which to check user capabilities
6896   * @param int $sitebytes Set maximum size
6897   * @param int $coursebytes Current course $course->maxbytes (in bytes)
6898   * @param int $modulebytes Current module ->maxbytes (in bytes)
6899   * @param stdClass $user The user
6900   * @param bool $unused This parameter has been deprecated and is not used any more.
6901   * @return int The maximum size for uploading files.
6902   */
6903  function get_user_max_upload_file_size($context, $sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $user = null,
6904          $unused = false) {
6905      global $USER;
6906  
6907      if (empty($user)) {
6908          $user = $USER;
6909      }
6910  
6911      if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) {
6912          return USER_CAN_IGNORE_FILE_SIZE_LIMITS;
6913      }
6914  
6915      return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes);
6916  }
6917  
6918  /**
6919   * Returns an array of possible sizes in local language
6920   *
6921   * Related to {@link get_max_upload_file_size()} - this function returns an
6922   * array of possible sizes in an array, translated to the
6923   * local language.
6924   *
6925   * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes.
6926   *
6927   * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)"
6928   * with the value set to 0. This option will be the first in the list.
6929   *
6930   * @uses SORT_NUMERIC
6931   * @param int $sitebytes Set maximum size
6932   * @param int $coursebytes Current course $course->maxbytes (in bytes)
6933   * @param int $modulebytes Current module ->maxbytes (in bytes)
6934   * @param int|array $custombytes custom upload size/s which will be added to list,
6935   *        Only value/s smaller then maxsize will be added to list.
6936   * @return array
6937   */
6938  function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null) {
6939      global $CFG;
6940  
6941      if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) {
6942          return array();
6943      }
6944  
6945      if ($sitebytes == 0) {
6946          // Will get the minimum of upload_max_filesize or post_max_size.
6947          $sitebytes = get_max_upload_file_size();
6948      }
6949  
6950      $filesize = array();
6951      $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152,
6952                        5242880, 10485760, 20971520, 52428800, 104857600,
6953                        262144000, 524288000, 786432000, 1073741824,
6954                        2147483648, 4294967296, 8589934592);
6955  
6956      // If custombytes is given and is valid then add it to the list.
6957      if (is_number($custombytes) and $custombytes > 0) {
6958          $custombytes = (int)$custombytes;
6959          if (!in_array($custombytes, $sizelist)) {
6960              $sizelist[] = $custombytes;
6961          }
6962      } else if (is_array($custombytes)) {
6963          $sizelist = array_unique(array_merge($sizelist, $custombytes));
6964      }
6965  
6966      // Allow maxbytes to be selected if it falls outside the above boundaries.
6967      if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) {
6968          // Note: get_real_size() is used in order to prevent problems with invalid values.
6969          $sizelist[] = get_real_size($CFG->maxbytes);
6970      }
6971  
6972      foreach ($sizelist as $sizebytes) {
6973          if ($sizebytes < $maxsize && $sizebytes > 0) {
6974              $filesize[(string)intval($sizebytes)] = display_size($sizebytes);
6975          }
6976      }
6977  
6978      $limitlevel = '';
6979      $displaysize = '';
6980      if ($modulebytes &&
6981          (($modulebytes < $coursebytes || $coursebytes == 0) &&
6982           ($modulebytes < $sitebytes || $sitebytes == 0))) {
6983          $limitlevel = get_string('activity', 'core');
6984          $displaysize = display_size($modulebytes);
6985          $filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list.
6986  
6987      } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) {
6988          $limitlevel = get_string('course', 'core');
6989          $displaysize = display_size($coursebytes);
6990          $filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list.
6991  
6992      } else if ($sitebytes) {
6993          $limitlevel = get_string('site', 'core');
6994          $displaysize = display_size($sitebytes);
6995          $filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list.
6996      }
6997  
6998      krsort($filesize, SORT_NUMERIC);
6999      if ($limitlevel) {
7000          $params = (object) array('contextname' => $limitlevel, 'displaysize' => $displaysize);
7001          $filesize  = array('0' => get_string('uploadlimitwithsize', 'core', $params)) + $filesize;
7002      }
7003  
7004      return $filesize;
7005  }
7006  
7007  /**
7008   * Returns an array with all the filenames in all subdirectories, relative to the given rootdir.
7009   *
7010   * If excludefiles is defined, then that file/directory is ignored
7011   * If getdirs is true, then (sub)directories are included in the output
7012   * If getfiles is true, then files are included in the output
7013   * (at least one of these must be true!)
7014   *
7015   * @todo Finish documenting this function. Add examples of $excludefile usage.
7016   *
7017   * @param string $rootdir A given root directory to start from
7018   * @param string|array $excludefiles If defined then the specified file/directory is ignored
7019   * @param bool $descend If true then subdirectories are recursed as well
7020   * @param bool $getdirs If true then (sub)directories are included in the output
7021   * @param bool $getfiles  If true then files are included in the output
7022   * @return array An array with all the filenames in all subdirectories, relative to the given rootdir
7023   */
7024  function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) {
7025  
7026      $dirs = array();
7027  
7028      if (!$getdirs and !$getfiles) {   // Nothing to show.
7029          return $dirs;
7030      }
7031  
7032      if (!is_dir($rootdir)) {          // Must be a directory.
7033          return $dirs;
7034      }
7035  
7036      if (!$dir = opendir($rootdir)) {  // Can't open it for some reason.
7037          return $dirs;
7038      }
7039  
7040      if (!is_array($excludefiles)) {
7041          $excludefiles = array($excludefiles);
7042      }
7043  
7044      while (false !== ($file = readdir($dir))) {
7045          $firstchar = substr($file, 0, 1);
7046          if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) {
7047              continue;
7048          }
7049          $fullfile = $rootdir .'/'. $file;
7050          if (filetype($fullfile) == 'dir') {
7051              if ($getdirs) {
7052                  $dirs[] = $file;
7053              }
7054              if ($descend) {
7055                  $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles);
7056                  foreach ($subdirs as $subdir) {
7057                      $dirs[] = $file .'/'. $subdir;
7058                  }
7059              }
7060          } else if ($getfiles) {
7061              $dirs[] = $file;
7062          }
7063      }
7064      closedir($dir);
7065  
7066      asort($dirs);
7067  
7068      return $dirs;
7069  }
7070  
7071  
7072  /**
7073   * Adds up all the files in a directory and works out the size.
7074   *
7075   * @param string $rootdir  The directory to start from
7076   * @param string $excludefile A file to exclude when summing directory size
7077   * @return int The summed size of all files and subfiles within the root directory
7078   */
7079  function get_directory_size($rootdir, $excludefile='') {
7080      global $CFG;
7081  
7082      // Do it this way if we can, it's much faster.
7083      if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) {
7084          $command = trim($CFG->pathtodu).' -sk '.escapeshellarg($rootdir);
7085          $output = null;
7086          $return = null;
7087          exec($command, $output, $return);
7088          if (is_array($output)) {
7089              // We told it to return k.
7090              return get_real_size(intval($output[0]).'k');
7091          }
7092      }
7093  
7094      if (!is_dir($rootdir)) {
7095          // Must be a directory.
7096          return 0;
7097      }
7098  
7099      if (!$dir = @opendir($rootdir)) {
7100          // Can't open it for some reason.
7101          return 0;
7102      }
7103  
7104      $size = 0;
7105  
7106      while (false !== ($file = readdir($dir))) {
7107          $firstchar = substr($file, 0, 1);
7108          if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) {
7109              continue;
7110          }
7111          $fullfile = $rootdir .'/'. $file;
7112          if (filetype($fullfile) == 'dir') {
7113              $size += get_directory_size($fullfile, $excludefile);
7114          } else {
7115              $size += filesize($fullfile);
7116          }
7117      }
7118      closedir($dir);
7119  
7120      return $size;
7121  }
7122  
7123  /**
7124   * Converts bytes into display form
7125   *
7126   * @static string $gb Localized string for size in gigabytes
7127   * @static string $mb Localized string for size in megabytes
7128   * @static string $kb Localized string for size in kilobytes
7129   * @static string $b Localized string for size in bytes
7130   * @param int $size  The size to convert to human readable form
7131   * @return string
7132   */
7133  function display_size($size) {
7134  
7135      static $units;
7136  
7137      if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) {
7138          return get_string('unlimited');
7139      }
7140  
7141      if (empty($units)) {
7142          $units[] = get_string('sizeb');
7143          $units[] = get_string('sizekb');
7144          $units[] = get_string('sizemb');
7145          $units[] = get_string('sizegb');
7146          $units[] = get_string('sizetb');
7147          $units[] = get_string('sizepb');
7148      }
7149  
7150      if ($size >= 1024 ** 5) {
7151          $size = round($size / 1024 ** 5 * 10) / 10 . $units[5];
7152      } else if ($size >= 1024 ** 4) {
7153          $size = round($size / 1024 ** 4 * 10) / 10 . $units[4];
7154      } else if ($size >= 1024 ** 3) {
7155          $size = round($size / 1024 ** 3 * 10) / 10 . $units[3];
7156      } else if ($size >= 1024 ** 2) {
7157          $size = round($size / 1024 ** 2 * 10) / 10 . $units[2];
7158      } else if ($size >= 1024 ** 1) {
7159          $size = round($size / 1024 ** 1 * 10) / 10 . $units[1];
7160      } else {
7161          $size = intval($size) .' '. $units[0]; // File sizes over 2GB can not work in 32bit PHP anyway.
7162      }
7163      return $size;
7164  }
7165  
7166  /**
7167   * Cleans a given filename by removing suspicious or troublesome characters
7168   *
7169   * @see clean_param()
7170   * @param string $string file name
7171   * @return string cleaned file name
7172   */
7173  function clean_filename($string) {
7174      return clean_param($string, PARAM_FILE);
7175  }
7176  
7177  // STRING TRANSLATION.
7178  
7179  /**
7180   * Returns the code for the current language
7181   *
7182   * @category string
7183   * @return string
7184   */
7185  function current_language() {
7186      global $CFG, $USER, $SESSION, $COURSE;
7187  
7188      if (!empty($SESSION->forcelang)) {
7189          // Allows overriding course-forced language (useful for admins to check
7190          // issues in courses whose language they don't understand).
7191          // Also used by some code to temporarily get language-related information in a
7192          // specific language (see force_current_language()).
7193          $return = $SESSION->forcelang;
7194  
7195      } else if (!empty($COURSE->id) and $COURSE->id != SITEID and !empty($COURSE->lang)) {
7196          // Course language can override all other settings for this page.
7197          $return = $COURSE->lang;
7198  
7199      } else if (!empty($SESSION->lang)) {
7200          // Session language can override other settings.
7201          $return = $SESSION->lang;
7202  
7203      } else if (!empty($USER->lang)) {
7204          $return = $USER->lang;
7205  
7206      } else if (isset($CFG->lang)) {
7207          $return = $CFG->lang;
7208  
7209      } else {
7210          $return = 'en';
7211      }
7212  
7213      // Just in case this slipped in from somewhere by accident.
7214      $return = str_replace('_utf8', '', $return);
7215  
7216      return $return;
7217  }
7218  
7219  /**
7220   * Returns parent language of current active language if defined
7221   *
7222   * @category string
7223   * @param string $lang null means current language
7224   * @return string
7225   */
7226  function get_parent_language($lang=null) {
7227  
7228      $parentlang = get_string_manager()->get_string('parentlanguage', 'langconfig', null, $lang);
7229  
7230      if ($parentlang === 'en') {
7231          $parentlang = '';
7232      }
7233  
7234      return $parentlang;
7235  }
7236  
7237  /**
7238   * Force the current language to get strings and dates localised in the given language.
7239   *
7240   * After calling this function, all strings will be provided in the given language
7241   * until this function is called again, or equivalent code is run.
7242   *
7243   * @param string $language
7244   * @return string previous $SESSION->forcelang value
7245   */
7246  function force_current_language($language) {
7247      global $SESSION;
7248      $sessionforcelang = isset($SESSION->forcelang) ? $SESSION->forcelang : '';
7249      if ($language !== $sessionforcelang) {
7250          // Seting forcelang to null or an empty string disables it's effect.
7251          if (empty($language) || get_string_manager()->translation_exists($language, false)) {
7252              $SESSION->forcelang = $language;
7253              moodle_setlocale();
7254          }
7255      }
7256      return $sessionforcelang;
7257  }
7258  
7259  /**
7260   * Returns current string_manager instance.
7261   *
7262   * The param $forcereload is needed for CLI installer only where the string_manager instance
7263   * must be replaced during the install.php script life time.
7264   *
7265   * @category string
7266   * @param bool $forcereload shall the singleton be released and new instance created instead?
7267   * @return core_string_manager
7268   */
7269  function get_string_manager($forcereload=false) {
7270      global $CFG;
7271  
7272      static $singleton = null;
7273  
7274      if ($forcereload) {
7275          $singleton = null;
7276      }
7277      if ($singleton === null) {
7278          if (empty($CFG->early_install_lang)) {
7279  
7280              $transaliases = array();
7281              if (empty($CFG->langlist)) {
7282                   $translist = array();
7283              } else {
7284                  $translist = explode(',', $CFG->langlist);
7285                  $translist = array_map('trim', $translist);
7286                  // Each language in the $CFG->langlist can has an "alias" that would substitute the default language name.
7287                  foreach ($translist as $i => $value) {
7288                      $parts = preg_split('/\s*\|\s*/', $value, 2);
7289                      if (count($parts) == 2) {
7290                          $transaliases[$parts[0]] = $parts[1];
7291                          $translist[$i] = $parts[0];
7292                      }
7293                  }
7294              }
7295  
7296              if (!empty($CFG->config_php_settings['customstringmanager'])) {
7297                  $classname = $CFG->config_php_settings['customstringmanager'];
7298  
7299                  if (class_exists($classname)) {
7300                      $implements = class_implements($classname);
7301  
7302                      if (isset($implements['core_string_manager'])) {
7303                          $singleton = new $classname($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
7304                          return $singleton;
7305  
7306                      } else {
7307                          debugging('Unable to instantiate custom string manager: class '.$classname.
7308                              ' does not implement the core_string_manager interface.');
7309                      }
7310  
7311                  } else {
7312                      debugging('Unable to instantiate custom string manager: class '.$classname.' can not be found.');
7313                  }
7314              }
7315  
7316              $singleton = new core_string_manager_standard($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
7317  
7318          } else {
7319              $singleton = new core_string_manager_install();
7320          }
7321      }
7322  
7323      return $singleton;
7324  }
7325  
7326  /**
7327   * Returns a localized string.
7328   *
7329   * Returns the translated string specified by $identifier as
7330   * for $module.  Uses the same format files as STphp.
7331   * $a is an object, string or number that can be used
7332   * within translation strings
7333   *
7334   * eg 'hello {$a->firstname} {$a->lastname}'
7335   * or 'hello {$a}'
7336   *
7337   * If you would like to directly echo the localized string use
7338   * the function {@link print_string()}
7339   *
7340   * Example usage of this function involves finding the string you would
7341   * like a local equivalent of and using its identifier and module information
7342   * to retrieve it.<br/>
7343   * If you open moodle/lang/en/moodle.php and look near line 278
7344   * you will find a string to prompt a user for their word for 'course'
7345   * <code>
7346   * $string['course'] = 'Course';
7347   * </code>
7348   * So if you want to display the string 'Course'
7349   * in any language that supports it on your site
7350   * you just need to use the identifier 'course'
7351   * <code>
7352   * $mystring = '<strong>'. get_string('course') .'</strong>';
7353   * or
7354   * </code>
7355   * If the string you want is in another file you'd take a slightly
7356   * different approach. Looking in moodle/lang/en/calendar.php you find
7357   * around line 75:
7358   * <code>
7359   * $string['typecourse'] = 'Course event';
7360   * </code>
7361   * If you want to display the string "Course event" in any language
7362   * supported you would use the identifier 'typecourse' and the module 'calendar'
7363   * (because it is in the file calendar.php):
7364   * <code>
7365   * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>';
7366   * </code>
7367   *
7368   * As a last resort, should the identifier fail to map to a string
7369   * the returned string will be [[ $identifier ]]
7370   *
7371   * In Moodle 2.3 there is a new argument to this function $lazyload.
7372   * Setting $lazyload to true causes get_string to return a lang_string object
7373   * rather than the string itself. The fetching of the string is then put off until
7374   * the string object is first used. The object can be used by calling it's out
7375   * method or by casting the object to a string, either directly e.g.
7376   *     (string)$stringobject
7377   * or indirectly by using the string within another string or echoing it out e.g.
7378   *     echo $stringobject
7379   *     return "<p>{$stringobject}</p>";
7380   * It is worth noting that using $lazyload and attempting to use the string as an
7381   * array key will cause a fatal error as objects cannot be used as array keys.
7382   * But you should never do that anyway!
7383   * For more information {@link lang_string}
7384   *
7385   * @category string
7386   * @param string $identifier The key identifier for the localized string
7387   * @param string $component The module where the key identifier is stored,
7388   *      usually expressed as the filename in the language pack without the
7389   *      .php on the end but can also be written as mod/forum or grade/export/xls.
7390   *      If none is specified then moodle.php is used.
7391   * @param string|object|array $a An object, string or number that can be used
7392   *      within translation strings
7393   * @param bool $lazyload If set to true a string object is returned instead of
7394   *      the string itself. The string then isn't calculated until it is first used.
7395   * @return string The localized string.
7396   * @throws coding_exception
7397   */
7398  function get_string($identifier, $component = '', $a = null, $lazyload = false) {
7399      global $CFG;
7400  
7401      // If the lazy load argument has been supplied return a lang_string object
7402      // instead.
7403      // We need to make sure it is true (and a bool) as you will see below there
7404      // used to be a forth argument at one point.
7405      if ($lazyload === true) {
7406          return new lang_string($identifier, $component, $a);
7407      }
7408  
7409      if ($CFG->debugdeveloper && clean_param($identifier, PARAM_STRINGID) === '') {
7410          throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.', DEBUG_DEVELOPER);
7411      }
7412  
7413      // There is now a forth argument again, this time it is a boolean however so
7414      // we can still check for the old extralocations parameter.
7415      if (!is_bool($lazyload) && !empty($lazyload)) {
7416          debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.');
7417      }
7418  
7419      if (strpos($component, '/') !== false) {
7420          debugging('The module name you passed to get_string is the deprecated format ' .
7421                  'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER);
7422          $componentpath = explode('/', $component);
7423  
7424          switch ($componentpath[0]) {
7425              case 'mod':
7426                  $component = $componentpath[1];
7427                  break;
7428              case 'blocks':
7429              case 'block':
7430                  $component = 'block_'.$componentpath[1];
7431                  break;
7432              case 'enrol':
7433                  $component = 'enrol_'.$componentpath[1];
7434                  break;
7435              case 'format':
7436                  $component = 'format_'.$componentpath[1];
7437                  break;
7438              case 'grade':
7439                  $component = 'grade'.$componentpath[1].'_'.$componentpath[2];
7440                  break;
7441          }
7442      }
7443  
7444      $result = get_string_manager()->get_string($identifier, $component, $a);
7445  
7446      // Debugging feature lets you display string identifier and component.
7447      if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
7448          $result .= ' {' . $identifier . '/' . $component . '}';
7449      }
7450      return $result;
7451  }
7452  
7453  /**
7454   * Converts an array of strings to their localized value.
7455   *
7456   * @param array $array An array of strings
7457   * @param string $component The language module that these strings can be found in.
7458   * @return stdClass translated strings.
7459   */
7460  function get_strings($array, $component = '') {
7461      $string = new stdClass;
7462      foreach ($array as $item) {
7463          $string->$item = get_string($item, $component);
7464      }
7465      return $string;
7466  }
7467  
7468  /**
7469   * Prints out a translated string.
7470   *
7471   * Prints out a translated string using the return value from the {@link get_string()} function.
7472   *
7473   * Example usage of this function when the string is in the moodle.php file:<br/>
7474   * <code>
7475   * echo '<strong>';
7476   * print_string('course');
7477   * echo '</strong>';
7478   * </code>
7479   *
7480   * Example usage of this function when the string is not in the moodle.php file:<br/>
7481   * <code>
7482   * echo '<h1>';
7483   * print_string('typecourse', 'calendar');
7484   * echo '</h1>';
7485   * </code>
7486   *
7487   * @category string
7488   * @param string $identifier The key identifier for the localized string
7489   * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used.
7490   * @param string|object|array $a An object, string or number that can be used within translation strings
7491   */
7492  function print_string($identifier, $component = '', $a = null) {
7493      echo get_string($identifier, $component, $a);
7494  }
7495  
7496  /**
7497   * Returns a list of charset codes
7498   *
7499   * Returns a list of charset codes. It's hardcoded, so they should be added manually
7500   * (checking that such charset is supported by the texlib library!)
7501   *
7502   * @return array And associative array with contents in the form of charset => charset
7503   */
7504  function get_list_of_charsets() {
7505  
7506      $charsets = array(
7507          'EUC-JP'     => 'EUC-JP',
7508          'ISO-2022-JP'=> 'ISO-2022-JP',
7509          'ISO-8859-1' => 'ISO-8859-1',
7510          'SHIFT-JIS'  => 'SHIFT-JIS',
7511          'GB2312'     => 'GB2312',
7512          'GB18030'    => 'GB18030', // GB18030 not supported by typo and mbstring.
7513          'UTF-8'      => 'UTF-8');
7514  
7515      asort($charsets);
7516  
7517      return $charsets;
7518  }
7519  
7520  /**
7521   * Returns a list of valid and compatible themes
7522   *
7523   * @return array
7524   */
7525  function get_list_of_themes() {
7526      global $CFG;
7527  
7528      $themes = array();
7529  
7530      if (!empty($CFG->themelist)) {       // Use admin's list of themes.
7531          $themelist = explode(',', $CFG->themelist);
7532      } else {
7533          $themelist = array_keys(core_component::get_plugin_list("theme"));
7534      }
7535  
7536      foreach ($themelist as $key => $themename) {
7537          $theme = theme_config::load($themename);
7538          $themes[$themename] = $theme;
7539      }
7540  
7541      core_collator::asort_objects_by_method($themes, 'get_theme_name');
7542  
7543      return $themes;
7544  }
7545  
7546  /**
7547   * Factory function for emoticon_manager
7548   *
7549   * @return emoticon_manager singleton
7550   */
7551  function get_emoticon_manager() {
7552      static $singleton = null;
7553  
7554      if (is_null($singleton)) {
7555          $singleton = new emoticon_manager();
7556      }
7557  
7558      return $singleton;
7559  }
7560  
7561  /**
7562   * Provides core support for plugins that have to deal with emoticons (like HTML editor or emoticon filter).
7563   *
7564   * Whenever this manager mentiones 'emoticon object', the following data
7565   * structure is expected: stdClass with properties text, imagename, imagecomponent,
7566   * altidentifier and altcomponent
7567   *
7568   * @see admin_setting_emoticons
7569   *
7570   * @copyright 2010 David Mudrak
7571   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
7572   */
7573  class emoticon_manager {
7574  
7575      /**
7576       * Returns the currently enabled emoticons
7577       *
7578       * @param boolean $selectable - If true, only return emoticons that should be selectable from a list.
7579       * @return array of emoticon objects
7580       */
7581      public function get_emoticons($selectable = false) {
7582          global $CFG;
7583          $notselectable = ['martin', 'egg'];
7584  
7585          if (empty($CFG->emoticons)) {
7586              return array();
7587          }
7588  
7589          $emoticons = $this->decode_stored_config($CFG->emoticons);
7590  
7591          if (!is_array($emoticons)) {
7592              // Something is wrong with the format of stored setting.
7593              debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL);
7594              return array();
7595          }
7596          if ($selectable) {
7597              foreach ($emoticons as $index => $emote) {
7598                  if (in_array($emote->altidentifier, $notselectable)) {
7599                      // Skip this one.
7600                      unset($emoticons[$index]);
7601                  }
7602              }
7603          }
7604  
7605          return $emoticons;
7606      }
7607  
7608      /**
7609       * Converts emoticon object into renderable pix_emoticon object
7610       *
7611       * @param stdClass $emoticon emoticon object
7612       * @param array $attributes explicit HTML attributes to set
7613       * @return pix_emoticon
7614       */
7615      public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array()) {
7616          $stringmanager = get_string_manager();
7617          if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) {
7618              $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent);
7619          } else {
7620              $alt = s($emoticon->text);
7621          }
7622          return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes);
7623      }
7624  
7625      /**
7626       * Encodes the array of emoticon objects into a string storable in config table
7627       *
7628       * @see self::decode_stored_config()
7629       * @param array $emoticons array of emtocion objects
7630       * @return string
7631       */
7632      public function encode_stored_config(array $emoticons) {
7633          return json_encode($emoticons);
7634      }
7635  
7636      /**
7637       * Decodes the string into an array of emoticon objects
7638       *
7639       * @see self::encode_stored_config()
7640       * @param string $encoded
7641       * @return string|null
7642       */
7643      public function decode_stored_config($encoded) {
7644          $decoded = json_decode($encoded);
7645          if (!is_array($decoded)) {
7646              return null;
7647          }
7648          return $decoded;
7649      }
7650  
7651      /**
7652       * Returns default set of emoticons supported by Moodle
7653       *
7654       * @return array of sdtClasses
7655       */
7656      public function default_emoticons() {
7657          return array(
7658              $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'),
7659              $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'),
7660              $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'),
7661              $this->prepare_emoticon_object(";-)", 's/wink', 'wink'),
7662              $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'),
7663              $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'),
7664              $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'),
7665              $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'),
7666              $this->prepare_emoticon_object("B-)", 's/cool', 'cool'),
7667              $this->prepare_emoticon_object("^-)", 's/approve', 'approve'),
7668              $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'),
7669              $this->prepare_emoticon_object(":o)", 's/clown', 'clown'),
7670              $this->prepare_emoticon_object(":-(", 's/sad', 'sad'),
7671              $this->prepare_emoticon_object(":(", 's/sad', 'sad'),
7672              $this->prepare_emoticon_object("8-.", 's/shy', 'shy'),
7673              $this->prepare_emoticon_object(":-I", 's/blush', 'blush'),
7674              $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'),
7675              $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'),
7676              $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'),
7677              $this->prepare_emoticon_object("8-[", 's/angry', 'angry'),
7678              $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'),
7679              $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'),
7680              $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'),
7681              $this->prepare_emoticon_object("}-]", 's/evil', 'evil'),
7682              $this->prepare_emoticon_object("(h)", 's/heart', 'heart'),
7683              $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'),
7684              $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'),
7685              $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'),
7686              $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'),
7687              $this->prepare_emoticon_object("( )", 's/egg', 'egg'),
7688          );
7689      }
7690  
7691      /**
7692       * Helper method preparing the stdClass with the emoticon properties
7693       *
7694       * @param string|array $text or array of strings
7695       * @param string $imagename to be used by {@link pix_emoticon}
7696       * @param string $altidentifier alternative string identifier, null for no alt
7697       * @param string $altcomponent where the alternative string is defined
7698       * @param string $imagecomponent to be used by {@link pix_emoticon}
7699       * @return stdClass
7700       */
7701      protected function prepare_emoticon_object($text, $imagename, $altidentifier = null,
7702                                                 $altcomponent = 'core_pix', $imagecomponent = 'core') {
7703          return (object)array(
7704              'text'           => $text,
7705              'imagename'      => $imagename,
7706              'imagecomponent' => $imagecomponent,
7707              'altidentifier'  => $altidentifier,
7708              'altcomponent'   => $altcomponent,
7709          );
7710      }
7711  }
7712  
7713  // ENCRYPTION.
7714  
7715  /**
7716   * rc4encrypt
7717   *
7718   * @param string $data        Data to encrypt.
7719   * @return string             The now encrypted data.
7720   */
7721  function rc4encrypt($data) {
7722      return endecrypt(get_site_identifier(), $data, '');
7723  }
7724  
7725  /**
7726   * rc4decrypt
7727   *
7728   * @param string $data        Data to decrypt.
7729   * @return string             The now decrypted data.
7730   */
7731  function rc4decrypt($data) {
7732      return endecrypt(get_site_identifier(), $data, 'de');
7733  }
7734  
7735  /**
7736   * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com]
7737   *
7738   * @todo Finish documenting this function
7739   *
7740   * @param string $pwd The password to use when encrypting or decrypting
7741   * @param string $data The data to be decrypted/encrypted
7742   * @param string $case Either 'de' for decrypt or '' for encrypt
7743   * @return string
7744   */
7745  function endecrypt ($pwd, $data, $case) {
7746  
7747      if ($case == 'de') {
7748          $data = urldecode($data);
7749      }
7750  
7751      $key[] = '';
7752      $box[] = '';
7753      $pwdlength = strlen($pwd);
7754  
7755      for ($i = 0; $i <= 255; $i++) {
7756          $key[$i] = ord(substr($pwd, ($i % $pwdlength), 1));
7757          $box[$i] = $i;
7758      }
7759  
7760      $x = 0;
7761  
7762      for ($i = 0; $i <= 255; $i++) {
7763          $x = ($x + $box[$i] + $key[$i]) % 256;
7764          $tempswap = $box[$i];
7765          $box[$i] = $box[$x];
7766          $box[$x] = $tempswap;
7767      }
7768  
7769      $cipher = '';
7770  
7771      $a = 0;
7772      $j = 0;
7773  
7774      for ($i = 0; $i < strlen($data); $i++) {
7775          $a = ($a + 1) % 256;
7776          $j = ($j + $box[$a]) % 256;
7777          $temp = $box[$a];
7778          $box[$a] = $box[$j];
7779          $box[$j] = $temp;
7780          $k = $box[(($box[$a] + $box[$j]) % 256)];
7781          $cipherby = ord(substr($data, $i, 1)) ^ $k;
7782          $cipher .= chr($cipherby);
7783      }
7784  
7785      if ($case == 'de') {
7786          $cipher = urldecode(urlencode($cipher));
7787      } else {
7788          $cipher = urlencode($cipher);
7789      }
7790  
7791      return $cipher;
7792  }
7793  
7794  // ENVIRONMENT CHECKING.
7795  
7796  /**
7797   * This method validates a plug name. It is much faster than calling clean_param.
7798   *
7799   * @param string $name a string that might be a plugin name.
7800   * @return bool if this string is a valid plugin name.
7801   */
7802  function is_valid_plugin_name($name) {
7803      // This does not work for 'mod', bad luck, use any other type.
7804      return core_component::is_valid_plugin_name('tool', $name);
7805  }
7806  
7807  /**
7808   * Get a list of all the plugins of a given type that define a certain API function
7809   * in a certain file. The plugin component names and function names are returned.
7810   *
7811   * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
7812   * @param string $function the part of the name of the function after the
7813   *      frankenstyle prefix. e.g 'hook' if you are looking for functions with
7814   *      names like report_courselist_hook.
7815   * @param string $file the name of file within the plugin that defines the
7816   *      function. Defaults to lib.php.
7817   * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
7818   *      and the function names as values (e.g. 'report_courselist_hook', 'forum_hook').
7819   */
7820  function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php') {
7821      global $CFG;
7822  
7823      // We don't include here as all plugin types files would be included.
7824      $plugins = get_plugins_with_function($function, $file, false);
7825  
7826      if (empty($plugins[$plugintype])) {
7827          return array();
7828      }
7829  
7830      $allplugins = core_component::get_plugin_list($plugintype);
7831  
7832      // Reformat the array and include the files.
7833      $pluginfunctions = array();
7834      foreach ($plugins[$plugintype] as $pluginname => $functionname) {
7835  
7836          // Check that it has not been removed and the file is still available.
7837          if (!empty($allplugins[$pluginname])) {
7838  
7839              $filepath = $allplugins[$pluginname] . DIRECTORY_SEPARATOR . $file;
7840              if (file_exists($filepath)) {
7841                  include_once($filepath);
7842  
7843                  // Now that the file is loaded, we must verify the function still exists.
7844                  if (function_exists($functionname)) {
7845                      $pluginfunctions[$plugintype . '_' . $pluginname] = $functionname;
7846                  } else {
7847                      // Invalidate the cache for next run.
7848                      \cache_helper::invalidate_by_definition('core', 'plugin_functions');
7849                  }
7850              }
7851          }
7852      }
7853  
7854      return $pluginfunctions;
7855  }
7856  
7857  /**
7858   * Get a list of all the plugins that define a certain API function in a certain file.
7859   *
7860   * @param string $function the part of the name of the function after the
7861   *      frankenstyle prefix. e.g 'hook' if you are looking for functions with
7862   *      names like report_courselist_hook.
7863   * @param string $file the name of file within the plugin that defines the
7864   *      function. Defaults to lib.php.
7865   * @param bool $include Whether to include the files that contain the functions or not.
7866   * @return array with [plugintype][plugin] = functionname
7867   */
7868  function get_plugins_with_function($function, $file = 'lib.php', $include = true) {
7869      global $CFG;
7870  
7871      if (during_initial_install() || isset($CFG->upgraderunning)) {
7872          // API functions _must not_ be called during an installation or upgrade.
7873          return [];
7874      }
7875  
7876      $cache = \cache::make('core', 'plugin_functions');
7877  
7878      // Including both although I doubt that we will find two functions definitions with the same name.
7879      // Clearning the filename as cache_helper::hash_key only allows a-zA-Z0-9_.
7880      $key = $function . '_' . clean_param($file, PARAM_ALPHA);
7881      $pluginfunctions = $cache->get($key);
7882      $dirty = false;
7883  
7884      // Use the plugin manager to check that plugins are currently installed.
7885      $pluginmanager = \core_plugin_manager::instance();
7886  
7887      if ($pluginfunctions !== false) {
7888  
7889          // Checking that the files are still available.
7890          foreach ($pluginfunctions as $plugintype => $plugins) {
7891  
7892              $allplugins = \core_component::get_plugin_list($plugintype);
7893              $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7894              foreach ($plugins as $plugin => $function) {
7895                  if (!isset($installedplugins[$plugin])) {
7896                      // Plugin code is still present on disk but it is not installed.
7897                      $dirty = true;
7898                      break 2;
7899                  }
7900  
7901                  // Cache might be out of sync with the codebase, skip the plugin if it is not available.
7902                  if (empty($allplugins[$plugin])) {
7903                      $dirty = true;
7904                      break 2;
7905                  }
7906  
7907                  $fileexists = file_exists($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7908                  if ($include && $fileexists) {
7909                      // Include the files if it was requested.
7910                      include_once($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7911                  } else if (!$fileexists) {
7912                      // If the file is not available any more it should not be returned.
7913                      $dirty = true;
7914                      break 2;
7915                  }
7916  
7917                  // Check if the function still exists in the file.
7918                  if ($include && !function_exists($function)) {
7919                      $dirty = true;
7920                      break 2;
7921                  }
7922              }
7923          }
7924  
7925          // If the cache is dirty, we should fall through and let it rebuild.
7926          if (!$dirty) {
7927              return $pluginfunctions;
7928          }
7929      }
7930  
7931      $pluginfunctions = array();
7932  
7933      // To fill the cached. Also, everything should continue working with cache disabled.
7934      $plugintypes = \core_component::get_plugin_types();
7935      foreach ($plugintypes as $plugintype => $unused) {
7936  
7937          // We need to include files here.
7938          $pluginswithfile = \core_component::get_plugin_list_with_file($plugintype, $file, true);
7939          $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7940          foreach ($pluginswithfile as $plugin => $notused) {
7941  
7942              if (!isset($installedplugins[$plugin])) {
7943                  continue;
7944              }
7945  
7946              $fullfunction = $plugintype . '_' . $plugin . '_' . $function;
7947  
7948              $pluginfunction = false;
7949              if (function_exists($fullfunction)) {
7950                  // Function exists with standard name. Store, indexed by frankenstyle name of plugin.
7951                  $pluginfunction = $fullfunction;
7952  
7953              } else if ($plugintype === 'mod') {
7954                  // For modules, we also allow plugin without full frankenstyle but just starting with the module name.
7955                  $shortfunction = $plugin . '_' . $function;
7956                  if (function_exists($shortfunction)) {
7957                      $pluginfunction = $shortfunction;
7958                  }
7959              }
7960  
7961              if ($pluginfunction) {
7962                  if (empty($pluginfunctions[$plugintype])) {
7963                      $pluginfunctions[$plugintype] = array();
7964                  }
7965                  $pluginfunctions[$plugintype][$plugin] = $pluginfunction;
7966              }
7967  
7968          }
7969      }
7970      $cache->set($key, $pluginfunctions);
7971  
7972      return $pluginfunctions;
7973  
7974  }
7975  
7976  /**
7977   * Lists plugin-like directories within specified directory
7978   *
7979   * This function was originally used for standard Moodle plugins, please use
7980   * new core_component::get_plugin_list() now.
7981   *
7982   * This function is used for general directory listing and backwards compatility.
7983   *
7984   * @param string $directory relative directory from root
7985   * @param string $exclude dir name to exclude from the list (defaults to none)
7986   * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot)
7987   * @return array Sorted array of directory names found under the requested parameters
7988   */
7989  function get_list_of_plugins($directory='mod', $exclude='', $basedir='') {
7990      global $CFG;
7991  
7992      $plugins = array();
7993  
7994      if (empty($basedir)) {
7995          $basedir = $CFG->dirroot .'/'. $directory;
7996  
7997      } else {
7998          $basedir = $basedir .'/'. $directory;
7999      }
8000  
8001      if ($CFG->debugdeveloper and empty($exclude)) {
8002          // Make sure devs do not use this to list normal plugins,
8003          // this is intended for general directories that are not plugins!
8004  
8005          $subtypes = core_component::get_plugin_types();
8006          if (in_array($basedir, $subtypes)) {
8007              debugging('get_list_of_plugins() should not be used to list real plugins, use core_component::get_plugin_list() instead!', DEBUG_DEVELOPER);
8008          }
8009          unset($subtypes);
8010      }
8011  
8012      if (file_exists($basedir) && filetype($basedir) == 'dir') {
8013          if (!$dirhandle = opendir($basedir)) {
8014              debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER);
8015              return array();
8016          }
8017          while (false !== ($dir = readdir($dirhandle))) {
8018              // Func: strpos is marginally but reliably faster than substr($dir, 0, 1).
8019              if (strpos($dir, '.') === 0 or $dir === 'CVS' or $dir === '_vti_cnf' or $dir === 'simpletest' or $dir === 'yui' or
8020                  $dir === 'tests' or $dir === 'classes' or $dir === $exclude) {
8021                  continue;
8022              }
8023              if (filetype($basedir .'/'. $dir) != 'dir') {
8024                  continue;
8025              }
8026              $plugins[] = $dir;
8027          }
8028          closedir($dirhandle);
8029      }
8030      if ($plugins) {
8031          asort($plugins);
8032      }
8033      return $plugins;
8034  }
8035  
8036  /**
8037   * Invoke plugin's callback functions
8038   *
8039   * @param string $type plugin type e.g. 'mod'
8040   * @param string $name plugin name
8041   * @param string $feature feature name
8042   * @param string $action feature's action
8043   * @param array $params parameters of callback function, should be an array
8044   * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
8045   * @return mixed
8046   *
8047   * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743
8048   */
8049  function plugin_callback($type, $name, $feature, $action, $params = null, $default = null) {
8050      return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default);
8051  }
8052  
8053  /**
8054   * Invoke component's callback functions
8055   *
8056   * @param string $component frankenstyle component name, e.g. 'mod_quiz'
8057   * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
8058   * @param array $params parameters of callback function
8059   * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
8060   * @return mixed
8061   */
8062  function component_callback($component, $function, array $params = array(), $default = null) {
8063  
8064      $functionname = component_callback_exists($component, $function);
8065  
8066      if ($functionname) {
8067          // Function exists, so just return function result.
8068          $ret = call_user_func_array($functionname, $params);
8069          if (is_null($ret)) {
8070              return $default;
8071          } else {
8072              return $ret;
8073          }
8074      }
8075      return $default;
8076  }
8077  
8078  /**
8079   * Determine if a component callback exists and return the function name to call. Note that this
8080   * function will include the required library files so that the functioname returned can be
8081   * called directly.
8082   *
8083   * @param string $component frankenstyle component name, e.g. 'mod_quiz'
8084   * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
8085   * @return mixed Complete function name to call if the callback exists or false if it doesn't.
8086   * @throws coding_exception if invalid component specfied
8087   */
8088  function component_callback_exists($component, $function) {
8089      global $CFG; // This is needed for the inclusions.
8090  
8091      $cleancomponent = clean_param($component, PARAM_COMPONENT);
8092      if (empty($cleancomponent)) {
8093          throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
8094      }
8095      $component = $cleancomponent;
8096  
8097      list($type, $name) = core_component::normalize_component($component);
8098      $component = $type . '_' . $name;
8099  
8100      $oldfunction = $name.'_'.$function;
8101      $function = $component.'_'.$function;
8102  
8103      $dir = core_component::get_component_directory($component);
8104      if (empty($dir)) {
8105          throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
8106      }
8107  
8108      // Load library and look for function.
8109      if (file_exists($dir.'/lib.php')) {
8110          require_once($dir.'/lib.php');
8111      }
8112  
8113      if (!function_exists($function) and function_exists($oldfunction)) {
8114          if ($type !== 'mod' and $type !== 'core') {
8115              debugging("Please use new function name $function instead of legacy $oldfunction", DEBUG_DEVELOPER);
8116          }
8117          $function = $oldfunction;
8118      }
8119  
8120      if (function_exists($function)) {
8121          return $function;
8122      }
8123      return false;
8124  }
8125  
8126  /**
8127   * Call the specified callback method on the provided class.
8128   *
8129   * If the callback returns null, then the default value is returned instead.
8130   * If the class does not exist, then the default value is returned.
8131   *
8132   * @param   string      $classname The name of the class to call upon.
8133   * @param   string      $methodname The name of the staticically defined method on the class.
8134   * @param   array       $params The arguments to pass into the method.
8135   * @param   mixed       $default The default value.
8136   * @return  mixed       The return value.
8137   */
8138  function component_class_callback($classname, $methodname, array $params, $default = null) {
8139      if (!class_exists($classname)) {
8140          return $default;
8141      }
8142  
8143      if (!method_exists($classname, $methodname)) {
8144          return $default;
8145      }
8146  
8147      $fullfunction = $classname . '::' . $methodname;
8148      $result = call_user_func_array($fullfunction, $params);
8149  
8150      if (null === $result) {
8151          return $default;
8152      } else {
8153          return $result;
8154      }
8155  }
8156  
8157  /**
8158   * Checks whether a plugin supports a specified feature.
8159   *
8160   * @param string $type Plugin type e.g. 'mod'
8161   * @param string $name Plugin name e.g. 'forum'
8162   * @param string $feature Feature code (FEATURE_xx constant)
8163   * @param mixed $default default value if feature support unknown
8164   * @return mixed Feature result (false if not supported, null if feature is unknown,
8165   *         otherwise usually true but may have other feature-specific value such as array)
8166   * @throws coding_exception
8167   */
8168  function plugin_supports($type, $name, $feature, $default = null) {
8169      global $CFG;
8170  
8171      if ($type === 'mod' and $name === 'NEWMODULE') {
8172          // Somebody forgot to rename the module template.
8173          return false;
8174      }
8175  
8176      $component = clean_param($type . '_' . $name, PARAM_COMPONENT);
8177      if (empty($component)) {
8178          throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name);
8179      }
8180  
8181      $function = null;
8182  
8183      if ($type === 'mod') {
8184          // We need this special case because we support subplugins in modules,
8185          // otherwise it would end up in infinite loop.
8186          if (file_exists("$CFG->dirroot/mod/$name/lib.php")) {
8187              include_once("$CFG->dirroot/mod/$name/lib.php");
8188              $function = $component.'_supports';
8189              if (!function_exists($function)) {
8190                  // Legacy non-frankenstyle function name.
8191                  $function = $name.'_supports';
8192              }
8193          }
8194  
8195      } else {
8196          if (!$path = core_component::get_plugin_directory($type, $name)) {
8197              // Non existent plugin type.
8198              return false;
8199          }
8200          if (file_exists("$path/lib.php")) {
8201              include_once("$path/lib.php");
8202              $function = $component.'_supports';
8203          }
8204      }
8205  
8206      if ($function and function_exists($function)) {
8207          $supports = $function($feature);
8208          if (is_null($supports)) {
8209              // Plugin does not know - use default.
8210              return $default;
8211          } else {
8212              return $supports;
8213          }
8214      }
8215  
8216      // Plugin does not care, so use default.
8217      return $default;
8218  }
8219  
8220  /**
8221   * Returns true if the current version of PHP is greater that the specified one.
8222   *
8223   * @todo Check PHP version being required here is it too low?
8224   *
8225   * @param string $version The version of php being tested.
8226   * @return bool
8227   */
8228  function check_php_version($version='5.2.4') {
8229      return (version_compare(phpversion(), $version) >= 0);
8230  }
8231  
8232  /**
8233   * Determine if moodle installation requires update.
8234   *
8235   * Checks version numbers of main code and all plugins to see
8236   * if there are any mismatches.
8237   *
8238   * @return bool
8239   */
8240  function moodle_needs_upgrading() {
8241      global $CFG;
8242  
8243      if (empty($CFG->version)) {
8244          return true;
8245      }
8246  
8247      // There is no need to purge plugininfo caches here because
8248      // these caches are not used during upgrade and they are purged after
8249      // every upgrade.
8250  
8251      if (empty($CFG->allversionshash)) {
8252          return true;
8253      }
8254  
8255      $hash = core_component::get_all_versions_hash();
8256  
8257      return ($hash !== $CFG->allversionshash);
8258  }
8259  
8260  /**
8261   * Returns the major version of this site
8262   *
8263   * Moodle version numbers consist of three numbers separated by a dot, for
8264   * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so
8265   * called major version. This function extracts the major version from either
8266   * $CFG->release (default) or eventually from the $release variable defined in
8267   * the main version.php.
8268   *
8269   * @param bool $fromdisk should the version if source code files be used
8270   * @return string|false the major version like '2.3', false if could not be determined
8271   */
8272  function moodle_major_version($fromdisk = false) {
8273      global $CFG;
8274  
8275      if ($fromdisk) {
8276          $release = null;
8277          require($CFG->dirroot.'/version.php');
8278          if (empty($release)) {
8279              return false;
8280          }
8281  
8282      } else {
8283          if (empty($CFG->release)) {
8284              return false;
8285          }
8286          $release = $CFG->release;
8287      }
8288  
8289      if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) {
8290          return $matches[0];
8291      } else {
8292          return false;
8293      }
8294  }
8295  
8296  // MISCELLANEOUS.
8297  
8298  /**
8299   * Gets the system locale
8300   *
8301   * @return string Retuns the current locale.
8302   */
8303  function moodle_getlocale() {
8304      global $CFG;
8305  
8306      // Fetch the correct locale based on ostype.
8307      if ($CFG->ostype == 'WINDOWS') {
8308          $stringtofetch = 'localewin';
8309      } else {
8310          $stringtofetch = 'locale';
8311      }
8312  
8313      if (!empty($CFG->locale)) { // Override locale for all language packs.
8314          return $CFG->locale;
8315      }
8316  
8317      return get_string($stringtofetch, 'langconfig');
8318  }
8319  
8320  /**
8321   * Sets the system locale
8322   *
8323   * @category string
8324   * @param string $locale Can be used to force a locale
8325   */
8326  function moodle_setlocale($locale='') {
8327      global $CFG;
8328  
8329      static $currentlocale = ''; // Last locale caching.
8330  
8331      $oldlocale = $currentlocale;
8332  
8333      // The priority is the same as in get_string() - parameter, config, course, session, user, global language.
8334      if (!empty($locale)) {
8335          $currentlocale = $locale;
8336      } else {
8337          $currentlocale = moodle_getlocale();
8338      }
8339  
8340      // Do nothing if locale already set up.
8341      if ($oldlocale == $currentlocale) {
8342          return;
8343      }
8344  
8345      // Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values,
8346      // set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7
8347      // Some day, numeric, monetary and other categories should be set too, I think. :-/.
8348  
8349      // Get current values.
8350      $monetary= setlocale (LC_MONETARY, 0);
8351      $numeric = setlocale (LC_NUMERIC, 0);
8352      $ctype   = setlocale (LC_CTYPE, 0);
8353      if ($CFG->ostype != 'WINDOWS') {
8354          $messages= setlocale (LC_MESSAGES, 0);
8355      }
8356      // Set locale to all.
8357      $result = setlocale (LC_ALL, $currentlocale);
8358      // If setting of locale fails try the other utf8 or utf-8 variant,
8359      // some operating systems support both (Debian), others just one (OSX).
8360      if ($result === false) {
8361          if (stripos($currentlocale, '.UTF-8') !== false) {
8362              $newlocale = str_ireplace('.UTF-8', '.UTF8', $currentlocale);
8363              setlocale (LC_ALL, $newlocale);
8364          } else if (stripos($currentlocale, '.UTF8') !== false) {
8365              $newlocale = str_ireplace('.UTF8', '.UTF-8', $currentlocale);
8366              setlocale (LC_ALL, $newlocale);
8367          }
8368      }
8369      // Set old values.
8370      setlocale (LC_MONETARY, $monetary);
8371      setlocale (LC_NUMERIC, $numeric);
8372      if ($CFG->ostype != 'WINDOWS') {
8373          setlocale (LC_MESSAGES, $messages);
8374      }
8375      if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') {
8376          // To workaround a well-known PHP problem with Turkish letter Ii.
8377          setlocale (LC_CTYPE, $ctype);
8378      }
8379  }
8380  
8381  /**
8382   * Count words in a string.
8383   *
8384   * Words are defined as things between whitespace.
8385   *
8386   * @category string
8387   * @param string $string The text to be searched for words. May be HTML.
8388   * @return int The count of words in the specified string
8389   */
8390  function count_words($string) {
8391      // Before stripping tags, add a space after the close tag of anything that is not obviously inline.
8392      // Also, br is a special case because it definitely delimits a word, but has no close tag.
8393      $string = preg_replace('~
8394              (                                   # Capture the tag we match.
8395                  </                              # Start of close tag.
8396                  (?!                             # Do not match any of these specific close tag names.
8397                      a> | b> | del> | em> | i> |
8398                      ins> | s> | small> |
8399                      strong> | sub> | sup> | u>
8400                  )
8401                  \w+                             # But, apart from those execptions, match any tag name.
8402                  >                               # End of close tag.
8403              |
8404                  <br> | <br\s*/>                 # Special cases that are not close tags.
8405              )
8406              ~x', '$1 ', $string); // Add a space after the close tag.
8407      // Now remove HTML tags.
8408      $string = strip_tags($string);
8409      // Decode HTML entities.
8410      $string = html_entity_decode($string);
8411  
8412      // Now, the word count is the number of blocks of characters separated
8413      // by any sort of space. That seems to be the definition used by all other systems.
8414      // To be precise about what is considered to separate words:
8415      // * Anything that Unicode considers a 'Separator'
8416      // * Anything that Unicode considers a 'Control character'
8417      // * An em- or en- dash.
8418      return count(preg_split('~[\p{Z}\p{Cc}—–]+~u', $string, -1, PREG_SPLIT_NO_EMPTY));
8419  }
8420  
8421  /**
8422   * Count letters in a string.
8423   *
8424   * Letters are defined as chars not in tags and different from whitespace.
8425   *
8426   * @category string
8427   * @param string $string The text to be searched for letters. May be HTML.
8428   * @return int The count of letters in the specified text.
8429   */
8430  function count_letters($string) {
8431      $string = strip_tags($string); // Tags are out now.
8432      $string = html_entity_decode($string);
8433      $string = preg_replace('/[[:space:]]*/', '', $string); // Whitespace are out now.
8434  
8435      return core_text::strlen($string);
8436  }
8437  
8438  /**
8439   * Generate and return a random string of the specified length.
8440   *
8441   * @param int $length The length of the string to be created.
8442   * @return string
8443   */
8444  function random_string($length=15) {
8445      $randombytes = random_bytes_emulate($length);
8446      $pool  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
8447      $pool .= 'abcdefghijklmnopqrstuvwxyz';
8448      $pool .= '0123456789';
8449      $poollen = strlen($pool);
8450      $string = '';
8451      for ($i = 0; $i < $length; $i++) {
8452          $rand = ord($randombytes[$i]);
8453          $string .= substr($pool, ($rand%($poollen)), 1);
8454      }
8455      return $string;
8456  }
8457  
8458  /**
8459   * Generate a complex random string (useful for md5 salts)
8460   *
8461   * This function is based on the above {@link random_string()} however it uses a
8462   * larger pool of characters and generates a string between 24 and 32 characters
8463   *
8464   * @param int $length Optional if set generates a string to exactly this length
8465   * @return string
8466   */
8467  function complex_random_string($length=null) {
8468      $pool  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
8469      $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
8470      $poollen = strlen($pool);
8471      if ($length===null) {
8472          $length = floor(rand(24, 32));
8473      }
8474      $randombytes = random_bytes_emulate($length);
8475      $string = '';
8476      for ($i = 0; $i < $length; $i++) {
8477          $rand = ord($randombytes[$i]);
8478          $string .= $pool[($rand%$poollen)];
8479      }
8480      return $string;
8481  }
8482  
8483  /**
8484   * Try to generates cryptographically secure pseudo-random bytes.
8485   *
8486   * Note this is achieved by fallbacking between:
8487   *  - PHP 7 random_bytes().
8488   *  - OpenSSL openssl_random_pseudo_bytes().
8489   *  - In house random generator getting its entropy from various, hard to guess, pseudo-random sources.
8490   *
8491   * @param int $length requested length in bytes
8492   * @return string binary data
8493   */
8494  function random_bytes_emulate($length) {
8495      global $CFG;
8496      if ($length <= 0) {
8497          debugging('Invalid random bytes length', DEBUG_DEVELOPER);
8498          return '';
8499      }
8500      if (function_exists('random_bytes')) {
8501          // Use PHP 7 goodness.
8502          $hash = @random_bytes($length);
8503          if ($hash !== false) {
8504              return $hash;
8505          }
8506      }
8507      if (function_exists('openssl_random_pseudo_bytes')) {
8508          // If you have the openssl extension enabled.
8509          $hash = openssl_random_pseudo_bytes($length);
8510          if ($hash !== false) {
8511              return $hash;
8512          }
8513      }
8514  
8515      // Bad luck, there is no reliable random generator, let's just slowly hash some unique stuff that is hard to guess.
8516      $staticdata = serialize($CFG) . serialize($_SERVER);
8517      $hash = '';
8518      do {
8519          $hash .= sha1($staticdata . microtime(true) . uniqid('', true), true);
8520      } while (strlen($hash) < $length);
8521  
8522      return substr($hash, 0, $length);
8523  }
8524  
8525  /**
8526   * Given some text (which may contain HTML) and an ideal length,
8527   * this function truncates the text neatly on a word boundary if possible
8528   *
8529   * @category string
8530   * @param string $text text to be shortened
8531   * @param int $ideal ideal string length
8532   * @param boolean $exact if false, $text will not be cut mid-word
8533   * @param string $ending The string to append if the passed string is truncated
8534   * @return string $truncate shortened string
8535   */
8536  function shorten_text($text, $ideal=30, $exact = false, $ending='...') {
8537      // If the plain text is shorter than the maximum length, return the whole text.
8538      if (core_text::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) {
8539          return $text;
8540      }
8541  
8542      // Splits on HTML tags. Each open/close/empty tag will be the first thing
8543      // and only tag in its 'line'.
8544      preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
8545  
8546      $totallength = core_text::strlen($ending);
8547      $truncate = '';
8548  
8549      // This array stores information about open and close tags and their position
8550      // in the truncated string. Each item in the array is an object with fields
8551      // ->open (true if open), ->tag (tag name in lower case), and ->pos
8552      // (byte position in truncated text).
8553      $tagdetails = array();
8554  
8555      foreach ($lines as $linematchings) {
8556          // If there is any html-tag in this line, handle it and add it (uncounted) to the output.
8557          if (!empty($linematchings[1])) {
8558              // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>).
8559              if (!preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $linematchings[1])) {
8560                  if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $linematchings[1], $tagmatchings)) {
8561                      // Record closing tag.
8562                      $tagdetails[] = (object) array(
8563                              'open' => false,
8564                              'tag'  => core_text::strtolower($tagmatchings[1]),
8565                              'pos'  => core_text::strlen($truncate),
8566                          );
8567  
8568                  } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $linematchings[1], $tagmatchings)) {
8569                      // Record opening tag.
8570                      $tagdetails[] = (object) array(
8571                              'open' => true,
8572                              'tag'  => core_text::strtolower($tagmatchings[1]),
8573                              'pos'  => core_text::strlen($truncate),
8574                          );
8575                  } else if (preg_match('/^<!--\[if\s.*?\]>$/s', $linematchings[1], $tagmatchings)) {
8576                      $tagdetails[] = (object) array(
8577                              'open' => true,
8578                              'tag'  => core_text::strtolower('if'),
8579                              'pos'  => core_text::strlen($truncate),
8580                      );
8581                  } else if (preg_match('/^<!--<!\[endif\]-->$/s', $linematchings[1], $tagmatchings)) {
8582                      $tagdetails[] = (object) array(
8583                              'open' => false,
8584                              'tag'  => core_text::strtolower('if'),
8585                              'pos'  => core_text::strlen($truncate),
8586                      );
8587                  }
8588              }
8589              // Add html-tag to $truncate'd text.
8590              $truncate .= $linematchings[1];
8591          }
8592  
8593          // Calculate the length of the plain text part of the line; handle entities as one character.
8594          $contentlength = core_text::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $linematchings[2]));
8595          if ($totallength + $contentlength > $ideal) {
8596              // The number of characters which are left.
8597              $left = $ideal - $totallength;
8598              $entitieslength = 0;
8599              // Search for html entities.
8600              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)) {
8601                  // Calculate the real length of all entities in the legal range.
8602                  foreach ($entities[0] as $entity) {
8603                      if ($entity[1]+1-$entitieslength <= $left) {
8604                          $left--;
8605                          $entitieslength += core_text::strlen($entity[0]);
8606                      } else {
8607                          // No more characters left.
8608                          break;
8609                      }
8610                  }
8611              }
8612              $breakpos = $left + $entitieslength;
8613  
8614              // If the words shouldn't be cut in the middle...
8615              if (!$exact) {
8616                  // Search the last occurence of a space.
8617                  for (; $breakpos > 0; $breakpos--) {
8618                      if ($char = core_text::substr($linematchings[2], $breakpos, 1)) {
8619                          if ($char === '.' or $char === ' ') {
8620                              $breakpos += 1;
8621                              break;
8622                          } else if (strlen($char) > 2) {
8623                              // Chinese/Japanese/Korean text can be truncated at any UTF-8 character boundary.
8624                              $breakpos += 1;
8625                              break;
8626                          }
8627                      }
8628                  }
8629              }
8630              if ($breakpos == 0) {
8631                  // This deals with the test_shorten_text_no_spaces case.
8632                  $breakpos = $left + $entitieslength;
8633              } else if ($breakpos > $left + $entitieslength) {
8634                  // This deals with the previous for loop breaking on the first char.
8635                  $breakpos = $left + $entitieslength;
8636              }
8637  
8638              $truncate .= core_text::substr($linematchings[2], 0, $breakpos);
8639              // Maximum length is reached, so get off the loop.
8640              break;
8641          } else {
8642              $truncate .= $linematchings[2];
8643              $totallength += $contentlength;
8644          }
8645  
8646          // If the maximum length is reached, get off the loop.
8647          if ($totallength >= $ideal) {
8648              break;
8649          }
8650      }
8651  
8652      // Add the defined ending to the text.
8653      $truncate .= $ending;
8654  
8655      // Now calculate the list of open html tags based on the truncate position.
8656      $opentags = array();
8657      foreach ($tagdetails as $taginfo) {
8658          if ($taginfo->open) {
8659              // Add tag to the beginning of $opentags list.
8660              array_unshift($opentags, $taginfo->tag);
8661          } else {
8662              // Can have multiple exact same open tags, close the last one.
8663              $pos = array_search($taginfo->tag, array_reverse($opentags, true));
8664              if ($pos !== false) {
8665                  unset($opentags[$pos]);
8666              }
8667          }
8668      }
8669  
8670      // Close all unclosed html-tags.
8671      foreach ($opentags as $tag) {
8672          if ($tag === 'if') {
8673              $truncate .= '<!--<![endif]-->';
8674          } else {
8675              $truncate .= '</' . $tag . '>';
8676          }
8677      }
8678  
8679      return $truncate;
8680  }
8681  
8682  /**
8683   * Shortens a given filename by removing characters positioned after the ideal string length.
8684   * When the filename is too long, the file cannot be created on the filesystem due to exceeding max byte size.
8685   * Limiting the filename to a certain size (considering multibyte characters) will prevent this.
8686   *
8687   * @param string $filename file name
8688   * @param int $length ideal string length
8689   * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8690   * @return string $shortened shortened file name
8691   */
8692  function shorten_filename($filename, $length = MAX_FILENAME_SIZE, $includehash = false) {
8693      $shortened = $filename;
8694      // Extract a part of the filename if it's char size exceeds the ideal string length.
8695      if (core_text::strlen($filename) > $length) {
8696          // Exclude extension if present in filename.
8697          $mimetypes = get_mimetypes_array();
8698          $extension = pathinfo($filename, PATHINFO_EXTENSION);
8699          if ($extension && !empty($mimetypes[$extension])) {
8700              $basename = pathinfo($filename, PATHINFO_FILENAME);
8701              $hash = empty($includehash) ? '' : ' - ' . substr(sha1($basename), 0, 10);
8702              $shortened = core_text::substr($basename, 0, $length - strlen($hash)) . $hash;
8703              $shortened .= '.' . $extension;
8704          } else {
8705              $hash = empty($includehash) ? '' : ' - ' . substr(sha1($filename), 0, 10);
8706              $shortened = core_text::substr($filename, 0, $length - strlen($hash)) . $hash;
8707          }
8708      }
8709      return $shortened;
8710  }
8711  
8712  /**
8713   * Shortens a given array of filenames by removing characters positioned after the ideal string length.
8714   *
8715   * @param array $path The paths to reduce the length.
8716   * @param int $length Ideal string length
8717   * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8718   * @return array $result Shortened paths in array.
8719   */
8720  function shorten_filenames(array $path, $length = MAX_FILENAME_SIZE, $includehash = false) {
8721      $result = null;
8722  
8723      $result = array_reduce($path, function($carry, $singlepath) use ($length, $includehash) {
8724          $carry[] = shorten_filename($singlepath, $length, $includehash);
8725          return $carry;
8726      }, []);
8727  
8728      return $result;
8729  }
8730  
8731  /**
8732   * Given dates in seconds, how many weeks is the date from startdate
8733   * The first week is 1, the second 2 etc ...
8734   *
8735   * @param int $startdate Timestamp for the start date
8736   * @param int $thedate Timestamp for the end date
8737   * @return string
8738   */
8739  function getweek ($startdate, $thedate) {
8740      if ($thedate < $startdate) {
8741          return 0;
8742      }
8743  
8744      return floor(($thedate - $startdate) / WEEKSECS) + 1;
8745  }
8746  
8747  /**
8748   * Returns a randomly generated password of length $maxlen.  inspired by
8749   *
8750   * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and
8751   * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254}
8752   *
8753   * @param int $maxlen  The maximum size of the password being generated.
8754   * @return string
8755   */
8756  function generate_password($maxlen=10) {
8757      global $CFG;
8758  
8759      if (empty($CFG->passwordpolicy)) {
8760          $fillers = PASSWORD_DIGITS;
8761          $wordlist = file($CFG->wordlist);
8762          $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8763          $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8764          $filler1 = $fillers[rand(0, strlen($fillers) - 1)];
8765          $password = $word1 . $filler1 . $word2;
8766      } else {
8767          $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0;
8768          $digits = $CFG->minpassworddigits;
8769          $lower = $CFG->minpasswordlower;
8770          $upper = $CFG->minpasswordupper;
8771          $nonalphanum = $CFG->minpasswordnonalphanum;
8772          $total = $lower + $upper + $digits + $nonalphanum;
8773          // Var minlength should be the greater one of the two ( $minlen and $total ).
8774          $minlen = $minlen < $total ? $total : $minlen;
8775          // Var maxlen can never be smaller than minlen.
8776          $maxlen = $minlen > $maxlen ? $minlen : $maxlen;
8777          $additional = $maxlen - $total;
8778  
8779          // Make sure we have enough characters to fulfill
8780          // complexity requirements.
8781          $passworddigits = PASSWORD_DIGITS;
8782          while ($digits > strlen($passworddigits)) {
8783              $passworddigits .= PASSWORD_DIGITS;
8784          }
8785          $passwordlower = PASSWORD_LOWER;
8786          while ($lower > strlen($passwordlower)) {
8787              $passwordlower .= PASSWORD_LOWER;
8788          }
8789          $passwordupper = PASSWORD_UPPER;
8790          while ($upper > strlen($passwordupper)) {
8791              $passwordupper .= PASSWORD_UPPER;
8792          }
8793          $passwordnonalphanum = PASSWORD_NONALPHANUM;
8794          while ($nonalphanum > strlen($passwordnonalphanum)) {
8795              $passwordnonalphanum .= PASSWORD_NONALPHANUM;
8796          }
8797  
8798          // Now mix and shuffle it all.
8799          $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) .
8800                                   substr(str_shuffle ($passwordupper), 0, $upper) .
8801                                   substr(str_shuffle ($passworddigits), 0, $digits) .
8802                                   substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) .
8803                                   substr(str_shuffle ($passwordlower .
8804                                                       $passwordupper .
8805                                                       $passworddigits .
8806                                                       $passwordnonalphanum), 0 , $additional));
8807      }
8808  
8809      return substr ($password, 0, $maxlen);
8810  }
8811  
8812  /**
8813   * Given a float, prints it nicely.
8814   * Localized floats must not be used in calculations!
8815   *
8816   * The stripzeros feature is intended for making numbers look nicer in small
8817   * areas where it is not necessary to indicate the degree of accuracy by showing
8818   * ending zeros. If you turn it on with $decimalpoints set to 3, for example,
8819   * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'.
8820   *
8821   * @param float $float The float to print
8822   * @param int $decimalpoints The number of decimal places to print. -1 is a special value for auto detect (full precision).
8823   * @param bool $localized use localized decimal separator
8824   * @param bool $stripzeros If true, removes final zeros after decimal point. It will be ignored and the trailing zeros after
8825   *                         the decimal point are always striped if $decimalpoints is -1.
8826   * @return string locale float
8827   */
8828  function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=false) {
8829      if (is_null($float)) {
8830          return '';
8831      }
8832      if ($localized) {
8833          $separator = get_string('decsep', 'langconfig');
8834      } else {
8835          $separator = '.';
8836      }
8837      if ($decimalpoints == -1) {
8838          // The following counts the number of decimals.
8839          // It is safe as both floatval() and round() functions have same behaviour when non-numeric values are provided.
8840          $floatval = floatval($float);
8841          for ($decimalpoints = 0; $floatval != round($float, $decimalpoints); $decimalpoints++);
8842      }
8843  
8844      $result = number_format($float, $decimalpoints, $separator, '');
8845      if ($stripzeros) {
8846          // Remove zeros and final dot if not needed.
8847          $result = preg_replace('~(' . preg_quote($separator, '~') . ')?0+$~', '', $result);
8848      }
8849      return $result;
8850  }
8851  
8852  /**
8853   * Converts locale specific floating point/comma number back to standard PHP float value
8854   * Do NOT try to do any math operations before this conversion on any user submitted floats!
8855   *
8856   * @param string $localefloat locale aware float representation
8857   * @param bool $strict If true, then check the input and return false if it is not a valid number.
8858   * @return mixed float|bool - false or the parsed float.
8859   */
8860  function unformat_float($localefloat, $strict = false) {
8861      $localefloat = trim($localefloat);
8862  
8863      if ($localefloat == '') {
8864          return null;
8865      }
8866  
8867      $localefloat = str_replace(' ', '', $localefloat); // No spaces - those might be used as thousand separators.
8868      $localefloat = str_replace(get_string('decsep', 'langconfig'), '.', $localefloat);
8869  
8870      if ($strict && !is_numeric($localefloat)) {
8871          return false;
8872      }
8873  
8874      return (float)$localefloat;
8875  }
8876  
8877  /**
8878   * Given a simple array, this shuffles it up just like shuffle()
8879   * Unlike PHP's shuffle() this function works on any machine.
8880   *
8881   * @param array $array The array to be rearranged
8882   * @return array
8883   */
8884  function swapshuffle($array) {
8885  
8886      $last = count($array) - 1;
8887      for ($i = 0; $i <= $last; $i++) {
8888          $from = rand(0, $last);
8889          $curr = $array[$i];
8890          $array[$i] = $array[$from];
8891          $array[$from] = $curr;
8892      }
8893      return $array;
8894  }
8895  
8896  /**
8897   * Like {@link swapshuffle()}, but works on associative arrays
8898   *
8899   * @param array $array The associative array to be rearranged
8900   * @return array
8901   */
8902  function swapshuffle_assoc($array) {
8903  
8904      $newarray = array();
8905      $newkeys = swapshuffle(array_keys($array));
8906  
8907      foreach ($newkeys as $newkey) {
8908          $newarray[$newkey] = $array[$newkey];
8909      }
8910      return $newarray;
8911  }
8912  
8913  /**
8914   * Given an arbitrary array, and a number of draws,
8915   * this function returns an array with that amount
8916   * of items.  The indexes are retained.
8917   *
8918   * @todo Finish documenting this function
8919   *
8920   * @param array $array
8921   * @param int $draws
8922   * @return array
8923   */
8924  function draw_rand_array($array, $draws) {
8925  
8926      $return = array();
8927  
8928      $last = count($array);
8929  
8930      if ($draws > $last) {
8931          $draws = $last;
8932      }
8933  
8934      while ($draws > 0) {
8935          $last--;
8936  
8937          $keys = array_keys($array);
8938          $rand = rand(0, $last);
8939  
8940          $return[$keys[$rand]] = $array[$keys[$rand]];
8941          unset($array[$keys[$rand]]);
8942  
8943          $draws--;
8944      }
8945  
8946      return $return;
8947  }
8948  
8949  /**
8950   * Calculate the difference between two microtimes
8951   *
8952   * @param string $a The first Microtime
8953   * @param string $b The second Microtime
8954   * @return string
8955   */
8956  function microtime_diff($a, $b) {
8957      list($adec, $asec) = explode(' ', $a);
8958      list($bdec, $bsec) = explode(' ', $b);
8959      return $bsec - $asec + $bdec - $adec;
8960  }
8961  
8962  /**
8963   * Given a list (eg a,b,c,d,e) this function returns
8964   * an array of 1->a, 2->b, 3->c etc
8965   *
8966   * @param string $list The string to explode into array bits
8967   * @param string $separator The separator used within the list string
8968   * @return array The now assembled array
8969   */
8970  function make_menu_from_list($list, $separator=',') {
8971  
8972      $array = array_reverse(explode($separator, $list), true);
8973      foreach ($array as $key => $item) {
8974          $outarray[$key+1] = trim($item);
8975      }
8976      return $outarray;
8977  }
8978  
8979  /**
8980   * Creates an array that represents all the current grades that
8981   * can be chosen using the given grading type.
8982   *
8983   * Negative numbers
8984   * are scales, zero is no grade, and positive numbers are maximum
8985   * grades.
8986   *
8987   * @todo Finish documenting this function or better deprecated this completely!
8988   *
8989   * @param int $gradingtype
8990   * @return array
8991   */
8992  function make_grades_menu($gradingtype) {
8993      global $DB;
8994  
8995      $grades = array();
8996      if ($gradingtype < 0) {
8997          if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) {
8998              return make_menu_from_list($scale->scale);
8999          }
9000      } else if ($gradingtype > 0) {
9001          for ($i=$gradingtype; $i>=0; $i--) {
9002              $grades[$i] = $i .' / '. $gradingtype;
9003          }
9004          return $grades;
9005      }
9006      return $grades;
9007  }
9008  
9009  /**
9010   * make_unique_id_code
9011   *
9012   * @todo Finish documenting this function
9013   *
9014   * @uses $_SERVER
9015   * @param string $extra Extra string to append to the end of the code
9016   * @return string
9017   */
9018  function make_unique_id_code($extra = '') {
9019  
9020      $hostname = 'unknownhost';
9021      if (!empty($_SERVER['HTTP_HOST'])) {
9022          $hostname = $_SERVER['HTTP_HOST'];
9023      } else if (!empty($_ENV['HTTP_HOST'])) {
9024          $hostname = $_ENV['HTTP_HOST'];
9025      } else if (!empty($_SERVER['SERVER_NAME'])) {
9026          $hostname = $_SERVER['SERVER_NAME'];
9027      } else if (!empty($_ENV['SERVER_NAME'])) {
9028          $hostname = $_ENV['SERVER_NAME'];
9029      }
9030  
9031      $date = gmdate("ymdHis");
9032  
9033      $random =  random_string(6);
9034  
9035      if ($extra) {
9036          return $hostname .'+'. $date .'+'. $random .'+'. $extra;
9037      } else {
9038          return $hostname .'+'. $date .'+'. $random;
9039      }
9040  }
9041  
9042  
9043  /**
9044   * Function to check the passed address is within the passed subnet
9045   *
9046   * The parameter is a comma separated string of subnet definitions.
9047   * Subnet strings can be in one of three formats:
9048   *   1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn          (number of bits in net mask)
9049   *   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)
9050   *   3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.                  (incomplete address, a bit non-technical ;-)
9051   * Code for type 1 modified from user posted comments by mediator at
9052   * {@link http://au.php.net/manual/en/function.ip2long.php}
9053   *
9054   * @param string $addr    The address you are checking
9055   * @param string $subnetstr    The string of subnet addresses
9056   * @return bool
9057   */
9058  function address_in_subnet($addr, $subnetstr) {
9059  
9060      if ($addr == '0.0.0.0') {
9061          return false;
9062      }
9063      $subnets = explode(',', $subnetstr);
9064      $found = false;
9065      $addr = trim($addr);
9066      $addr = cleanremoteaddr($addr, false); // Normalise.
9067      if ($addr === null) {
9068          return false;
9069      }
9070      $addrparts = explode(':', $addr);
9071  
9072      $ipv6 = strpos($addr, ':');
9073  
9074      foreach ($subnets as $subnet) {
9075          $subnet = trim($subnet);
9076          if ($subnet === '') {
9077              continue;
9078          }
9079  
9080          if (strpos($subnet, '/') !== false) {
9081              // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn.
9082              list($ip, $mask) = explode('/', $subnet);
9083              $mask = trim($mask);
9084              if (!is_number($mask)) {
9085                  continue; // Incorect mask number, eh?
9086              }
9087              $ip = cleanremoteaddr($ip, false); // Normalise.
9088              if ($ip === null) {
9089                  continue;
9090              }
9091              if (strpos($ip, ':') !== false) {
9092                  // IPv6.
9093                  if (!$ipv6) {
9094                      continue;
9095                  }
9096                  if ($mask > 128 or $mask < 0) {
9097                      continue; // Nonsense.
9098                  }
9099                  if ($mask == 0) {
9100                      return true; // Any address.
9101                  }
9102                  if ($mask == 128) {
9103                      if ($ip === $addr) {
9104                          return true;
9105                      }
9106                      continue;
9107                  }
9108                  $ipparts = explode(':', $ip);
9109                  $modulo  = $mask % 16;
9110                  $ipnet   = array_slice($ipparts, 0, ($mask-$modulo)/16);
9111                  $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16);
9112                  if (implode(':', $ipnet) === implode(':', $addrnet)) {
9113                      if ($modulo == 0) {
9114                          return true;
9115                      }
9116                      $pos     = ($mask-$modulo)/16;
9117                      $ipnet   = hexdec($ipparts[$pos]);
9118                      $addrnet = hexdec($addrparts[$pos]);
9119                      $mask    = 0xffff << (16 - $modulo);
9120                      if (($addrnet & $mask) == ($ipnet & $mask)) {
9121                          return true;
9122                      }
9123                  }
9124  
9125              } else {
9126                  // IPv4.
9127                  if ($ipv6) {
9128                      continue;
9129                  }
9130                  if ($mask > 32 or $mask < 0) {
9131                      continue; // Nonsense.
9132                  }
9133                  if ($mask == 0) {
9134                      return true;
9135                  }
9136                  if ($mask == 32) {
9137                      if ($ip === $addr) {
9138                          return true;
9139                      }
9140                      continue;
9141                  }
9142                  $mask = 0xffffffff << (32 - $mask);
9143                  if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
9144                      return true;
9145                  }
9146              }
9147  
9148          } else if (strpos($subnet, '-') !== false) {
9149              // 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.
9150              $parts = explode('-', $subnet);
9151              if (count($parts) != 2) {
9152                  continue;
9153              }
9154  
9155              if (strpos($subnet, ':') !== false) {
9156                  // IPv6.
9157                  if (!$ipv6) {
9158                      continue;
9159                  }
9160                  $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
9161                  if ($ipstart === null) {
9162                      continue;
9163                  }
9164                  $ipparts = explode(':', $ipstart);
9165                  $start = hexdec(array_pop($ipparts));
9166                  $ipparts[] = trim($parts[1]);
9167                  $ipend = cleanremoteaddr(implode(':', $ipparts), false); // Normalise.
9168                  if ($ipend === null) {
9169                      continue;
9170                  }
9171                  $ipparts[7] = '';
9172                  $ipnet = implode(':', $ipparts);
9173                  if (strpos($addr, $ipnet) !== 0) {
9174                      continue;
9175                  }
9176                  $ipparts = explode(':', $ipend);
9177                  $end = hexdec($ipparts[7]);
9178  
9179                  $addrend = hexdec($addrparts[7]);
9180  
9181                  if (($addrend >= $start) and ($addrend <= $end)) {
9182                      return true;
9183                  }
9184  
9185              } else {
9186                  // IPv4.
9187                  if ($ipv6) {
9188                      continue;
9189                  }
9190                  $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
9191                  if ($ipstart === null) {
9192                      continue;
9193                  }
9194                  $ipparts = explode('.', $ipstart);
9195                  $ipparts[3] = trim($parts[1]);
9196                  $ipend = cleanremoteaddr(implode('.', $ipparts), false); // Normalise.
9197                  if ($ipend === null) {
9198                      continue;
9199                  }
9200  
9201                  if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
9202                      return true;
9203                  }
9204              }
9205  
9206          } else {
9207              // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
9208              if (strpos($subnet, ':') !== false) {
9209                  // IPv6.
9210                  if (!$ipv6) {
9211                      continue;
9212                  }
9213                  $parts = explode(':', $subnet);
9214                  $count = count($parts);
9215                  if ($parts[$count-1] === '') {
9216                      unset($parts[$count-1]); // Trim trailing :'s.
9217                      $count--;
9218                      $subnet = implode('.', $parts);
9219                  }
9220                  $isip = cleanremoteaddr($subnet, false); // Normalise.
9221                  if ($isip !== null) {
9222                      if ($isip === $addr) {
9223                          return true;
9224                      }
9225                      continue;
9226                  } else if ($count > 8) {
9227                      continue;
9228                  }
9229                  $zeros = array_fill(0, 8-$count, '0');
9230                  $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16);
9231                  if (address_in_subnet($addr, $subnet)) {
9232                      return true;
9233                  }
9234  
9235              } else {
9236                  // IPv4.
9237                  if ($ipv6) {
9238                      continue;
9239                  }
9240                  $parts = explode('.', $subnet);
9241                  $count = count($parts);
9242                  if ($parts[$count-1] === '') {
9243                      unset($parts[$count-1]); // Trim trailing .
9244                      $count--;
9245                      $subnet = implode('.', $parts);
9246                  }
9247                  if ($count == 4) {
9248                      $subnet = cleanremoteaddr($subnet, false); // Normalise.
9249                      if ($subnet === $addr) {
9250                          return true;
9251                      }
9252                      continue;
9253                  } else if ($count > 4) {
9254                      continue;
9255                  }
9256                  $zeros = array_fill(0, 4-$count, '0');
9257                  $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8);
9258                  if (address_in_subnet($addr, $subnet)) {
9259                      return true;
9260                  }
9261              }
9262          }
9263      }
9264  
9265      return false;
9266  }
9267  
9268  /**
9269   * For outputting debugging info
9270   *
9271   * @param string $string The string to write
9272   * @param string $eol The end of line char(s) to use
9273   * @param string $sleep Period to make the application sleep
9274   *                      This ensures any messages have time to display before redirect
9275   */
9276  function mtrace($string, $eol="\n", $sleep=0) {
9277      global $CFG;
9278  
9279      if (isset($CFG->mtrace_wrapper) && function_exists($CFG->mtrace_wrapper)) {
9280          $fn = $CFG->mtrace_wrapper;
9281          $fn($string, $eol);
9282          return;
9283      } else if (defined('STDOUT') && !PHPUNIT_TEST && !defined('BEHAT_TEST')) {
9284          // We must explicitly call the add_line function here.
9285          // Uses of fwrite to STDOUT are not picked up by ob_start.
9286          if ($output = \core\task\logmanager::add_line("{$string}{$eol}")) {
9287              fwrite(STDOUT, $output);
9288          }
9289      } else {
9290          echo $string . $eol;
9291      }
9292  
9293      // Flush again.
9294      flush();
9295  
9296      // Delay to keep message on user's screen in case of subsequent redirect.
9297      if ($sleep) {
9298          sleep($sleep);
9299      }
9300  }
9301  
9302  /**
9303   * Replace 1 or more slashes or backslashes to 1 slash
9304   *
9305   * @param string $path The path to strip
9306   * @return string the path with double slashes removed
9307   */
9308  function cleardoubleslashes ($path) {
9309      return preg_replace('/(\/|\\\){1,}/', '/', $path);
9310  }
9311  
9312  /**
9313   * Is the current ip in a given list?
9314   *
9315   * @param string $list
9316   * @return bool
9317   */
9318  function remoteip_in_list($list) {
9319      $clientip = getremoteaddr(null);
9320  
9321      if (!$clientip) {
9322          // Ensure access on cli.
9323          return true;
9324      }
9325      return \core\ip_utils::is_ip_in_subnet_list($clientip, $list);
9326  }
9327  
9328  /**
9329   * Returns most reliable client address
9330   *
9331   * @param string $default If an address can't be determined, then return this
9332   * @return string The remote IP address
9333   */
9334  function getremoteaddr($default='0.0.0.0') {
9335      global $CFG;
9336  
9337      if (!isset($CFG->getremoteaddrconf)) {
9338          // This will happen, for example, before just after the upgrade, as the
9339          // user is redirected to the admin screen.
9340          $variablestoskip = GETREMOTEADDR_SKIP_DEFAULT;
9341      } else {
9342          $variablestoskip = $CFG->getremoteaddrconf;
9343      }
9344      if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) {
9345          if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
9346              $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']);
9347              return $address ? $address : $default;
9348          }
9349      }
9350      if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
9351          if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
9352              $forwardedaddresses = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']);
9353  
9354              $forwardedaddresses = array_filter($forwardedaddresses, function($ip) {
9355                  global $CFG;
9356                  return !\core\ip_utils::is_ip_in_subnet_list($ip, $CFG->reverseproxyignore ?? '', ',');
9357              });
9358  
9359              // Multiple proxies can append values to this header including an
9360              // untrusted original request header so we must only trust the last ip.
9361              $address = end($forwardedaddresses);
9362  
9363              if (substr_count($address, ":") > 1) {
9364                  // Remove port and brackets from IPv6.
9365                  if (preg_match("/\[(.*)\]:/", $address, $matches)) {
9366                      $address = $matches[1];
9367                  }
9368              } else {
9369                  // Remove port from IPv4.
9370                  if (substr_count($address, ":") == 1) {
9371                      $parts = explode(":", $address);
9372                      $address = $parts[0];
9373                  }
9374              }
9375  
9376              $address = cleanremoteaddr($address);
9377              return $address ? $address : $default;
9378          }
9379      }
9380      if (!empty($_SERVER['REMOTE_ADDR'])) {
9381          $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']);
9382          return $address ? $address : $default;
9383      } else {
9384          return $default;
9385      }
9386  }
9387  
9388  /**
9389   * Cleans an ip address. Internal addresses are now allowed.
9390   * (Originally local addresses were not allowed.)
9391   *
9392   * @param string $addr IPv4 or IPv6 address
9393   * @param bool $compress use IPv6 address compression
9394   * @return string normalised ip address string, null if error
9395   */
9396  function cleanremoteaddr($addr, $compress=false) {
9397      $addr = trim($addr);
9398  
9399      if (strpos($addr, ':') !== false) {
9400          // Can be only IPv6.
9401          $parts = explode(':', $addr);
9402          $count = count($parts);
9403  
9404          if (strpos($parts[$count-1], '.') !== false) {
9405              // Legacy ipv4 notation.
9406              $last = array_pop($parts);
9407              $ipv4 = cleanremoteaddr($last, true);
9408              if ($ipv4 === null) {
9409                  return null;
9410              }
9411              $bits = explode('.', $ipv4);
9412              $parts[] = dechex($bits[0]).dechex($bits[1]);
9413              $parts[] = dechex($bits[2]).dechex($bits[3]);
9414              $count = count($parts);
9415              $addr = implode(':', $parts);
9416          }
9417  
9418          if ($count < 3 or $count > 8) {
9419              return null; // Severly malformed.
9420          }
9421  
9422          if ($count != 8) {
9423              if (strpos($addr, '::') === false) {
9424                  return null; // Malformed.
9425              }
9426              // Uncompress.
9427              $insertat = array_search('', $parts, true);
9428              $missing = array_fill(0, 1 + 8 - $count, '0');
9429              array_splice($parts, $insertat, 1, $missing);
9430              foreach ($parts as $key => $part) {
9431                  if ($part === '') {
9432                      $parts[$key] = '0';
9433                  }
9434              }
9435          }
9436  
9437          $adr = implode(':', $parts);
9438          if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) {
9439              return null; // Incorrect format - sorry.
9440          }
9441  
9442          // Normalise 0s and case.
9443          $parts = array_map('hexdec', $parts);
9444          $parts = array_map('dechex', $parts);
9445  
9446          $result = implode(':', $parts);
9447  
9448          if (!$compress) {
9449              return $result;
9450          }
9451  
9452          if ($result === '0:0:0:0:0:0:0:0') {
9453              return '::'; // All addresses.
9454          }
9455  
9456          $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1);
9457          if ($compressed !== $result) {
9458              return $compressed;
9459          }
9460  
9461          $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
9462          if ($compressed !== $result) {
9463              return $compressed;
9464          }
9465  
9466          $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
9467          if ($compressed !== $result) {
9468              return $compressed;
9469          }
9470  
9471          return $result;
9472      }
9473  
9474      // First get all things that look like IPv4 addresses.
9475      $parts = array();
9476      if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
9477          return null;
9478      }
9479      unset($parts[0]);
9480  
9481      foreach ($parts as $key => $match) {
9482          if ($match > 255) {
9483              return null;
9484          }
9485          $parts[$key] = (int)$match; // Normalise 0s.
9486      }
9487  
9488      return implode('.', $parts);
9489  }
9490  
9491  
9492  /**
9493   * Is IP address a public address?
9494   *
9495   * @param string $ip The ip to check
9496   * @return bool true if the ip is public
9497   */
9498  function ip_is_public($ip) {
9499      return (bool) filter_var($ip, FILTER_VALIDATE_IP, (FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE));
9500  }
9501  
9502  /**
9503   * This function will make a complete copy of anything it's given,
9504   * regardless of whether it's an object or not.
9505   *
9506   * @param mixed $thing Something you want cloned
9507   * @return mixed What ever it is you passed it
9508   */
9509  function fullclone($thing) {
9510      return unserialize(serialize($thing));
9511  }
9512  
9513  /**
9514   * Used to make sure that $min <= $value <= $max
9515   *
9516   * Make sure that value is between min, and max
9517   *
9518   * @param int $min The minimum value
9519   * @param int $value The value to check
9520   * @param int $max The maximum value
9521   * @return int
9522   */
9523  function bounded_number($min, $value, $max) {
9524      if ($value < $min) {
9525          return $min;
9526      }
9527      if ($value > $max) {
9528          return $max;
9529      }
9530      return $value;
9531  }
9532  
9533  /**
9534   * Check if there is a nested array within the passed array
9535   *
9536   * @param array $array
9537   * @return bool true if there is a nested array false otherwise
9538   */
9539  function array_is_nested($array) {
9540      foreach ($array as $value) {
9541          if (is_array($value)) {
9542              return true;
9543          }
9544      }
9545      return false;
9546  }
9547  
9548  /**
9549   * get_performance_info() pairs up with init_performance_info()
9550   * loaded in setup.php. Returns an array with 'html' and 'txt'
9551   * values ready for use, and each of the individual stats provided
9552   * separately as well.
9553   *
9554   * @return array
9555   */
9556  function get_performance_info() {
9557      global $CFG, $PERF, $DB, $PAGE;
9558  
9559      $info = array();
9560      $info['txt']  = me() . ' '; // Holds log-friendly representation.
9561  
9562      $info['html'] = '';
9563      if (!empty($CFG->themedesignermode)) {
9564          // Attempt to avoid devs debugging peformance issues, when its caused by css building and so on.
9565          $info['html'] .= '<p><strong>Warning: Theme designer mode is enabled.</strong></p>';
9566      }
9567      $info['html'] .= '<ul class="list-unstyled row mx-md-0">';         // Holds userfriendly HTML representation.
9568  
9569      $info['realtime'] = microtime_diff($PERF->starttime, microtime());
9570  
9571      $info['html'] .= '<li class="timeused col-sm-4">'.$info['realtime'].' secs</li> ';
9572      $info['txt'] .= 'time: '.$info['realtime'].'s ';
9573  
9574      // GET/POST (or NULL if $_SERVER['REQUEST_METHOD'] is undefined) is useful for txt logged information.
9575      $info['txt'] .= 'method: ' . ($_SERVER['REQUEST_METHOD'] ?? "NULL") . ' ';
9576  
9577      if (function_exists('memory_get_usage')) {
9578          $info['memory_total'] = memory_get_usage();
9579          $info['memory_growth'] = memory_get_usage() - $PERF->startmemory;
9580          $info['html'] .= '<li class="memoryused col-sm-4">RAM: '.display_size($info['memory_total']).'</li> ';
9581          $info['txt']  .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '.
9582              $info['memory_growth'].'B ('.display_size($info['memory_growth']).') ';
9583      }
9584  
9585      if (function_exists('memory_get_peak_usage')) {
9586          $info['memory_peak'] = memory_get_peak_usage();
9587          $info['html'] .= '<li class="memoryused col-sm-4">RAM peak: '.display_size($info['memory_peak']).'</li> ';
9588          $info['txt']  .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') ';
9589      }
9590  
9591      $info['html'] .= '</ul><ul class="list-unstyled row mx-md-0">';
9592      $inc = get_included_files();
9593      $info['includecount'] = count($inc);
9594      $info['html'] .= '<li class="included col-sm-4">Included '.$info['includecount'].' files</li> ';
9595      $info['txt']  .= 'includecount: '.$info['includecount'].' ';
9596  
9597      if (!empty($CFG->early_install_lang) or empty($PAGE)) {
9598          // We can not track more performance before installation or before PAGE init, sorry.
9599          return $info;
9600      }
9601  
9602      $filtermanager = filter_manager::instance();
9603      if (method_exists($filtermanager, 'get_performance_summary')) {
9604          list($filterinfo, $nicenames) = $filtermanager->get_performance_summary();
9605          $info = array_merge($filterinfo, $info);
9606          foreach ($filterinfo as $key => $value) {
9607              $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9608              $info['txt'] .= "$key: $value ";
9609          }
9610      }
9611  
9612      $stringmanager = get_string_manager();
9613      if (method_exists($stringmanager, 'get_performance_summary')) {
9614          list($filterinfo, $nicenames) = $stringmanager->get_performance_summary();
9615          $info = array_merge($filterinfo, $info);
9616          foreach ($filterinfo as $key => $value) {
9617              $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9618              $info['txt'] .= "$key: $value ";
9619          }
9620      }
9621  
9622      if (!empty($PERF->logwrites)) {
9623          $info['logwrites'] = $PERF->logwrites;
9624          $info['html'] .= '<li class="logwrites col-sm-4">Log DB writes '.$info['logwrites'].'</li> ';
9625          $info['txt'] .= 'logwrites: '.$info['logwrites'].' ';
9626      }
9627  
9628      $info['dbqueries'] = $DB->perf_get_reads().'/'.($DB->perf_get_writes() - $PERF->logwrites);
9629      $info['html'] .= '<li class="dbqueries col-sm-4">DB reads/writes: '.$info['dbqueries'].'</li> ';
9630      $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' ';
9631  
9632      if ($DB->want_read_slave()) {
9633          $info['dbreads_slave'] = $DB->perf_get_reads_slave();
9634          $info['html'] .= '<li class="dbqueries col-sm-4">DB reads from slave: '.$info['dbreads_slave'].'</li> ';
9635          $info['txt'] .= 'db reads from slave: '.$info['dbreads_slave'].' ';
9636      }
9637  
9638      $info['dbtime'] = round($DB->perf_get_queries_time(), 5);
9639      $info['html'] .= '<li class="dbtime col-sm-4">DB queries time: '.$info['dbtime'].' secs</li> ';
9640      $info['txt'] .= 'db queries time: ' . $info['dbtime'] . 's ';
9641  
9642      if (function_exists('posix_times')) {
9643          $ptimes = posix_times();
9644          if (is_array($ptimes)) {
9645              foreach ($ptimes as $key => $val) {
9646                  $info[$key] = $ptimes[$key] -  $PERF->startposixtimes[$key];
9647              }
9648              $info['html'] .= "<li class=\"posixtimes col-sm-4\">ticks: $info[ticks] user: $info[utime]";
9649              $info['html'] .= "sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</li> ";
9650              $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] ";
9651          }
9652      }
9653  
9654      // Grab the load average for the last minute.
9655      // /proc will only work under some linux configurations
9656      // while uptime is there under MacOSX/Darwin and other unices.
9657      if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) {
9658          list($serverload) = explode(' ', $loadavg[0]);
9659          unset($loadavg);
9660      } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) {
9661          if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) {
9662              $serverload = $matches[1];
9663          } else {
9664              trigger_error('Could not parse uptime output!');
9665          }
9666      }
9667      if (!empty($serverload)) {
9668          $info['serverload'] = $serverload;
9669          $info['html'] .= '<li class="serverload col-sm-4">Load average: '.$info['serverload'].'</li> ';
9670          $info['txt'] .= "serverload: {$info['serverload']} ";
9671      }
9672  
9673      // Display size of session if session started.
9674      if ($si = \core\session\manager::get_performance_info()) {
9675          $info['sessionsize'] = $si['size'];
9676          $info['html'] .= "<li class=\"serverload col-sm-4\">" . $si['html'] . "</li>";
9677          $info['txt'] .= $si['txt'];
9678      }
9679  
9680      $info['html'] .= '</ul>';
9681      $html = '';
9682      if ($stats = cache_helper::get_stats()) {
9683  
9684          $table = new html_table();
9685          $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9686          $table->head = ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S'];
9687          $table->data = [];
9688          $table->align = ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right'];
9689  
9690          $text = 'Caches used (hits/misses/sets): ';
9691          $hits = 0;
9692          $misses = 0;
9693          $sets = 0;
9694          $maxstores = 0;
9695  
9696          // We want to align static caches into their own column.
9697          $hasstatic = false;
9698          foreach ($stats as $definition => $details) {
9699              $numstores = count($details['stores']);
9700              $first = key($details['stores']);
9701              if ($first !== cache_store::STATIC_ACCEL) {
9702                  $numstores++; // Add a blank space for the missing static store.
9703              }
9704              $maxstores = max($maxstores, $numstores);
9705          }
9706  
9707          $storec = 0;
9708  
9709          while ($storec++ < ($maxstores - 2)) {
9710              if ($storec == ($maxstores - 2)) {
9711                  $table->head[] = get_string('mappingfinal', 'cache');
9712              } else {
9713                  $table->head[] = "Store $storec";
9714              }
9715              $table->align[] = 'left';
9716              $table->align[] = 'right';
9717              $table->align[] = 'right';
9718              $table->align[] = 'right';
9719              $table->head[] = 'H';
9720              $table->head[] = 'M';
9721              $table->head[] = 'S';
9722          }
9723  
9724          ksort($stats);
9725  
9726          foreach ($stats as $definition => $details) {
9727              switch ($details['mode']) {
9728                  case cache_store::MODE_APPLICATION:
9729                      $modeclass = 'application';
9730                      $mode = ' <span title="application cache">App</span>';
9731                      break;
9732                  case cache_store::MODE_SESSION:
9733                      $modeclass = 'session';
9734                      $mode = ' <span title="session cache">Ses</span>';
9735                      break;
9736                  case cache_store::MODE_REQUEST:
9737                      $modeclass = 'request';
9738                      $mode = ' <span title="request cache">Req</span>';
9739                      break;
9740              }
9741              $row = [$mode, $definition];
9742  
9743              $text .= "$definition {";
9744  
9745              $storec = 0;
9746              foreach ($details['stores'] as $store => $data) {
9747  
9748                  if ($storec == 0 && $store !== cache_store::STATIC_ACCEL) {
9749                      $row[] = '';
9750                      $row[] = '';
9751                      $row[] = '';
9752                      $storec++;
9753                  }
9754  
9755                  $hits   += $data['hits'];
9756                  $misses += $data['misses'];
9757                  $sets   += $data['sets'];
9758                  if ($data['hits'] == 0 and $data['misses'] > 0) {
9759                      $cachestoreclass = 'nohits bg-danger';
9760                  } else if ($data['hits'] < $data['misses']) {
9761                      $cachestoreclass = 'lowhits bg-warning text-dark';
9762                  } else {
9763                      $cachestoreclass = 'hihits';
9764                  }
9765                  $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
9766                  $cell = new html_table_cell($store);
9767                  $cell->attributes = ['class' => $cachestoreclass];
9768                  $row[] = $cell;
9769                  $cell = new html_table_cell($data['hits']);
9770                  $cell->attributes = ['class' => $cachestoreclass];
9771                  $row[] = $cell;
9772                  $cell = new html_table_cell($data['misses']);
9773                  $cell->attributes = ['class' => $cachestoreclass];
9774                  $row[] = $cell;
9775  
9776                  if ($store !== cache_store::STATIC_ACCEL) {
9777                      // The static cache is never set.
9778                      $cell = new html_table_cell($data['sets']);
9779                      $cell->attributes = ['class' => $cachestoreclass];
9780                      $row[] = $cell;
9781                  }
9782                  $storec++;
9783              }
9784              while ($storec++ < $maxstores) {
9785                  $row[] = '';
9786                  $row[] = '';
9787                  $row[] = '';
9788                  $row[] = '';
9789              }
9790              $text .= '} ';
9791  
9792              $table->data[] = $row;
9793          }
9794  
9795          $html .= html_writer::table($table);
9796  
9797          // Now lets also show sub totals for each cache store.
9798          $storetotals = [];
9799          $storetotal = ['hits' => 0, 'misses' => 0, 'sets' => 0];
9800          foreach ($stats as $definition => $details) {
9801              foreach ($details['stores'] as $store => $data) {
9802                  if (!array_key_exists($store, $storetotals)) {
9803                      $storetotals[$store] = ['hits' => 0, 'misses' => 0, 'sets' => 0];
9804                  }
9805                  $storetotals[$store]['class']   = $data['class'];
9806                  $storetotals[$store]['hits']   += $data['hits'];
9807                  $storetotals[$store]['misses'] += $data['misses'];
9808                  $storetotals[$store]['sets']   += $data['sets'];
9809                  $storetotal['hits']   += $data['hits'];
9810                  $storetotal['misses'] += $data['misses'];
9811                  $storetotal['sets']   += $data['sets'];
9812              }
9813          }
9814  
9815          $table = new html_table();
9816          $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9817          $table->head = [get_string('storename', 'cache'), get_string('type_cachestore', 'plugin'), 'H', 'M', 'S'];
9818          $table->data = [];
9819          $table->align = ['left', 'left', 'right', 'right', 'right'];
9820  
9821          ksort($storetotals);
9822  
9823          foreach ($storetotals as $store => $data) {
9824              $row = [];
9825              if ($data['hits'] == 0 and $data['misses'] > 0) {
9826                  $cachestoreclass = 'nohits bg-danger';
9827              } else if ($data['hits'] < $data['misses']) {
9828                  $cachestoreclass = 'lowhits bg-warning text-dark';
9829              } else {
9830                  $cachestoreclass = 'hihits';
9831              }
9832              $cell = new html_table_cell($store);
9833              $cell->attributes = ['class' => $cachestoreclass];
9834              $row[] = $cell;
9835              $cell = new html_table_cell($data['class']);
9836              $cell->attributes = ['class' => $cachestoreclass];
9837              $row[] = $cell;
9838              $cell = new html_table_cell($data['hits']);
9839              $cell->attributes = ['class' => $cachestoreclass];
9840              $row[] = $cell;
9841              $cell = new html_table_cell($data['misses']);
9842              $cell->attributes = ['class' => $cachestoreclass];
9843              $row[] = $cell;
9844              $cell = new html_table_cell($data['sets']);
9845              $cell->attributes = ['class' => $cachestoreclass];
9846              $row[] = $cell;
9847              $table->data[] = $row;
9848          }
9849          $row = [
9850              get_string('total'),
9851              '',
9852              $storetotal['hits'],
9853              $storetotal['misses'],
9854              $storetotal['sets'],
9855          ];
9856          $table->data[] = $row;
9857  
9858          $html .= html_writer::table($table);
9859  
9860          $info['cachesused'] = "$hits / $misses / $sets";
9861          $info['html'] .= $html;
9862          $info['txt'] .= $text.'. ';
9863      } else {
9864          $info['cachesused'] = '0 / 0 / 0';
9865          $info['html'] .= '<div class="cachesused">Caches used (hits/misses/sets): 0/0/0</div>';
9866          $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 ';
9867      }
9868  
9869      $info['html'] = '<div class="performanceinfo siteinfo container-fluid px-md-0 overflow-auto mt-3">'.$info['html'].'</div>';
9870      return $info;
9871  }
9872  
9873  /**
9874   * Renames a file or directory to a unique name within the same directory.
9875   *
9876   * This function is designed to avoid any potential race conditions, and select an unused name.
9877   *
9878   * @param string $filepath Original filepath
9879   * @param string $prefix Prefix to use for the temporary name
9880   * @return string|bool New file path or false if failed
9881   * @since Moodle 3.10
9882   */
9883  function rename_to_unused_name(string $filepath, string $prefix = '_temp_') {
9884      $dir = dirname($filepath);
9885      $basename = $dir . '/' . $prefix;
9886      $limit = 0;
9887      while ($limit < 100) {
9888          // Select a new name based on a random number.
9889          $newfilepath = $basename . md5(mt_rand());
9890  
9891          // Attempt a rename to that new name.
9892          if (@rename($filepath, $newfilepath)) {
9893              return $newfilepath;
9894          }
9895  
9896          // The first time, do some sanity checks, maybe it is failing for a good reason and there
9897          // is no point trying 100 times if so.
9898          if ($limit === 0 && (!file_exists($filepath) || !is_writable($dir))) {
9899              return false;
9900          }
9901          $limit++;
9902      }
9903      return false;
9904  }
9905  
9906  /**
9907   * Delete directory or only its content
9908   *
9909   * @param string $dir directory path
9910   * @param bool $contentonly
9911   * @return bool success, true also if dir does not exist
9912   */
9913  function remove_dir($dir, $contentonly=false) {
9914      if (!is_dir($dir)) {
9915          // Nothing to do.
9916          return true;
9917      }
9918  
9919      if (!$contentonly) {
9920          // Start by renaming the directory; this will guarantee that other processes don't write to it
9921          // while it is in the process of being deleted.
9922          $tempdir = rename_to_unused_name($dir);
9923          if ($tempdir) {
9924              // If the rename was successful then delete the $tempdir instead.
9925              $dir = $tempdir;
9926          }
9927          // If the rename fails, we will continue through and attempt to delete the directory
9928          // without renaming it since that is likely to at least delete most of the files.
9929      }
9930  
9931      if (!$handle = opendir($dir)) {
9932          return false;
9933      }
9934      $result = true;
9935      while (false!==($item = readdir($handle))) {
9936          if ($item != '.' && $item != '..') {
9937              if (is_dir($dir.'/'.$item)) {
9938                  $result = remove_dir($dir.'/'.$item) && $result;
9939              } else {
9940                  $result = unlink($dir.'/'.$item) && $result;
9941              }
9942          }
9943      }
9944      closedir($handle);
9945      if ($contentonly) {
9946          clearstatcache(); // Make sure file stat cache is properly invalidated.
9947          return $result;
9948      }
9949      $result = rmdir($dir); // If anything left the result will be false, no need for && $result.
9950      clearstatcache(); // Make sure file stat cache is properly invalidated.
9951      return $result;
9952  }
9953  
9954  /**
9955   * Detect if an object or a class contains a given property
9956   * will take an actual object or the name of a class
9957   *
9958   * @param mix $obj Name of class or real object to test
9959   * @param string $property name of property to find
9960   * @return bool true if property exists
9961   */
9962  function object_property_exists( $obj, $property ) {
9963      if (is_string( $obj )) {
9964          $properties = get_class_vars( $obj );
9965      } else {
9966          $properties = get_object_vars( $obj );
9967      }
9968      return array_key_exists( $property, $properties );
9969  }
9970  
9971  /**
9972   * Converts an object into an associative array
9973   *
9974   * This function converts an object into an associative array by iterating
9975   * over its public properties. Because this function uses the foreach
9976   * construct, Iterators are respected. It works recursively on arrays of objects.
9977   * Arrays and simple values are returned as is.
9978   *
9979   * If class has magic properties, it can implement IteratorAggregate
9980   * and return all available properties in getIterator()
9981   *
9982   * @param mixed $var
9983   * @return array
9984   */
9985  function convert_to_array($var) {
9986      $result = array();
9987  
9988      // Loop over elements/properties.
9989      foreach ($var as $key => $value) {
9990          // Recursively convert objects.
9991          if (is_object($value) || is_array($value)) {
9992              $result[$key] = convert_to_array($value);
9993          } else {
9994              // Simple values are untouched.
9995              $result[$key] = $value;
9996          }
9997      }
9998      return $result;
9999  }
10000  
10001  /**
10002   * Detect a custom script replacement in the data directory that will
10003   * replace an existing moodle script
10004   *
10005   * @return string|bool full path name if a custom script exists, false if no custom script exists
10006   */
10007  function custom_script_path() {
10008      global $CFG, $SCRIPT;
10009  
10010      if ($SCRIPT === null) {
10011          // Probably some weird external script.
10012          return false;
10013      }
10014  
10015      $scriptpath = $CFG->customscripts . $SCRIPT;
10016  
10017      // Check the custom script exists.
10018      if (file_exists($scriptpath) and is_file($scriptpath)) {
10019          return $scriptpath;
10020      } else {
10021          return false;
10022      }
10023  }
10024  
10025  /**
10026   * Returns whether or not the user object is a remote MNET user. This function
10027   * is in moodlelib because it does not rely on loading any of the MNET code.
10028   *
10029   * @param object $user A valid user object
10030   * @return bool        True if the user is from a remote Moodle.
10031   */
10032  function is_mnet_remote_user($user) {
10033      global $CFG;
10034  
10035      if (!isset($CFG->mnet_localhost_id)) {
10036          include_once($CFG->dirroot . '/mnet/lib.php');
10037          $env = new mnet_environment();
10038          $env->init();
10039          unset($env);
10040      }
10041  
10042      return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id);
10043  }
10044  
10045  /**
10046   * This function will search for browser prefereed languages, setting Moodle
10047   * to use the best one available if $SESSION->lang is undefined
10048   */
10049  function setup_lang_from_browser() {
10050      global $CFG, $SESSION, $USER;
10051  
10052      if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) {
10053          // Lang is defined in session or user profile, nothing to do.
10054          return;
10055      }
10056  
10057      if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do.
10058          return;
10059      }
10060  
10061      // Extract and clean langs from headers.
10062      $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
10063      $rawlangs = str_replace('-', '_', $rawlangs);         // We are using underscores.
10064      $rawlangs = explode(',', $rawlangs);                  // Convert to array.
10065      $langs = array();
10066  
10067      $order = 1.0;
10068      foreach ($rawlangs as $lang) {
10069          if (strpos($lang, ';') === false) {
10070              $langs[(string)$order] = $lang;
10071              $order = $order-0.01;
10072          } else {
10073              $parts = explode(';', $lang);
10074              $pos = strpos($parts[1], '=');
10075              $langs[substr($parts[1], $pos+1)] = $parts[0];
10076          }
10077      }
10078      krsort($langs, SORT_NUMERIC);
10079  
10080      // Look for such langs under standard locations.
10081      foreach ($langs as $lang) {
10082          // Clean it properly for include.
10083          $lang = strtolower(clean_param($lang, PARAM_SAFEDIR));
10084          if (get_string_manager()->translation_exists($lang, false)) {
10085              // Lang exists, set it in session.
10086              $SESSION->lang = $lang;
10087              // We have finished. Go out.
10088              break;
10089          }
10090      }
10091      return;
10092  }
10093  
10094  /**
10095   * Check if $url matches anything in proxybypass list
10096   *
10097   * Any errors just result in the proxy being used (least bad)
10098   *
10099   * @param string $url url to check
10100   * @return boolean true if we should bypass the proxy
10101   */
10102  function is_proxybypass( $url ) {
10103      global $CFG;
10104  
10105      // Sanity check.
10106      if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) {
10107          return false;
10108      }
10109  
10110      // Get the host part out of the url.
10111      if (!$host = parse_url( $url, PHP_URL_HOST )) {
10112          return false;
10113      }
10114  
10115      // Get the possible bypass hosts into an array.
10116      $matches = explode( ',', $CFG->proxybypass );
10117  
10118      // Check for a match.
10119      // (IPs need to match the left hand side and hosts the right of the url,
10120      // but we can recklessly check both as there can't be a false +ve).
10121      foreach ($matches as $match) {
10122          $match = trim($match);
10123  
10124          // Try for IP match (Left side).
10125          $lhs = substr($host, 0, strlen($match));
10126          if (strcasecmp($match, $lhs)==0) {
10127              return true;
10128          }
10129  
10130          // Try for host match (Right side).
10131          $rhs = substr($host, -strlen($match));
10132          if (strcasecmp($match, $rhs)==0) {
10133              return true;
10134          }
10135      }
10136  
10137      // Nothing matched.
10138      return false;
10139  }
10140  
10141  /**
10142   * Check if the passed navigation is of the new style
10143   *
10144   * @param mixed $navigation
10145   * @return bool true for yes false for no
10146   */
10147  function is_newnav($navigation) {
10148      if (is_array($navigation) && !empty($navigation['newnav'])) {
10149          return true;
10150      } else {
10151          return false;
10152      }
10153  }
10154  
10155  /**
10156   * Checks whether the given variable name is defined as a variable within the given object.
10157   *
10158   * This will NOT work with stdClass objects, which have no class variables.
10159   *
10160   * @param string $var The variable name
10161   * @param object $object The object to check
10162   * @return boolean
10163   */
10164  function in_object_vars($var, $object) {
10165      $classvars = get_class_vars(get_class($object));
10166      $classvars = array_keys($classvars);
10167      return in_array($var, $classvars);
10168  }
10169  
10170  /**
10171   * Returns an array without repeated objects.
10172   * This function is similar to array_unique, but for arrays that have objects as values
10173   *
10174   * @param array $array
10175   * @param bool $keepkeyassoc
10176   * @return array
10177   */
10178  function object_array_unique($array, $keepkeyassoc = true) {
10179      $duplicatekeys = array();
10180      $tmp         = array();
10181  
10182      foreach ($array as $key => $val) {
10183          // Convert objects to arrays, in_array() does not support objects.
10184          if (is_object($val)) {
10185              $val = (array)$val;
10186          }
10187  
10188          if (!in_array($val, $tmp)) {
10189              $tmp[] = $val;
10190          } else {
10191              $duplicatekeys[] = $key;
10192          }
10193      }
10194  
10195      foreach ($duplicatekeys as $key) {
10196          unset($array[$key]);
10197      }
10198  
10199      return $keepkeyassoc ? $array : array_values($array);
10200  }
10201  
10202  /**
10203   * Is a userid the primary administrator?
10204   *
10205   * @param int $userid int id of user to check
10206   * @return boolean
10207   */
10208  function is_primary_admin($userid) {
10209      $primaryadmin =  get_admin();
10210  
10211      if ($userid == $primaryadmin->id) {
10212          return true;
10213      } else {
10214          return false;
10215      }
10216  }
10217  
10218  /**
10219   * Returns the site identifier
10220   *
10221   * @return string $CFG->siteidentifier, first making sure it is properly initialised.
10222   */
10223  function get_site_identifier() {
10224      global $CFG;
10225      // Check to see if it is missing. If so, initialise it.
10226      if (empty($CFG->siteidentifier)) {
10227          set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']);
10228      }
10229      // Return it.
10230      return $CFG->siteidentifier;
10231  }
10232  
10233  /**
10234   * Check whether the given password has no more than the specified
10235   * number of consecutive identical characters.
10236   *
10237   * @param string $password   password to be checked against the password policy
10238   * @param integer $maxchars  maximum number of consecutive identical characters
10239   * @return bool
10240   */
10241  function check_consecutive_identical_characters($password, $maxchars) {
10242  
10243      if ($maxchars < 1) {
10244          return true; // Zero 0 is to disable this check.
10245      }
10246      if (strlen($password) <= $maxchars) {
10247          return true; // Too short to fail this test.
10248      }
10249  
10250      $previouschar = '';
10251      $consecutivecount = 1;
10252      foreach (str_split($password) as $char) {
10253          if ($char != $previouschar) {
10254              $consecutivecount = 1;
10255          } else {
10256              $consecutivecount++;
10257              if ($consecutivecount > $maxchars) {
10258                  return false; // Check failed already.
10259              }
10260          }
10261  
10262          $previouschar = $char;
10263      }
10264  
10265      return true;
10266  }
10267  
10268  /**
10269   * Helper function to do partial function binding.
10270   * so we can use it for preg_replace_callback, for example
10271   * this works with php functions, user functions, static methods and class methods
10272   * it returns you a callback that you can pass on like so:
10273   *
10274   * $callback = partial('somefunction', $arg1, $arg2);
10275   *     or
10276   * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
10277   *     or even
10278   * $obj = new someclass();
10279   * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2);
10280   *
10281   * and then the arguments that are passed through at calltime are appended to the argument list.
10282   *
10283   * @param mixed $function a php callback
10284   * @param mixed $arg1,... $argv arguments to partially bind with
10285   * @return array Array callback
10286   */
10287  function partial() {
10288      if (!class_exists('partial')) {
10289          /**
10290           * Used to manage function binding.
10291           * @copyright  2009 Penny Leach
10292           * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10293           */
10294          class partial{
10295              /** @var array */
10296              public $values = array();
10297              /** @var string The function to call as a callback. */
10298              public $func;
10299              /**
10300               * Constructor
10301               * @param string $func
10302               * @param array $args
10303               */
10304              public function __construct($func, $args) {
10305                  $this->values = $args;
10306                  $this->func = $func;
10307              }
10308              /**
10309               * Calls the callback function.
10310               * @return mixed
10311               */
10312              public function method() {
10313                  $args = func_get_args();
10314                  return call_user_func_array($this->func, array_merge($this->values, $args));
10315              }
10316          }
10317      }
10318      $args = func_get_args();
10319      $func = array_shift($args);
10320      $p = new partial($func, $args);
10321      return array($p, 'method');
10322  }
10323  
10324  /**
10325   * helper function to load up and initialise the mnet environment
10326   * this must be called before you use mnet functions.
10327   *
10328   * @return mnet_environment the equivalent of old $MNET global
10329   */
10330  function get_mnet_environment() {
10331      global $CFG;
10332      require_once($CFG->dirroot . '/mnet/lib.php');
10333      static $instance = null;
10334      if (empty($instance)) {
10335          $instance = new mnet_environment();
10336          $instance->init();
10337      }
10338      return $instance;
10339  }
10340  
10341  /**
10342   * during xmlrpc server code execution, any code wishing to access
10343   * information about the remote peer must use this to get it.
10344   *
10345   * @return mnet_remote_client the equivalent of old $MNETREMOTE_CLIENT global
10346   */
10347  function get_mnet_remote_client() {
10348      if (!defined('MNET_SERVER')) {
10349          debugging(get_string('notinxmlrpcserver', 'mnet'));
10350          return false;
10351      }
10352      global $MNET_REMOTE_CLIENT;
10353      if (isset($MNET_REMOTE_CLIENT)) {
10354          return $MNET_REMOTE_CLIENT;
10355      }
10356      return false;
10357  }
10358  
10359  /**
10360   * during the xmlrpc server code execution, this will be called
10361   * to setup the object returned by {@link get_mnet_remote_client}
10362   *
10363   * @param mnet_remote_client $client the client to set up
10364   * @throws moodle_exception
10365   */
10366  function set_mnet_remote_client($client) {
10367      if (!defined('MNET_SERVER')) {
10368          throw new moodle_exception('notinxmlrpcserver', 'mnet');
10369      }
10370      global $MNET_REMOTE_CLIENT;
10371      $MNET_REMOTE_CLIENT = $client;
10372  }
10373  
10374  /**
10375   * return the jump url for a given remote user
10376   * this is used for rewriting forum post links in emails, etc
10377   *
10378   * @param stdclass $user the user to get the idp url for
10379   */
10380  function mnet_get_idp_jump_url($user) {
10381      global $CFG;
10382  
10383      static $mnetjumps = array();
10384      if (!array_key_exists($user->mnethostid, $mnetjumps)) {
10385          $idp = mnet_get_peer_host($user->mnethostid);
10386          $idpjumppath = mnet_get_app_jumppath($idp->applicationid);
10387          $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl=';
10388      }
10389      return $mnetjumps[$user->mnethostid];
10390  }
10391  
10392  /**
10393   * Gets the homepage to use for the current user
10394   *
10395   * @return int One of HOMEPAGE_*
10396   */
10397  function get_home_page() {
10398      global $CFG;
10399  
10400      if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) {
10401          if ($CFG->defaulthomepage == HOMEPAGE_MY) {
10402              return HOMEPAGE_MY;
10403          } else {
10404              return (int)get_user_preferences('user_home_page_preference', HOMEPAGE_MY);
10405          }
10406      }
10407      return HOMEPAGE_SITE;
10408  }
10409  
10410  /**
10411   * Gets the name of a course to be displayed when showing a list of courses.
10412   * By default this is just $course->fullname but user can configure it. The
10413   * result of this function should be passed through print_string.
10414   * @param stdClass|core_course_list_element $course Moodle course object
10415   * @return string Display name of course (either fullname or short + fullname)
10416   */
10417  function get_course_display_name_for_list($course) {
10418      global $CFG;
10419      if (!empty($CFG->courselistshortnames)) {
10420          if (!($course instanceof stdClass)) {
10421              $course = (object)convert_to_array($course);
10422          }
10423          return get_string('courseextendednamedisplay', '', $course);
10424      } else {
10425          return $course->fullname;
10426      }
10427  }
10428  
10429  /**
10430   * Safe analogue of unserialize() that can only parse arrays
10431   *
10432   * Arrays may contain only integers or strings as both keys and values. Nested arrays are allowed.
10433   * Note: If any string (key or value) has semicolon (;) as part of the string parsing will fail.
10434   * This is a simple method to substitute unnecessary unserialize() in code and not intended to cover all possible cases.
10435   *
10436   * @param string $expression
10437   * @return array|bool either parsed array or false if parsing was impossible.
10438   */
10439  function unserialize_array($expression) {
10440      $subs = [];
10441      // Find nested arrays, parse them and store in $subs , substitute with special string.
10442      while (preg_match('/([\^;\}])(a:\d+:\{[^\{\}]*\})/', $expression, $matches) && strlen($matches[2]) < strlen($expression)) {
10443          $key = '--SUB' . count($subs) . '--';
10444          $subs[$key] = unserialize_array($matches[2]);
10445          if ($subs[$key] === false) {
10446              return false;
10447          }
10448          $expression = str_replace($matches[2], $key . ';', $expression);
10449      }
10450  
10451      // Check the expression is an array.
10452      if (!preg_match('/^a:(\d+):\{([^\}]*)\}$/', $expression, $matches1)) {
10453          return false;
10454      }
10455      // Get the size and elements of an array (key;value;key;value;....).
10456      $parts = explode(';', $matches1[2]);
10457      $size = intval($matches1[1]);
10458      if (count($parts) < $size * 2 + 1) {
10459          return false;
10460      }
10461      // Analyze each part and make sure it is an integer or string or a substitute.
10462      $value = [];
10463      for ($i = 0; $i < $size * 2; $i++) {
10464          if (preg_match('/^i:(\d+)$/', $parts[$i], $matches2)) {
10465              $parts[$i] = (int)$matches2[1];
10466          } else if (preg_match('/^s:(\d+):"(.*)"$/', $parts[$i], $matches3) && strlen($matches3[2]) == (int)$matches3[1]) {
10467              $parts[$i] = $matches3[2];
10468          } else if (preg_match('/^--SUB\d+--$/', $parts[$i])) {
10469              $parts[$i] = $subs[$parts[$i]];
10470          } else {
10471              return false;
10472          }
10473      }
10474      // Combine keys and values.
10475      for ($i = 0; $i < $size * 2; $i += 2) {
10476          $value[$parts[$i]] = $parts[$i+1];
10477      }
10478      return $value;
10479  }
10480  
10481  /**
10482   * Safe method for unserializing given input that is expected to contain only a serialized instance of an stdClass object
10483   *
10484   * If any class type other than stdClass is included in the input string, it will not be instantiated and will be cast to an
10485   * stdClass object. The initial cast to array, then back to object is to ensure we are always returning the correct type,
10486   * otherwise we would return an instances of {@see __PHP_Incomplete_class} for malformed strings
10487   *
10488   * @param string $input
10489   * @return stdClass
10490   */
10491  function unserialize_object(string $input): stdClass {
10492      $instance = (array) unserialize($input, ['allowed_classes' => [stdClass::class]]);
10493      return (object) $instance;
10494  }
10495  
10496  /**
10497   * The lang_string class
10498   *
10499   * This special class is used to create an object representation of a string request.
10500   * It is special because processing doesn't occur until the object is first used.
10501   * The class was created especially to aid performance in areas where strings were
10502   * required to be generated but were not necessarily used.
10503   * As an example the admin tree when generated uses over 1500 strings, of which
10504   * normally only 1/3 are ever actually printed at any time.
10505   * The performance advantage is achieved by not actually processing strings that
10506   * arn't being used, as such reducing the processing required for the page.
10507   *
10508   * How to use the lang_string class?
10509   *     There are two methods of using the lang_string class, first through the
10510   *     forth argument of the get_string function, and secondly directly.
10511   *     The following are examples of both.
10512   * 1. Through get_string calls e.g.
10513   *     $string = get_string($identifier, $component, $a, true);
10514   *     $string = get_string('yes', 'moodle', null, true);
10515   * 2. Direct instantiation
10516   *     $string = new lang_string($identifier, $component, $a, $lang);
10517   *     $string = new lang_string('yes');
10518   *
10519   * How do I use a lang_string object?
10520   *     The lang_string object makes use of a magic __toString method so that you
10521   *     are able to use the object exactly as you would use a string in most cases.
10522   *     This means you are able to collect it into a variable and then directly
10523   *     echo it, or concatenate it into another string, or similar.
10524   *     The other thing you can do is manually get the string by calling the
10525   *     lang_strings out method e.g.
10526   *         $string = new lang_string('yes');
10527   *         $string->out();
10528   *     Also worth noting is that the out method can take one argument, $lang which
10529   *     allows the developer to change the language on the fly.
10530   *
10531   * When should I use a lang_string object?
10532   *     The lang_string object is designed to be used in any situation where a
10533   *     string may not be needed, but needs to be generated.
10534   *     The admin tree is a good example of where lang_string objects should be
10535   *     used.
10536   *     A more practical example would be any class that requries strings that may
10537   *     not be printed (after all classes get renderer by renderers and who knows
10538   *     what they will do ;))
10539   *
10540   * When should I not use a lang_string object?
10541   *     Don't use lang_strings when you are going to use a string immediately.
10542   *     There is no need as it will be processed immediately and there will be no
10543   *     advantage, and in fact perhaps a negative hit as a class has to be
10544   *     instantiated for a lang_string object, however get_string won't require
10545   *     that.
10546   *
10547   * Limitations:
10548   * 1. You cannot use a lang_string object as an array offset. Doing so will
10549   *     result in PHP throwing an error. (You can use it as an object property!)
10550   *
10551   * @package    core
10552   * @category   string
10553   * @copyright  2011 Sam Hemelryk
10554   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10555   */
10556  class lang_string {
10557  
10558      /** @var string The strings identifier */
10559      protected $identifier;
10560      /** @var string The strings component. Default '' */
10561      protected $component = '';
10562      /** @var array|stdClass Any arguments required for the string. Default null */
10563      protected $a = null;
10564      /** @var string The language to use when processing the string. Default null */
10565      protected $lang = null;
10566  
10567      /** @var string The processed string (once processed) */
10568      protected $string = null;
10569  
10570      /**
10571       * A special boolean. If set to true then the object has been woken up and
10572       * cannot be regenerated. If this is set then $this->string MUST be used.
10573       * @var bool
10574       */
10575      protected $forcedstring = false;
10576  
10577      /**
10578       * Constructs a lang_string object
10579       *
10580       * This function should do as little processing as possible to ensure the best
10581       * performance for strings that won't be used.
10582       *
10583       * @param string $identifier The strings identifier
10584       * @param string $component The strings component
10585       * @param stdClass|array $a Any arguments the string requires
10586       * @param string $lang The language to use when processing the string.
10587       * @throws coding_exception
10588       */
10589      public function __construct($identifier, $component = '', $a = null, $lang = null) {
10590          if (empty($component)) {
10591              $component = 'moodle';
10592          }
10593  
10594          $this->identifier = $identifier;
10595          $this->component = $component;
10596          $this->lang = $lang;
10597  
10598          // We MUST duplicate $a to ensure that it if it changes by reference those
10599          // changes are not carried across.
10600          // To do this we always ensure $a or its properties/values are strings
10601          // and that any properties/values that arn't convertable are forgotten.
10602          if ($a !== null) {
10603              if (is_scalar($a)) {
10604                  $this->a = $a;
10605              } else if ($a instanceof lang_string) {
10606                  $this->a = $a->out();
10607              } else if (is_object($a) or is_array($a)) {
10608                  $a = (array)$a;
10609                  $this->a = array();
10610                  foreach ($a as $key => $value) {
10611                      // Make sure conversion errors don't get displayed (results in '').
10612                      if (is_array($value)) {
10613                          $this->a[$key] = '';
10614                      } else if (is_object($value)) {
10615                          if (method_exists($value, '__toString')) {
10616                              $this->a[$key] = $value->__toString();
10617                          } else {
10618                              $this->a[$key] = '';
10619                          }
10620                      } else {
10621                          $this->a[$key] = (string)$value;
10622                      }
10623                  }
10624              }
10625          }
10626  
10627          if (debugging(false, DEBUG_DEVELOPER)) {
10628              if (clean_param($this->identifier, PARAM_STRINGID) == '') {
10629                  throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition');
10630              }
10631              if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') {
10632                  throw new coding_exception('Invalid string compontent. Please check your string definition');
10633              }
10634              if (!get_string_manager()->string_exists($this->identifier, $this->component)) {
10635                  debugging('String does not exist. Please check your string definition for '.$this->identifier.'/'.$this->component, DEBUG_DEVELOPER);
10636              }
10637          }
10638      }
10639  
10640      /**
10641       * Processes the string.
10642       *
10643       * This function actually processes the string, stores it in the string property
10644       * and then returns it.
10645       * You will notice that this function is VERY similar to the get_string method.
10646       * That is because it is pretty much doing the same thing.
10647       * However as this function is an upgrade it isn't as tolerant to backwards
10648       * compatibility.
10649       *
10650       * @return string
10651       * @throws coding_exception
10652       */
10653      protected function get_string() {
10654          global $CFG;
10655  
10656          // Check if we need to process the string.
10657          if ($this->string === null) {
10658              // Check the quality of the identifier.
10659              if ($CFG->debugdeveloper && clean_param($this->identifier, PARAM_STRINGID) === '') {
10660                  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);
10661              }
10662  
10663              // Process the string.
10664              $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang);
10665              // Debugging feature lets you display string identifier and component.
10666              if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
10667                  $this->string .= ' {' . $this->identifier . '/' . $this->component . '}';
10668              }
10669          }
10670          // Return the string.
10671          return $this->string;
10672      }
10673  
10674      /**
10675       * Returns the string
10676       *
10677       * @param string $lang The langauge to use when processing the string
10678       * @return string
10679       */
10680      public function out($lang = null) {
10681          if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) {
10682              if ($this->forcedstring) {
10683                  debugging('lang_string objects that have been used cannot be printed in another language. ('.$this->lang.' used)', DEBUG_DEVELOPER);
10684                  return $this->get_string();
10685              }
10686              $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang);
10687              return $translatedstring->out();
10688          }
10689          return $this->get_string();
10690      }
10691  
10692      /**
10693       * Magic __toString method for printing a string
10694       *
10695       * @return string
10696       */
10697      public function __toString() {
10698          return $this->get_string();
10699      }
10700  
10701      /**
10702       * Magic __set_state method used for var_export
10703       *
10704       * @return string
10705       */
10706      public function __set_state() {
10707          return $this->get_string();
10708      }
10709  
10710      /**
10711       * Prepares the lang_string for sleep and stores only the forcedstring and
10712       * string properties... the string cannot be regenerated so we need to ensure
10713       * it is generated for this.
10714       *
10715       * @return string
10716       */
10717      public function __sleep() {
10718          $this->get_string();
10719          $this->forcedstring = true;
10720          return array('forcedstring', 'string', 'lang');
10721      }
10722  
10723      /**
10724       * Returns the identifier.
10725       *
10726       * @return string
10727       */
10728      public function get_identifier() {
10729          return $this->identifier;
10730      }
10731  
10732      /**
10733       * Returns the component.
10734       *
10735       * @return string
10736       */
10737      public function get_component() {
10738          return $this->component;
10739      }
10740  }
10741  
10742  /**
10743   * Get human readable name describing the given callable.
10744   *
10745   * This performs syntax check only to see if the given param looks like a valid function, method or closure.
10746   * It does not check if the callable actually exists.
10747   *
10748   * @param callable|string|array $callable
10749   * @return string|bool Human readable name of callable, or false if not a valid callable.
10750   */
10751  function get_callable_name($callable) {
10752  
10753      if (!is_callable($callable, true, $name)) {
10754          return false;
10755  
10756      } else {
10757          return $name;
10758      }
10759  }
10760  
10761  /**
10762   * Tries to guess if $CFG->wwwroot is publicly accessible or not.
10763   * Never put your faith on this function and rely on its accuracy as there might be false positives.
10764   * It just performs some simple checks, and mainly is used for places where we want to hide some options
10765   * such as site registration when $CFG->wwwroot is not publicly accessible.
10766   * Good thing is there is no false negative.
10767   * Note that it's possible to force the result of this check by specifying $CFG->site_is_public in config.php
10768   *
10769   * @return bool
10770   */
10771  function site_is_public() {
10772      global $CFG;
10773  
10774      // Return early if site admin has forced this setting.
10775      if (isset($CFG->site_is_public)) {
10776          return (bool)$CFG->site_is_public;
10777      }
10778  
10779      $host = parse_url($CFG->wwwroot, PHP_URL_HOST);
10780  
10781      if ($host === 'localhost' || preg_match('|^127\.\d+\.\d+\.\d+$|', $host)) {
10782          $ispublic = false;
10783      } else if (\core\ip_utils::is_ip_address($host) && !ip_is_public($host)) {
10784          $ispublic = false;
10785      } else if (($address = \core\ip_utils::get_ip_address($host)) && !ip_is_public($address)) {
10786          $ispublic = false;
10787      } else {
10788          $ispublic = true;
10789      }
10790  
10791      return $ispublic;
10792  }