[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/ -> moodlelib.php (source)

   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 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 and letters only.
  87   */
  88  define('PARAM_ALPHANUM', 'alphanum');
  89  
  90  /**
  91   * PARAM_ALPHANUMEXT - expected numbers, letters only and _-.
  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 recieving
 119   * the input (required/optional_param or formslib) and then sanitse 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   * Instead, do something like
 141   *     $rawvalue = required_param('name', PARAM_RAW);
 142   *     // ... other code including require_login, which sets current lang ...
 143   *     $realvalue = unformat_float($rawvalue);
 144   *     // ... then use $realvalue
 145   */
 146  define('PARAM_FLOAT',  'float');
 147  
 148  /**
 149   * PARAM_HOST - expected fully qualified domain name (FQDN) or an IPv4 dotted quad (IP address)
 150   */
 151  define('PARAM_HOST',     'host');
 152  
 153  /**
 154   * PARAM_INT - integers only, use when expecting only numbers.
 155   */
 156  define('PARAM_INT',      'int');
 157  
 158  /**
 159   * PARAM_LANG - checks to see if the string is a valid installed language in the current site.
 160   */
 161  define('PARAM_LANG',  'lang');
 162  
 163  /**
 164   * PARAM_LOCALURL - expected properly formatted URL as well as one that refers to the local server itself. (NOT orthogonal to the
 165   * others! Implies PARAM_URL!)
 166   */
 167  define('PARAM_LOCALURL', 'localurl');
 168  
 169  /**
 170   * PARAM_NOTAGS - all html tags are stripped from the text. Do not abuse this type.
 171   */
 172  define('PARAM_NOTAGS',   'notags');
 173  
 174  /**
 175   * PARAM_PATH - safe relative path name, all dangerous chars are stripped, protects against XSS, SQL injections and directory
 176   * traversals note: the leading slash is not removed, window drive letter is not allowed
 177   */
 178  define('PARAM_PATH',     'path');
 179  
 180  /**
 181   * PARAM_PEM - Privacy Enhanced Mail format
 182   */
 183  define('PARAM_PEM',      'pem');
 184  
 185  /**
 186   * PARAM_PERMISSION - A permission, one of CAP_INHERIT, CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT.
 187   */
 188  define('PARAM_PERMISSION',   'permission');
 189  
 190  /**
 191   * PARAM_RAW specifies a parameter that is not cleaned/processed in any way except the discarding of the invalid utf-8 characters
 192   */
 193  define('PARAM_RAW', 'raw');
 194  
 195  /**
 196   * PARAM_RAW_TRIMMED like PARAM_RAW but leading and trailing whitespace is stripped.
 197   */
 198  define('PARAM_RAW_TRIMMED', 'raw_trimmed');
 199  
 200  /**
 201   * PARAM_SAFEDIR - safe directory name, suitable for include() and require()
 202   */
 203  define('PARAM_SAFEDIR',  'safedir');
 204  
 205  /**
 206   * PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths, etc.
 207   */
 208  define('PARAM_SAFEPATH',  'safepath');
 209  
 210  /**
 211   * PARAM_SEQUENCE - expects a sequence of numbers like 8 to 1,5,6,4,6,8,9.  Numbers and comma only.
 212   */
 213  define('PARAM_SEQUENCE',  'sequence');
 214  
 215  /**
 216   * PARAM_TAG - one tag (interests, blogs, etc.) - mostly international characters and space, <> not supported
 217   */
 218  define('PARAM_TAG',   'tag');
 219  
 220  /**
 221   * PARAM_TAGLIST - list of tags separated by commas (interests, blogs, etc.)
 222   */
 223  define('PARAM_TAGLIST',   'taglist');
 224  
 225  /**
 226   * PARAM_TEXT - general plain text compatible with multilang filter, no other html tags. Please note '<', or '>' are allowed here.
 227   */
 228  define('PARAM_TEXT',  'text');
 229  
 230  /**
 231   * PARAM_THEME - Checks to see if the string is a valid theme name in the current site
 232   */
 233  define('PARAM_THEME',  'theme');
 234  
 235  /**
 236   * PARAM_URL - expected properly formatted URL. Please note that domain part is required, http://localhost/ is not accepted but
 237   * http://localhost.localdomain/ is ok.
 238   */
 239  define('PARAM_URL',      'url');
 240  
 241  /**
 242   * PARAM_USERNAME - Clean username to only contains allowed characters. This is to be used ONLY when manually creating user
 243   * accounts, do NOT use when syncing with external systems!!
 244   */
 245  define('PARAM_USERNAME',    'username');
 246  
 247  /**
 248   * PARAM_STRINGID - used to check if the given string is valid string identifier for get_string()
 249   */
 250  define('PARAM_STRINGID',    'stringid');
 251  
 252  // DEPRECATED PARAM TYPES OR ALIASES - DO NOT USE FOR NEW CODE.
 253  /**
 254   * PARAM_CLEAN - obsoleted, please use a more specific type of parameter.
 255   * It was one of the first types, that is why it is abused so much ;-)
 256   * @deprecated since 2.0
 257   */
 258  define('PARAM_CLEAN',    'clean');
 259  
 260  /**
 261   * PARAM_INTEGER - deprecated alias for PARAM_INT
 262   * @deprecated since 2.0
 263   */
 264  define('PARAM_INTEGER',  'int');
 265  
 266  /**
 267   * PARAM_NUMBER - deprecated alias of PARAM_FLOAT
 268   * @deprecated since 2.0
 269   */
 270  define('PARAM_NUMBER',  'float');
 271  
 272  /**
 273   * PARAM_ACTION - deprecated alias for PARAM_ALPHANUMEXT, use for various actions in forms and urls
 274   * NOTE: originally alias for PARAM_APLHA
 275   * @deprecated since 2.0
 276   */
 277  define('PARAM_ACTION',   'alphanumext');
 278  
 279  /**
 280   * PARAM_FORMAT - deprecated alias for PARAM_ALPHANUMEXT, use for names of plugins, formats, etc.
 281   * NOTE: originally alias for PARAM_APLHA
 282   * @deprecated since 2.0
 283   */
 284  define('PARAM_FORMAT',   'alphanumext');
 285  
 286  /**
 287   * PARAM_MULTILANG - deprecated alias of PARAM_TEXT.
 288   * @deprecated since 2.0
 289   */
 290  define('PARAM_MULTILANG',  'text');
 291  
 292  /**
 293   * PARAM_TIMEZONE - expected timezone. Timezone can be int +-(0-13) or float +-(0.5-12.5) or
 294   * string separated by '/' and can have '-' &/ '_' (eg. America/North_Dakota/New_Salem
 295   * America/Port-au-Prince)
 296   */
 297  define('PARAM_TIMEZONE', 'timezone');
 298  
 299  /**
 300   * PARAM_CLEANFILE - deprecated alias of PARAM_FILE; originally was removing regional chars too
 301   */
 302  define('PARAM_CLEANFILE', 'file');
 303  
 304  /**
 305   * PARAM_COMPONENT is used for full component names (aka frankenstyle) such as 'mod_forum', 'core_rating', 'auth_ldap'.
 306   * Short legacy subsystem names and module names are accepted too ex: 'forum', 'rating', 'user'.
 307   * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
 308   * NOTE: numbers and underscores are strongly discouraged in plugin names!
 309   */
 310  define('PARAM_COMPONENT', 'component');
 311  
 312  /**
 313   * PARAM_AREA is a name of area used when addressing files, comments, ratings, etc.
 314   * It is usually used together with context id and component.
 315   * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
 316   */
 317  define('PARAM_AREA', 'area');
 318  
 319  /**
 320   * PARAM_PLUGIN is used for plugin names such as 'forum', 'glossary', 'ldap', 'radius', 'paypal', 'completionstatus'.
 321   * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
 322   * NOTE: numbers and underscores are strongly discouraged in plugin names! Underscores are forbidden in module names.
 323   */
 324  define('PARAM_PLUGIN', 'plugin');
 325  
 326  
 327  // Web Services.
 328  
 329  /**
 330   * VALUE_REQUIRED - if the parameter is not supplied, there is an error
 331   */
 332  define('VALUE_REQUIRED', 1);
 333  
 334  /**
 335   * VALUE_OPTIONAL - if the parameter is not supplied, then the param has no value
 336   */
 337  define('VALUE_OPTIONAL', 2);
 338  
 339  /**
 340   * VALUE_DEFAULT - if the parameter is not supplied, then the default value is used
 341   */
 342  define('VALUE_DEFAULT', 0);
 343  
 344  /**
 345   * NULL_NOT_ALLOWED - the parameter can not be set to null in the database
 346   */
 347  define('NULL_NOT_ALLOWED', false);
 348  
 349  /**
 350   * NULL_ALLOWED - the parameter can be set to null in the database
 351   */
 352  define('NULL_ALLOWED', true);
 353  
 354  // Page types.
 355  
 356  /**
 357   * PAGE_COURSE_VIEW is a definition of a page type. For more information on the page class see moodle/lib/pagelib.php.
 358   */
 359  define('PAGE_COURSE_VIEW', 'course-view');
 360  
 361  /** Get remote addr constant */
 362  define('GETREMOTEADDR_SKIP_HTTP_CLIENT_IP', '1');
 363  /** Get remote addr constant */
 364  define('GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR', '2');
 365  
 366  // Blog access level constant declaration.
 367  define ('BLOG_USER_LEVEL', 1);
 368  define ('BLOG_GROUP_LEVEL', 2);
 369  define ('BLOG_COURSE_LEVEL', 3);
 370  define ('BLOG_SITE_LEVEL', 4);
 371  define ('BLOG_GLOBAL_LEVEL', 5);
 372  
 373  
 374  // Tag constants.
 375  /**
 376   * To prevent problems with multibytes strings,Flag updating in nav not working on the review page. this should not exceed the
 377   * length of "varchar(255) / 3 (bytes / utf-8 character) = 85".
 378   * TODO: this is not correct, varchar(255) are 255 unicode chars ;-)
 379   *
 380   * @todo define(TAG_MAX_LENGTH) this is not correct, varchar(255) are 255 unicode chars ;-)
 381   */
 382  define('TAG_MAX_LENGTH', 50);
 383  
 384  // Password policy constants.
 385  define ('PASSWORD_LOWER', 'abcdefghijklmnopqrstuvwxyz');
 386  define ('PASSWORD_UPPER', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
 387  define ('PASSWORD_DIGITS', '0123456789');
 388  define ('PASSWORD_NONALPHANUM', '.,;:!?_-+/*@#&$');
 389  
 390  // Feature constants.
 391  // Used for plugin_supports() to report features that are, or are not, supported by a module.
 392  
 393  /** True if module can provide a grade */
 394  define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade');
 395  /** True if module supports outcomes */
 396  define('FEATURE_GRADE_OUTCOMES', 'outcomes');
 397  /** True if module supports advanced grading methods */
 398  define('FEATURE_ADVANCED_GRADING', 'grade_advanced_grading');
 399  /** True if module controls the grade visibility over the gradebook */
 400  define('FEATURE_CONTROLS_GRADE_VISIBILITY', 'controlsgradevisbility');
 401  /** True if module supports plagiarism plugins */
 402  define('FEATURE_PLAGIARISM', 'plagiarism');
 403  
 404  /** True if module has code to track whether somebody viewed it */
 405  define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views');
 406  /** True if module has custom completion rules */
 407  define('FEATURE_COMPLETION_HAS_RULES', 'completion_has_rules');
 408  
 409  /** True if module has no 'view' page (like label) */
 410  define('FEATURE_NO_VIEW_LINK', 'viewlink');
 411  /** True (which is default) if the module wants support for setting the ID number for grade calculation purposes. */
 412  define('FEATURE_IDNUMBER', 'idnumber');
 413  /** True if module supports groups */
 414  define('FEATURE_GROUPS', 'groups');
 415  /** True if module supports groupings */
 416  define('FEATURE_GROUPINGS', 'groupings');
 417  /**
 418   * True if module supports groupmembersonly (which no longer exists)
 419   * @deprecated Since Moodle 2.8
 420   */
 421  define('FEATURE_GROUPMEMBERSONLY', 'groupmembersonly');
 422  
 423  /** Type of module */
 424  define('FEATURE_MOD_ARCHETYPE', 'mod_archetype');
 425  /** True if module supports intro editor */
 426  define('FEATURE_MOD_INTRO', 'mod_intro');
 427  /** True if module has default completion */
 428  define('FEATURE_MODEDIT_DEFAULT_COMPLETION', 'modedit_default_completion');
 429  
 430  define('FEATURE_COMMENT', 'comment');
 431  
 432  define('FEATURE_RATE', 'rate');
 433  /** True if module supports backup/restore of moodle2 format */
 434  define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2');
 435  
 436  /** True if module can show description on course main page */
 437  define('FEATURE_SHOW_DESCRIPTION', 'showdescription');
 438  
 439  /** True if module uses the question bank */
 440  define('FEATURE_USES_QUESTIONS', 'usesquestions');
 441  
 442  /** Unspecified module archetype */
 443  define('MOD_ARCHETYPE_OTHER', 0);
 444  /** Resource-like type module */
 445  define('MOD_ARCHETYPE_RESOURCE', 1);
 446  /** Assignment module archetype */
 447  define('MOD_ARCHETYPE_ASSIGNMENT', 2);
 448  /** System (not user-addable) module archetype */
 449  define('MOD_ARCHETYPE_SYSTEM', 3);
 450  
 451  /**
 452   * Return this from modname_get_types callback to use default display in activity chooser.
 453   * Deprecated, will be removed in 3.5, TODO MDL-53697.
 454   * @deprecated since Moodle 3.1
 455   */
 456  define('MOD_SUBTYPE_NO_CHILDREN', 'modsubtypenochildren');
 457  
 458  /**
 459   * Security token used for allowing access
 460   * from external application such as web services.
 461   * Scripts do not use any session, performance is relatively
 462   * low because we need to load access info in each request.
 463   * Scripts are executed in parallel.
 464   */
 465  define('EXTERNAL_TOKEN_PERMANENT', 0);
 466  
 467  /**
 468   * Security token used for allowing access
 469   * of embedded applications, the code is executed in the
 470   * active user session. Token is invalidated after user logs out.
 471   * Scripts are executed serially - normal session locking is used.
 472   */
 473  define('EXTERNAL_TOKEN_EMBEDDED', 1);
 474  
 475  /**
 476   * The home page should be the site home
 477   */
 478  define('HOMEPAGE_SITE', 0);
 479  /**
 480   * The home page should be the users my page
 481   */
 482  define('HOMEPAGE_MY', 1);
 483  /**
 484   * The home page can be chosen by the user
 485   */
 486  define('HOMEPAGE_USER', 2);
 487  
 488  /**
 489   * Hub directory url (should be moodle.org)
 490   */
 491  define('HUB_HUBDIRECTORYURL', "http://hubdirectory.moodle.org");
 492  
 493  
 494  /**
 495   * Moodle.org url (should be moodle.org)
 496   */
 497  define('HUB_MOODLEORGHUBURL', "http://hub.moodle.org");
 498  
 499  /**
 500   * Moodle mobile app service name
 501   */
 502  define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app');
 503  
 504  /**
 505   * Indicates the user has the capabilities required to ignore activity and course file size restrictions
 506   */
 507  define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1);
 508  
 509  /**
 510   * Course display settings: display all sections on one page.
 511   */
 512  define('COURSE_DISPLAY_SINGLEPAGE', 0);
 513  /**
 514   * Course display settings: split pages into a page per section.
 515   */
 516  define('COURSE_DISPLAY_MULTIPAGE', 1);
 517  
 518  /**
 519   * Authentication constant: String used in password field when password is not stored.
 520   */
 521  define('AUTH_PASSWORD_NOT_CACHED', 'not cached');
 522  
 523  // PARAMETER HANDLING.
 524  
 525  /**
 526   * Returns a particular value for the named variable, taken from
 527   * POST or GET.  If the parameter doesn't exist then an error is
 528   * thrown because we require this variable.
 529   *
 530   * This function should be used to initialise all required values
 531   * in a script that are based on parameters.  Usually it will be
 532   * used like this:
 533   *    $id = required_param('id', PARAM_INT);
 534   *
 535   * Please note the $type parameter is now required and the value can not be array.
 536   *
 537   * @param string $parname the name of the page parameter we want
 538   * @param string $type expected type of parameter
 539   * @return mixed
 540   * @throws coding_exception
 541   */
 542  function required_param($parname, $type) {
 543      if (func_num_args() != 2 or empty($parname) or empty($type)) {
 544          throw new coding_exception('required_param() requires $parname and $type to be specified (parameter: '.$parname.')');
 545      }
 546      // POST has precedence.
 547      if (isset($_POST[$parname])) {
 548          $param = $_POST[$parname];
 549      } else if (isset($_GET[$parname])) {
 550          $param = $_GET[$parname];
 551      } else {
 552          print_error('missingparam', '', '', $parname);
 553      }
 554  
 555      if (is_array($param)) {
 556          debugging('Invalid array parameter detected in required_param(): '.$parname);
 557          // TODO: switch to fatal error in Moodle 2.3.
 558          return required_param_array($parname, $type);
 559      }
 560  
 561      return clean_param($param, $type);
 562  }
 563  
 564  /**
 565   * Returns a particular array value for the named variable, taken from
 566   * POST or GET.  If the parameter doesn't exist then an error is
 567   * thrown because we require this variable.
 568   *
 569   * This function should be used to initialise all required values
 570   * in a script that are based on parameters.  Usually it will be
 571   * used like this:
 572   *    $ids = required_param_array('ids', PARAM_INT);
 573   *
 574   *  Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
 575   *
 576   * @param string $parname the name of the page parameter we want
 577   * @param string $type expected type of parameter
 578   * @return array
 579   * @throws coding_exception
 580   */
 581  function required_param_array($parname, $type) {
 582      if (func_num_args() != 2 or empty($parname) or empty($type)) {
 583          throw new coding_exception('required_param_array() requires $parname and $type to be specified (parameter: '.$parname.')');
 584      }
 585      // POST has precedence.
 586      if (isset($_POST[$parname])) {
 587          $param = $_POST[$parname];
 588      } else if (isset($_GET[$parname])) {
 589          $param = $_GET[$parname];
 590      } else {
 591          print_error('missingparam', '', '', $parname);
 592      }
 593      if (!is_array($param)) {
 594          print_error('missingparam', '', '', $parname);
 595      }
 596  
 597      $result = array();
 598      foreach ($param as $key => $value) {
 599          if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
 600              debugging('Invalid key name in required_param_array() detected: '.$key.', parameter: '.$parname);
 601              continue;
 602          }
 603          $result[$key] = clean_param($value, $type);
 604      }
 605  
 606      return $result;
 607  }
 608  
 609  /**
 610   * Returns a particular value for the named variable, taken from
 611   * POST or GET, otherwise returning a given default.
 612   *
 613   * This function should be used to initialise all optional values
 614   * in a script that are based on parameters.  Usually it will be
 615   * used like this:
 616   *    $name = optional_param('name', 'Fred', PARAM_TEXT);
 617   *
 618   * Please note the $type parameter is now required and the value can not be array.
 619   *
 620   * @param string $parname the name of the page parameter we want
 621   * @param mixed  $default the default value to return if nothing is found
 622   * @param string $type expected type of parameter
 623   * @return mixed
 624   * @throws coding_exception
 625   */
 626  function optional_param($parname, $default, $type) {
 627      if (func_num_args() != 3 or empty($parname) or empty($type)) {
 628          throw new coding_exception('optional_param requires $parname, $default + $type to be specified (parameter: '.$parname.')');
 629      }
 630  
 631      // POST has precedence.
 632      if (isset($_POST[$parname])) {
 633          $param = $_POST[$parname];
 634      } else if (isset($_GET[$parname])) {
 635          $param = $_GET[$parname];
 636      } else {
 637          return $default;
 638      }
 639  
 640      if (is_array($param)) {
 641          debugging('Invalid array parameter detected in required_param(): '.$parname);
 642          // TODO: switch to $default in Moodle 2.3.
 643          return optional_param_array($parname, $default, $type);
 644      }
 645  
 646      return clean_param($param, $type);
 647  }
 648  
 649  /**
 650   * Returns a particular array value for the named variable, taken from
 651   * POST or GET, otherwise returning a given default.
 652   *
 653   * This function should be used to initialise all optional values
 654   * in a script that are based on parameters.  Usually it will be
 655   * used like this:
 656   *    $ids = optional_param('id', array(), PARAM_INT);
 657   *
 658   * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
 659   *
 660   * @param string $parname the name of the page parameter we want
 661   * @param mixed $default the default value to return if nothing is found
 662   * @param string $type expected type of parameter
 663   * @return array
 664   * @throws coding_exception
 665   */
 666  function optional_param_array($parname, $default, $type) {
 667      if (func_num_args() != 3 or empty($parname) or empty($type)) {
 668          throw new coding_exception('optional_param_array requires $parname, $default + $type to be specified (parameter: '.$parname.')');
 669      }
 670  
 671      // POST has precedence.
 672      if (isset($_POST[$parname])) {
 673          $param = $_POST[$parname];
 674      } else if (isset($_GET[$parname])) {
 675          $param = $_GET[$parname];
 676      } else {
 677          return $default;
 678      }
 679      if (!is_array($param)) {
 680          debugging('optional_param_array() expects array parameters only: '.$parname);
 681          return $default;
 682      }
 683  
 684      $result = array();
 685      foreach ($param as $key => $value) {
 686          if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
 687              debugging('Invalid key name in optional_param_array() detected: '.$key.', parameter: '.$parname);
 688              continue;
 689          }
 690          $result[$key] = clean_param($value, $type);
 691      }
 692  
 693      return $result;
 694  }
 695  
 696  /**
 697   * Strict validation of parameter values, the values are only converted
 698   * to requested PHP type. Internally it is using clean_param, the values
 699   * before and after cleaning must be equal - otherwise
 700   * an invalid_parameter_exception is thrown.
 701   * Objects and classes are not accepted.
 702   *
 703   * @param mixed $param
 704   * @param string $type PARAM_ constant
 705   * @param bool $allownull are nulls valid value?
 706   * @param string $debuginfo optional debug information
 707   * @return mixed the $param value converted to PHP type
 708   * @throws invalid_parameter_exception if $param is not of given type
 709   */
 710  function validate_param($param, $type, $allownull=NULL_NOT_ALLOWED, $debuginfo='') {
 711      if (is_null($param)) {
 712          if ($allownull == NULL_ALLOWED) {
 713              return null;
 714          } else {
 715              throw new invalid_parameter_exception($debuginfo);
 716          }
 717      }
 718      if (is_array($param) or is_object($param)) {
 719          throw new invalid_parameter_exception($debuginfo);
 720      }
 721  
 722      $cleaned = clean_param($param, $type);
 723  
 724      if ($type == PARAM_FLOAT) {
 725          // Do not detect precision loss here.
 726          if (is_float($param) or is_int($param)) {
 727              // These always fit.
 728          } else if (!is_numeric($param) or !preg_match('/^[\+-]?[0-9]*\.?[0-9]*(e[-+]?[0-9]+)?$/i', (string)$param)) {
 729              throw new invalid_parameter_exception($debuginfo);
 730          }
 731      } else if ((string)$param !== (string)$cleaned) {
 732          // Conversion to string is usually lossless.
 733          throw new invalid_parameter_exception($debuginfo);
 734      }
 735  
 736      return $cleaned;
 737  }
 738  
 739  /**
 740   * Makes sure array contains only the allowed types, this function does not validate array key names!
 741   *
 742   * <code>
 743   * $options = clean_param($options, PARAM_INT);
 744   * </code>
 745   *
 746   * @param array $param the variable array we are cleaning
 747   * @param string $type expected format of param after cleaning.
 748   * @param bool $recursive clean recursive arrays
 749   * @return array
 750   * @throws coding_exception
 751   */
 752  function clean_param_array(array $param = null, $type, $recursive = false) {
 753      // Convert null to empty array.
 754      $param = (array)$param;
 755      foreach ($param as $key => $value) {
 756          if (is_array($value)) {
 757              if ($recursive) {
 758                  $param[$key] = clean_param_array($value, $type, true);
 759              } else {
 760                  throw new coding_exception('clean_param_array can not process multidimensional arrays when $recursive is false.');
 761              }
 762          } else {
 763              $param[$key] = clean_param($value, $type);
 764          }
 765      }
 766      return $param;
 767  }
 768  
 769  /**
 770   * Used by {@link optional_param()} and {@link required_param()} to
 771   * clean the variables and/or cast to specific types, based on
 772   * an options field.
 773   * <code>
 774   * $course->format = clean_param($course->format, PARAM_ALPHA);
 775   * $selectedgradeitem = clean_param($selectedgradeitem, PARAM_INT);
 776   * </code>
 777   *
 778   * @param mixed $param the variable we are cleaning
 779   * @param string $type expected format of param after cleaning.
 780   * @return mixed
 781   * @throws coding_exception
 782   */
 783  function clean_param($param, $type) {
 784      global $CFG;
 785  
 786      if (is_array($param)) {
 787          throw new coding_exception('clean_param() can not process arrays, please use clean_param_array() instead.');
 788      } else if (is_object($param)) {
 789          if (method_exists($param, '__toString')) {
 790              $param = $param->__toString();
 791          } else {
 792              throw new coding_exception('clean_param() can not process objects, please use clean_param_array() instead.');
 793          }
 794      }
 795  
 796      switch ($type) {
 797          case PARAM_RAW:
 798              // No cleaning at all.
 799              $param = fix_utf8($param);
 800              return $param;
 801  
 802          case PARAM_RAW_TRIMMED:
 803              // No cleaning, but strip leading and trailing whitespace.
 804              $param = fix_utf8($param);
 805              return trim($param);
 806  
 807          case PARAM_CLEAN:
 808              // General HTML cleaning, try to use more specific type if possible this is deprecated!
 809              // Please use more specific type instead.
 810              if (is_numeric($param)) {
 811                  return $param;
 812              }
 813              $param = fix_utf8($param);
 814              // Sweep for scripts, etc.
 815              return clean_text($param);
 816  
 817          case PARAM_CLEANHTML:
 818              // Clean html fragment.
 819              $param = fix_utf8($param);
 820              // Sweep for scripts, etc.
 821              $param = clean_text($param, FORMAT_HTML);
 822              return trim($param);
 823  
 824          case PARAM_INT:
 825              // Convert to integer.
 826              return (int)$param;
 827  
 828          case PARAM_FLOAT:
 829              // Convert to float.
 830              return (float)$param;
 831  
 832          case PARAM_ALPHA:
 833              // Remove everything not `a-z`.
 834              return preg_replace('/[^a-zA-Z]/i', '', $param);
 835  
 836          case PARAM_ALPHAEXT:
 837              // Remove everything not `a-zA-Z_-` (originally allowed "/" too).
 838              return preg_replace('/[^a-zA-Z_-]/i', '', $param);
 839  
 840          case PARAM_ALPHANUM:
 841              // Remove everything not `a-zA-Z0-9`.
 842              return preg_replace('/[^A-Za-z0-9]/i', '', $param);
 843  
 844          case PARAM_ALPHANUMEXT:
 845              // Remove everything not `a-zA-Z0-9_-`.
 846              return preg_replace('/[^A-Za-z0-9_-]/i', '', $param);
 847  
 848          case PARAM_SEQUENCE:
 849              // Remove everything not `0-9,`.
 850              return preg_replace('/[^0-9,]/i', '', $param);
 851  
 852          case PARAM_BOOL:
 853              // Convert to 1 or 0.
 854              $tempstr = strtolower($param);
 855              if ($tempstr === 'on' or $tempstr === 'yes' or $tempstr === 'true') {
 856                  $param = 1;
 857              } else if ($tempstr === 'off' or $tempstr === 'no'  or $tempstr === 'false') {
 858                  $param = 0;
 859              } else {
 860                  $param = empty($param) ? 0 : 1;
 861              }
 862              return $param;
 863  
 864          case PARAM_NOTAGS:
 865              // Strip all tags.
 866              $param = fix_utf8($param);
 867              return strip_tags($param);
 868  
 869          case PARAM_TEXT:
 870              // Leave only tags needed for multilang.
 871              $param = fix_utf8($param);
 872              // If the multilang syntax is not correct we strip all tags because it would break xhtml strict which is required
 873              // for accessibility standards please note this cleaning does not strip unbalanced '>' for BC compatibility reasons.
 874              do {
 875                  if (strpos($param, '</lang>') !== false) {
 876                      // Old and future mutilang syntax.
 877                      $param = strip_tags($param, '<lang>');
 878                      if (!preg_match_all('/<.*>/suU', $param, $matches)) {
 879                          break;
 880                      }
 881                      $open = false;
 882                      foreach ($matches[0] as $match) {
 883                          if ($match === '</lang>') {
 884                              if ($open) {
 885                                  $open = false;
 886                                  continue;
 887                              } else {
 888                                  break 2;
 889                              }
 890                          }
 891                          if (!preg_match('/^<lang lang="[a-zA-Z0-9_-]+"\s*>$/u', $match)) {
 892                              break 2;
 893                          } else {
 894                              $open = true;
 895                          }
 896                      }
 897                      if ($open) {
 898                          break;
 899                      }
 900                      return $param;
 901  
 902                  } else if (strpos($param, '</span>') !== false) {
 903                      // Current problematic multilang syntax.
 904                      $param = strip_tags($param, '<span>');
 905                      if (!preg_match_all('/<.*>/suU', $param, $matches)) {
 906                          break;
 907                      }
 908                      $open = false;
 909                      foreach ($matches[0] as $match) {
 910                          if ($match === '</span>') {
 911                              if ($open) {
 912                                  $open = false;
 913                                  continue;
 914                              } else {
 915                                  break 2;
 916                              }
 917                          }
 918                          if (!preg_match('/^<span(\s+lang="[a-zA-Z0-9_-]+"|\s+class="multilang"){2}\s*>$/u', $match)) {
 919                              break 2;
 920                          } else {
 921                              $open = true;
 922                          }
 923                      }
 924                      if ($open) {
 925                          break;
 926                      }
 927                      return $param;
 928                  }
 929              } while (false);
 930              // Easy, just strip all tags, if we ever want to fix orphaned '&' we have to do that in format_string().
 931              return strip_tags($param);
 932  
 933          case PARAM_COMPONENT:
 934              // We do not want any guessing here, either the name is correct or not
 935              // please note only normalised component names are accepted.
 936              if (!preg_match('/^[a-z]+(_[a-z][a-z0-9_]*)?[a-z0-9]+$/', $param)) {
 937                  return '';
 938              }
 939              if (strpos($param, '__') !== false) {
 940                  return '';
 941              }
 942              if (strpos($param, 'mod_') === 0) {
 943                  // Module names must not contain underscores because we need to differentiate them from invalid plugin types.
 944                  if (substr_count($param, '_') != 1) {
 945                      return '';
 946                  }
 947              }
 948              return $param;
 949  
 950          case PARAM_PLUGIN:
 951          case PARAM_AREA:
 952              // We do not want any guessing here, either the name is correct or not.
 953              if (!is_valid_plugin_name($param)) {
 954                  return '';
 955              }
 956              return $param;
 957  
 958          case PARAM_SAFEDIR:
 959              // Remove everything not a-zA-Z0-9_- .
 960              return preg_replace('/[^a-zA-Z0-9_-]/i', '', $param);
 961  
 962          case PARAM_SAFEPATH:
 963              // Remove everything not a-zA-Z0-9/_- .
 964              return preg_replace('/[^a-zA-Z0-9\/_-]/i', '', $param);
 965  
 966          case PARAM_FILE:
 967              // Strip all suspicious characters from filename.
 968              $param = fix_utf8($param);
 969              $param = preg_replace('~[[:cntrl:]]|[&<>"`\|\':\\\\/]~u', '', $param);
 970              if ($param === '.' || $param === '..') {
 971                  $param = '';
 972              }
 973              return $param;
 974  
 975          case PARAM_PATH:
 976              // Strip all suspicious characters from file path.
 977              $param = fix_utf8($param);
 978              $param = str_replace('\\', '/', $param);
 979  
 980              // Explode the path and clean each element using the PARAM_FILE rules.
 981              $breadcrumb = explode('/', $param);
 982              foreach ($breadcrumb as $key => $crumb) {
 983                  if ($crumb === '.' && $key === 0) {
 984                      // Special condition to allow for relative current path such as ./currentdirfile.txt.
 985                  } else {
 986                      $crumb = clean_param($crumb, PARAM_FILE);
 987                  }
 988                  $breadcrumb[$key] = $crumb;
 989              }
 990              $param = implode('/', $breadcrumb);
 991  
 992              // Remove multiple current path (./././) and multiple slashes (///).
 993              $param = preg_replace('~//+~', '/', $param);
 994              $param = preg_replace('~/(\./)+~', '/', $param);
 995              return $param;
 996  
 997          case PARAM_HOST:
 998              // Allow FQDN or IPv4 dotted quad.
 999              $param = preg_replace('/[^\.\d\w-]/', '', $param );
1000              // Match ipv4 dotted quad.
1001              if (preg_match('/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/', $param, $match)) {
1002                  // Confirm values are ok.
1003                  if ( $match[0] > 255
1004                       || $match[1] > 255
1005                       || $match[3] > 255
1006                       || $match[4] > 255 ) {
1007                      // Hmmm, what kind of dotted quad is this?
1008                      $param = '';
1009                  }
1010              } else if ( preg_match('/^[\w\d\.-]+$/', $param) // Dots, hyphens, numbers.
1011                         && !preg_match('/^[\.-]/',  $param) // No leading dots/hyphens.
1012                         && !preg_match('/[\.-]$/',  $param) // No trailing dots/hyphens.
1013                         ) {
1014                  // All is ok - $param is respected.
1015              } else {
1016                  // All is not ok...
1017                  $param='';
1018              }
1019              return $param;
1020  
1021          case PARAM_URL:          // Allow safe ftp, http, mailto urls.
1022              $param = fix_utf8($param);
1023              include_once($CFG->dirroot . '/lib/validateurlsyntax.php');
1024              if (!empty($param) && validateUrlSyntax($param, 's?H?S?F?E?u-P-a?I?p?f?q?r?')) {
1025                  // All is ok, param is respected.
1026              } else {
1027                  // Not really ok.
1028                  $param ='';
1029              }
1030              return $param;
1031  
1032          case PARAM_LOCALURL:
1033              // Allow http absolute, root relative and relative URLs within wwwroot.
1034              $param = clean_param($param, PARAM_URL);
1035              if (!empty($param)) {
1036  
1037                  // Simulate the HTTPS version of the site.
1038                  $httpswwwroot = str_replace('http://', 'https://', $CFG->wwwroot);
1039  
1040                  if ($param === $CFG->wwwroot) {
1041                      // Exact match;
1042                  } else if (!empty($CFG->loginhttps) && $param === $httpswwwroot) {
1043                      // Exact match;
1044                  } else if (preg_match(':^/:', $param)) {
1045                      // Root-relative, ok!
1046                  } else if (preg_match('/^' . preg_quote($CFG->wwwroot . '/', '/') . '/i', $param)) {
1047                      // Absolute, and matches our wwwroot.
1048                  } else if (!empty($CFG->loginhttps) && preg_match('/^' . preg_quote($httpswwwroot . '/', '/') . '/i', $param)) {
1049                      // Absolute, and matches our httpswwwroot.
1050                  } else {
1051                      // Relative - let's make sure there are no tricks.
1052                      if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?')) {
1053                          // Looks ok.
1054                      } else {
1055                          $param = '';
1056                      }
1057                  }
1058              }
1059              return $param;
1060  
1061          case PARAM_PEM:
1062              $param = trim($param);
1063              // PEM formatted strings may contain letters/numbers and the symbols:
1064              //   forward slash: /
1065              //   plus sign:     +
1066              //   equal sign:    =
1067              //   , surrounded by BEGIN and END CERTIFICATE prefix and suffixes.
1068              if (preg_match('/^-----BEGIN CERTIFICATE-----([\s\w\/\+=]+)-----END CERTIFICATE-----$/', trim($param), $matches)) {
1069                  list($wholething, $body) = $matches;
1070                  unset($wholething, $matches);
1071                  $b64 = clean_param($body, PARAM_BASE64);
1072                  if (!empty($b64)) {
1073                      return "-----BEGIN CERTIFICATE-----\n$b64\n-----END CERTIFICATE-----\n";
1074                  } else {
1075                      return '';
1076                  }
1077              }
1078              return '';
1079  
1080          case PARAM_BASE64:
1081              if (!empty($param)) {
1082                  // PEM formatted strings may contain letters/numbers and the symbols
1083                  //   forward slash: /
1084                  //   plus sign:     +
1085                  //   equal sign:    =.
1086                  if (0 >= preg_match('/^([\s\w\/\+=]+)$/', trim($param))) {
1087                      return '';
1088                  }
1089                  $lines = preg_split('/[\s]+/', $param, -1, PREG_SPLIT_NO_EMPTY);
1090                  // Each line of base64 encoded data must be 64 characters in length, except for the last line which may be less
1091                  // than (or equal to) 64 characters long.
1092                  for ($i=0, $j=count($lines); $i < $j; $i++) {
1093                      if ($i + 1 == $j) {
1094                          if (64 < strlen($lines[$i])) {
1095                              return '';
1096                          }
1097                          continue;
1098                      }
1099  
1100                      if (64 != strlen($lines[$i])) {
1101                          return '';
1102                      }
1103                  }
1104                  return implode("\n", $lines);
1105              } else {
1106                  return '';
1107              }
1108  
1109          case PARAM_TAG:
1110              $param = fix_utf8($param);
1111              // Please note it is not safe to use the tag name directly anywhere,
1112              // it must be processed with s(), urlencode() before embedding anywhere.
1113              // Remove some nasties.
1114              $param = preg_replace('~[[:cntrl:]]|[<>`]~u', '', $param);
1115              // Convert many whitespace chars into one.
1116              $param = preg_replace('/\s+/u', ' ', $param);
1117              $param = core_text::substr(trim($param), 0, TAG_MAX_LENGTH);
1118              return $param;
1119  
1120          case PARAM_TAGLIST:
1121              $param = fix_utf8($param);
1122              $tags = explode(',', $param);
1123              $result = array();
1124              foreach ($tags as $tag) {
1125                  $res = clean_param($tag, PARAM_TAG);
1126                  if ($res !== '') {
1127                      $result[] = $res;
1128                  }
1129              }
1130              if ($result) {
1131                  return implode(',', $result);
1132              } else {
1133                  return '';
1134              }
1135  
1136          case PARAM_CAPABILITY:
1137              if (get_capability_info($param)) {
1138                  return $param;
1139              } else {
1140                  return '';
1141              }
1142  
1143          case PARAM_PERMISSION:
1144              $param = (int)$param;
1145              if (in_array($param, array(CAP_INHERIT, CAP_ALLOW, CAP_PREVENT, CAP_PROHIBIT))) {
1146                  return $param;
1147              } else {
1148                  return CAP_INHERIT;
1149              }
1150  
1151          case PARAM_AUTH:
1152              $param = clean_param($param, PARAM_PLUGIN);
1153              if (empty($param)) {
1154                  return '';
1155              } else if (exists_auth_plugin($param)) {
1156                  return $param;
1157              } else {
1158                  return '';
1159              }
1160  
1161          case PARAM_LANG:
1162              $param = clean_param($param, PARAM_SAFEDIR);
1163              if (get_string_manager()->translation_exists($param)) {
1164                  return $param;
1165              } else {
1166                  // Specified language is not installed or param malformed.
1167                  return '';
1168              }
1169  
1170          case PARAM_THEME:
1171              $param = clean_param($param, PARAM_PLUGIN);
1172              if (empty($param)) {
1173                  return '';
1174              } else if (file_exists("$CFG->dirroot/theme/$param/config.php")) {
1175                  return $param;
1176              } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$param/config.php")) {
1177                  return $param;
1178              } else {
1179                  // Specified theme is not installed.
1180                  return '';
1181              }
1182  
1183          case PARAM_USERNAME:
1184              $param = fix_utf8($param);
1185              $param = trim($param);
1186              // Convert uppercase to lowercase MDL-16919.
1187              $param = core_text::strtolower($param);
1188              if (empty($CFG->extendedusernamechars)) {
1189                  $param = str_replace(" " , "", $param);
1190                  // Regular expression, eliminate all chars EXCEPT:
1191                  // alphanum, dash (-), underscore (_), at sign (@) and period (.) characters.
1192                  $param = preg_replace('/[^-\.@_a-z0-9]/', '', $param);
1193              }
1194              return $param;
1195  
1196          case PARAM_EMAIL:
1197              $param = fix_utf8($param);
1198              if (validate_email($param)) {
1199                  return $param;
1200              } else {
1201                  return '';
1202              }
1203  
1204          case PARAM_STRINGID:
1205              if (preg_match('|^[a-zA-Z][a-zA-Z0-9\.:/_-]*$|', $param)) {
1206                  return $param;
1207              } else {
1208                  return '';
1209              }
1210  
1211          case PARAM_TIMEZONE:
1212              // Can be int, float(with .5 or .0) or string seperated by '/' and can have '-_'.
1213              $param = fix_utf8($param);
1214              $timezonepattern = '/^(([+-]?(0?[0-9](\.[5|0])?|1[0-3](\.0)?|1[0-2]\.5))|(99)|[[:alnum:]]+(\/?[[:alpha:]_-])+)$/';
1215              if (preg_match($timezonepattern, $param)) {
1216                  return $param;
1217              } else {
1218                  return '';
1219              }
1220  
1221          default:
1222              // Doh! throw error, switched parameters in optional_param or another serious problem.
1223              print_error("unknownparamtype", '', '', $type);
1224      }
1225  }
1226  
1227  /**
1228   * Makes sure the data is using valid utf8, invalid characters are discarded.
1229   *
1230   * Note: this function is not intended for full objects with methods and private properties.
1231   *
1232   * @param mixed $value
1233   * @return mixed with proper utf-8 encoding
1234   */
1235  function fix_utf8($value) {
1236      if (is_null($value) or $value === '') {
1237          return $value;
1238  
1239      } else if (is_string($value)) {
1240          if ((string)(int)$value === $value) {
1241              // Shortcut.
1242              return $value;
1243          }
1244          // No null bytes expected in our data, so let's remove it.
1245          $value = str_replace("\0", '', $value);
1246  
1247          // Note: this duplicates min_fix_utf8() intentionally.
1248          static $buggyiconv = null;
1249          if ($buggyiconv === null) {
1250              $buggyiconv = (!function_exists('iconv') or @iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€');
1251          }
1252  
1253          if ($buggyiconv) {
1254              if (function_exists('mb_convert_encoding')) {
1255                  $subst = mb_substitute_character();
1256                  mb_substitute_character('');
1257                  $result = mb_convert_encoding($value, 'utf-8', 'utf-8');
1258                  mb_substitute_character($subst);
1259  
1260              } else {
1261                  // Warn admins on admin/index.php page.
1262                  $result = $value;
1263              }
1264  
1265          } else {
1266              $result = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
1267          }
1268  
1269          return $result;
1270  
1271      } else if (is_array($value)) {
1272          foreach ($value as $k => $v) {
1273              $value[$k] = fix_utf8($v);
1274          }
1275          return $value;
1276  
1277      } else if (is_object($value)) {
1278          // Do not modify original.
1279          $value = clone($value);
1280          foreach ($value as $k => $v) {
1281              $value->$k = fix_utf8($v);
1282          }
1283          return $value;
1284  
1285      } else {
1286          // This is some other type, no utf-8 here.
1287          return $value;
1288      }
1289  }
1290  
1291  /**
1292   * Return true if given value is integer or string with integer value
1293   *
1294   * @param mixed $value String or Int
1295   * @return bool true if number, false if not
1296   */
1297  function is_number($value) {
1298      if (is_int($value)) {
1299          return true;
1300      } else if (is_string($value)) {
1301          return ((string)(int)$value) === $value;
1302      } else {
1303          return false;
1304      }
1305  }
1306  
1307  /**
1308   * Returns host part from url.
1309   *
1310   * @param string $url full url
1311   * @return string host, null if not found
1312   */
1313  function get_host_from_url($url) {
1314      preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches);
1315      if ($matches) {
1316          return $matches[1];
1317      }
1318      return null;
1319  }
1320  
1321  /**
1322   * Tests whether anything was returned by text editor
1323   *
1324   * This function is useful for testing whether something you got back from
1325   * the HTML editor actually contains anything. Sometimes the HTML editor
1326   * appear to be empty, but actually you get back a <br> tag or something.
1327   *
1328   * @param string $string a string containing HTML.
1329   * @return boolean does the string contain any actual content - that is text,
1330   * images, objects, etc.
1331   */
1332  function html_is_blank($string) {
1333      return trim(strip_tags($string, '<img><object><applet><input><select><textarea><hr>')) == '';
1334  }
1335  
1336  /**
1337   * Set a key in global configuration
1338   *
1339   * Set a key/value pair in both this session's {@link $CFG} global variable
1340   * and in the 'config' database table for future sessions.
1341   *
1342   * Can also be used to update keys for plugin-scoped configs in config_plugin table.
1343   * In that case it doesn't affect $CFG.
1344   *
1345   * A NULL value will delete the entry.
1346   *
1347   * NOTE: this function is called from lib/db/upgrade.php
1348   *
1349   * @param string $name the key to set
1350   * @param string $value the value to set (without magic quotes)
1351   * @param string $plugin (optional) the plugin scope, default null
1352   * @return bool true or exception
1353   */
1354  function set_config($name, $value, $plugin=null) {
1355      global $CFG, $DB;
1356  
1357      if (empty($plugin)) {
1358          if (!array_key_exists($name, $CFG->config_php_settings)) {
1359              // So it's defined for this invocation at least.
1360              if (is_null($value)) {
1361                  unset($CFG->$name);
1362              } else {
1363                  // Settings from db are always strings.
1364                  $CFG->$name = (string)$value;
1365              }
1366          }
1367  
1368          if ($DB->get_field('config', 'name', array('name' => $name))) {
1369              if ($value === null) {
1370                  $DB->delete_records('config', array('name' => $name));
1371              } else {
1372                  $DB->set_field('config', 'value', $value, array('name' => $name));
1373              }
1374          } else {
1375              if ($value !== null) {
1376                  $config = new stdClass();
1377                  $config->name  = $name;
1378                  $config->value = $value;
1379                  $DB->insert_record('config', $config, false);
1380              }
1381          }
1382          if ($name === 'siteidentifier') {
1383              cache_helper::update_site_identifier($value);
1384          }
1385          cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1386      } else {
1387          // Plugin scope.
1388          if ($id = $DB->get_field('config_plugins', 'id', array('name' => $name, 'plugin' => $plugin))) {
1389              if ($value===null) {
1390                  $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
1391              } else {
1392                  $DB->set_field('config_plugins', 'value', $value, array('id' => $id));
1393              }
1394          } else {
1395              if ($value !== null) {
1396                  $config = new stdClass();
1397                  $config->plugin = $plugin;
1398                  $config->name   = $name;
1399                  $config->value  = $value;
1400                  $DB->insert_record('config_plugins', $config, false);
1401              }
1402          }
1403          cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1404      }
1405  
1406      return true;
1407  }
1408  
1409  /**
1410   * Get configuration values from the global config table
1411   * or the config_plugins table.
1412   *
1413   * If called with one parameter, it will load all the config
1414   * variables for one plugin, and return them as an object.
1415   *
1416   * If called with 2 parameters it will return a string single
1417   * value or false if the value is not found.
1418   *
1419   * NOTE: this function is called from lib/db/upgrade.php
1420   *
1421   * @static string|false $siteidentifier The site identifier is not cached. We use this static cache so
1422   *     that we need only fetch it once per request.
1423   * @param string $plugin full component name
1424   * @param string $name default null
1425   * @return mixed hash-like object or single value, return false no config found
1426   * @throws dml_exception
1427   */
1428  function get_config($plugin, $name = null) {
1429      global $CFG, $DB;
1430  
1431      static $siteidentifier = null;
1432  
1433      if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) {
1434          $forced =& $CFG->config_php_settings;
1435          $iscore = true;
1436          $plugin = 'core';
1437      } else {
1438          if (array_key_exists($plugin, $CFG->forced_plugin_settings)) {
1439              $forced =& $CFG->forced_plugin_settings[$plugin];
1440          } else {
1441              $forced = array();
1442          }
1443          $iscore = false;
1444      }
1445  
1446      if ($siteidentifier === null) {
1447          try {
1448              // This may fail during installation.
1449              // If you have a look at {@link initialise_cfg()} you will see that this is how we detect the need to
1450              // install the database.
1451              $siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
1452          } catch (dml_exception $ex) {
1453              // Set siteidentifier to false. We don't want to trip this continually.
1454              $siteidentifier = false;
1455              throw $ex;
1456          }
1457      }
1458  
1459      if (!empty($name)) {
1460          if (array_key_exists($name, $forced)) {
1461              return (string)$forced[$name];
1462          } else if ($name === 'siteidentifier' && $plugin == 'core') {
1463              return $siteidentifier;
1464          }
1465      }
1466  
1467      $cache = cache::make('core', 'config');
1468      $result = $cache->get($plugin);
1469      if ($result === false) {
1470          // The user is after a recordset.
1471          if (!$iscore) {
1472              $result = $DB->get_records_menu('config_plugins', array('plugin' => $plugin), '', 'name,value');
1473          } else {
1474              // This part is not really used any more, but anyway...
1475              $result = $DB->get_records_menu('config', array(), '', 'name,value');;
1476          }
1477          $cache->set($plugin, $result);
1478      }
1479  
1480      if (!empty($name)) {
1481          if (array_key_exists($name, $result)) {
1482              return $result[$name];
1483          }
1484          return false;
1485      }
1486  
1487      if ($plugin === 'core') {
1488          $result['siteidentifier'] = $siteidentifier;
1489      }
1490  
1491      foreach ($forced as $key => $value) {
1492          if (is_null($value) or is_array($value) or is_object($value)) {
1493              // We do not want any extra mess here, just real settings that could be saved in db.
1494              unset($result[$key]);
1495          } else {
1496              // Convert to string as if it went through the DB.
1497              $result[$key] = (string)$value;
1498          }
1499      }
1500  
1501      return (object)$result;
1502  }
1503  
1504  /**
1505   * Removes a key from global configuration.
1506   *
1507   * NOTE: this function is called from lib/db/upgrade.php
1508   *
1509   * @param string $name the key to set
1510   * @param string $plugin (optional) the plugin scope
1511   * @return boolean whether the operation succeeded.
1512   */
1513  function unset_config($name, $plugin=null) {
1514      global $CFG, $DB;
1515  
1516      if (empty($plugin)) {
1517          unset($CFG->$name);
1518          $DB->delete_records('config', array('name' => $name));
1519          cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1520      } else {
1521          $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
1522          cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1523      }
1524  
1525      return true;
1526  }
1527  
1528  /**
1529   * Remove all the config variables for a given plugin.
1530   *
1531   * NOTE: this function is called from lib/db/upgrade.php
1532   *
1533   * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice';
1534   * @return boolean whether the operation succeeded.
1535   */
1536  function unset_all_config_for_plugin($plugin) {
1537      global $DB;
1538      // Delete from the obvious config_plugins first.
1539      $DB->delete_records('config_plugins', array('plugin' => $plugin));
1540      // Next delete any suspect settings from config.
1541      $like = $DB->sql_like('name', '?', true, true, false, '|');
1542      $params = array($DB->sql_like_escape($plugin.'_', '|') . '%');
1543      $DB->delete_records_select('config', $like, $params);
1544      // Finally clear both the plugin cache and the core cache (suspect settings now removed from core).
1545      cache_helper::invalidate_by_definition('core', 'config', array(), array('core', $plugin));
1546  
1547      return true;
1548  }
1549  
1550  /**
1551   * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability.
1552   *
1553   * All users are verified if they still have the necessary capability.
1554   *
1555   * @param string $value the value of the config setting.
1556   * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor.
1557   * @param bool $includeadmins include administrators.
1558   * @return array of user objects.
1559   */
1560  function get_users_from_config($value, $capability, $includeadmins = true) {
1561      if (empty($value) or $value === '$@NONE@$') {
1562          return array();
1563      }
1564  
1565      // We have to make sure that users still have the necessary capability,
1566      // it should be faster to fetch them all first and then test if they are present
1567      // instead of validating them one-by-one.
1568      $users = get_users_by_capability(context_system::instance(), $capability);
1569      if ($includeadmins) {
1570          $admins = get_admins();
1571          foreach ($admins as $admin) {
1572              $users[$admin->id] = $admin;
1573          }
1574      }
1575  
1576      if ($value === '$@ALL@$') {
1577          return $users;
1578      }
1579  
1580      $result = array(); // Result in correct order.
1581      $allowed = explode(',', $value);
1582      foreach ($allowed as $uid) {
1583          if (isset($users[$uid])) {
1584              $user = $users[$uid];
1585              $result[$user->id] = $user;
1586          }
1587      }
1588  
1589      return $result;
1590  }
1591  
1592  
1593  /**
1594   * Invalidates browser caches and cached data in temp.
1595   *
1596   * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
1597   * {@link phpunit_util::reset_dataroot()}
1598   *
1599   * @return void
1600   */
1601  function purge_all_caches() {
1602      global $CFG, $DB;
1603  
1604      reset_text_filters_cache();
1605      js_reset_all_caches();
1606      theme_reset_all_caches();
1607      get_string_manager()->reset_caches();
1608      core_text::reset_caches();
1609      if (class_exists('core_plugin_manager')) {
1610          core_plugin_manager::reset_caches();
1611      }
1612  
1613      // Bump up cacherev field for all courses.
1614      try {
1615          increment_revision_number('course', 'cacherev', '');
1616      } catch (moodle_exception $e) {
1617          // Ignore exception since this function is also called before upgrade script when field course.cacherev does not exist yet.
1618      }
1619  
1620      $DB->reset_caches();
1621      cache_helper::purge_all();
1622  
1623      // Purge all other caches: rss, simplepie, etc.
1624      remove_dir($CFG->cachedir.'', true);
1625  
1626      // Make sure cache dir is writable, throws exception if not.
1627      make_cache_directory('');
1628  
1629      // This is the only place where we purge local caches, we are only adding files there.
1630      // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes.
1631      remove_dir($CFG->localcachedir, true);
1632      set_config('localcachedirpurged', time());
1633      make_localcache_directory('', true);
1634      \core\task\manager::clear_static_caches();
1635  }
1636  
1637  /**
1638   * Get volatile flags
1639   *
1640   * @param string $type
1641   * @param int $changedsince default null
1642   * @return array records array
1643   */
1644  function get_cache_flags($type, $changedsince = null) {
1645      global $DB;
1646  
1647      $params = array('type' => $type, 'expiry' => time());
1648      $sqlwhere = "flagtype = :type AND expiry >= :expiry";
1649      if ($changedsince !== null) {
1650          $params['changedsince'] = $changedsince;
1651          $sqlwhere .= " AND timemodified > :changedsince";
1652      }
1653      $cf = array();
1654      if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) {
1655          foreach ($flags as $flag) {
1656              $cf[$flag->name] = $flag->value;
1657          }
1658      }
1659      return $cf;
1660  }
1661  
1662  /**
1663   * Get volatile flags
1664   *
1665   * @param string $type
1666   * @param string $name
1667   * @param int $changedsince default null
1668   * @return string|false The cache flag value or false
1669   */
1670  function get_cache_flag($type, $name, $changedsince=null) {
1671      global $DB;
1672  
1673      $params = array('type' => $type, 'name' => $name, 'expiry' => time());
1674  
1675      $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry";
1676      if ($changedsince !== null) {
1677          $params['changedsince'] = $changedsince;
1678          $sqlwhere .= " AND timemodified > :changedsince";
1679      }
1680  
1681      return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params);
1682  }
1683  
1684  /**
1685   * Set a volatile flag
1686   *
1687   * @param string $type the "type" namespace for the key
1688   * @param string $name the key to set
1689   * @param string $value the value to set (without magic quotes) - null will remove the flag
1690   * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs
1691   * @return bool Always returns true
1692   */
1693  function set_cache_flag($type, $name, $value, $expiry = null) {
1694      global $DB;
1695  
1696      $timemodified = time();
1697      if ($expiry === null || $expiry < $timemodified) {
1698          $expiry = $timemodified + 24 * 60 * 60;
1699      } else {
1700          $expiry = (int)$expiry;
1701      }
1702  
1703      if ($value === null) {
1704          unset_cache_flag($type, $name);
1705          return true;
1706      }
1707  
1708      if ($f = $DB->get_record('cache_flags', array('name' => $name, 'flagtype' => $type), '*', IGNORE_MULTIPLE)) {
1709          // This is a potential problem in DEBUG_DEVELOPER.
1710          if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) {
1711              return true; // No need to update.
1712          }
1713          $f->value        = $value;
1714          $f->expiry       = $expiry;
1715          $f->timemodified = $timemodified;
1716          $DB->update_record('cache_flags', $f);
1717      } else {
1718          $f = new stdClass();
1719          $f->flagtype     = $type;
1720          $f->name         = $name;
1721          $f->value        = $value;
1722          $f->expiry       = $expiry;
1723          $f->timemodified = $timemodified;
1724          $DB->insert_record('cache_flags', $f);
1725      }
1726      return true;
1727  }
1728  
1729  /**
1730   * Removes a single volatile flag
1731   *
1732   * @param string $type the "type" namespace for the key
1733   * @param string $name the key to set
1734   * @return bool
1735   */
1736  function unset_cache_flag($type, $name) {
1737      global $DB;
1738      $DB->delete_records('cache_flags', array('name' => $name, 'flagtype' => $type));
1739      return true;
1740  }
1741  
1742  /**
1743   * Garbage-collect volatile flags
1744   *
1745   * @return bool Always returns true
1746   */
1747  function gc_cache_flags() {
1748      global $DB;
1749      $DB->delete_records_select('cache_flags', 'expiry < ?', array(time()));
1750      return true;
1751  }
1752  
1753  // USER PREFERENCE API.
1754  
1755  /**
1756   * Refresh user preference cache. This is used most often for $USER
1757   * object that is stored in session, but it also helps with performance in cron script.
1758   *
1759   * Preferences for each user are loaded on first use on every page, then again after the timeout expires.
1760   *
1761   * @package  core
1762   * @category preference
1763   * @access   public
1764   * @param    stdClass         $user          User object. Preferences are preloaded into 'preference' property
1765   * @param    int              $cachelifetime Cache life time on the current page (in seconds)
1766   * @throws   coding_exception
1767   * @return   null
1768   */
1769  function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120) {
1770      global $DB;
1771      // Static cache, we need to check on each page load, not only every 2 minutes.
1772      static $loadedusers = array();
1773  
1774      if (!isset($user->id)) {
1775          throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field');
1776      }
1777  
1778      if (empty($user->id) or isguestuser($user->id)) {
1779          // No permanent storage for not-logged-in users and guest.
1780          if (!isset($user->preference)) {
1781              $user->preference = array();
1782          }
1783          return;
1784      }
1785  
1786      $timenow = time();
1787  
1788      if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) {
1789          // Already loaded at least once on this page. Are we up to date?
1790          if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) {
1791              // No need to reload - we are on the same page and we loaded prefs just a moment ago.
1792              return;
1793  
1794          } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) {
1795              // No change since the lastcheck on this page.
1796              $user->preference['_lastloaded'] = $timenow;
1797              return;
1798          }
1799      }
1800  
1801      // OK, so we have to reload all preferences.
1802      $loadedusers[$user->id] = true;
1803      $user->preference = $DB->get_records_menu('user_preferences', array('userid' => $user->id), '', 'name,value'); // All values.
1804      $user->preference['_lastloaded'] = $timenow;
1805  }
1806  
1807  /**
1808   * Called from set/unset_user_preferences, so that the prefs can be correctly reloaded in different sessions.
1809   *
1810   * NOTE: internal function, do not call from other code.
1811   *
1812   * @package core
1813   * @access private
1814   * @param integer $userid the user whose prefs were changed.
1815   */
1816  function mark_user_preferences_changed($userid) {
1817      global $CFG;
1818  
1819      if (empty($userid) or isguestuser($userid)) {
1820          // No cache flags for guest and not-logged-in users.
1821          return;
1822      }
1823  
1824      set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout);
1825  }
1826  
1827  /**
1828   * Sets a preference for the specified user.
1829   *
1830   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1831   *
1832   * @package  core
1833   * @category preference
1834   * @access   public
1835   * @param    string            $name  The key to set as preference for the specified user
1836   * @param    string            $value The value to set for the $name key in the specified user's
1837   *                                    record, null means delete current value.
1838   * @param    stdClass|int|null $user  A moodle user object or id, null means current user
1839   * @throws   coding_exception
1840   * @return   bool                     Always true or exception
1841   */
1842  function set_user_preference($name, $value, $user = null) {
1843      global $USER, $DB;
1844  
1845      if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1846          throw new coding_exception('Invalid preference name in set_user_preference() call');
1847      }
1848  
1849      if (is_null($value)) {
1850          // Null means delete current.
1851          return unset_user_preference($name, $user);
1852      } else if (is_object($value)) {
1853          throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed');
1854      } else if (is_array($value)) {
1855          throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed');
1856      }
1857      // Value column maximum length is 1333 characters.
1858      $value = (string)$value;
1859      if (core_text::strlen($value) > 1333) {
1860          throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column');
1861      }
1862  
1863      if (is_null($user)) {
1864          $user = $USER;
1865      } else if (isset($user->id)) {
1866          // It is a valid object.
1867      } else if (is_numeric($user)) {
1868          $user = (object)array('id' => (int)$user);
1869      } else {
1870          throw new coding_exception('Invalid $user parameter in set_user_preference() call');
1871      }
1872  
1873      check_user_preferences_loaded($user);
1874  
1875      if (empty($user->id) or isguestuser($user->id)) {
1876          // No permanent storage for not-logged-in users and guest.
1877          $user->preference[$name] = $value;
1878          return true;
1879      }
1880  
1881      if ($preference = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => $name))) {
1882          if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) {
1883              // Preference already set to this value.
1884              return true;
1885          }
1886          $DB->set_field('user_preferences', 'value', $value, array('id' => $preference->id));
1887  
1888      } else {
1889          $preference = new stdClass();
1890          $preference->userid = $user->id;
1891          $preference->name   = $name;
1892          $preference->value  = $value;
1893          $DB->insert_record('user_preferences', $preference);
1894      }
1895  
1896      // Update value in cache.
1897      $user->preference[$name] = $value;
1898  
1899      // Set reload flag for other sessions.
1900      mark_user_preferences_changed($user->id);
1901  
1902      return true;
1903  }
1904  
1905  /**
1906   * Sets a whole array of preferences for the current user
1907   *
1908   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1909   *
1910   * @package  core
1911   * @category preference
1912   * @access   public
1913   * @param    array             $prefarray An array of key/value pairs to be set
1914   * @param    stdClass|int|null $user      A moodle user object or id, null means current user
1915   * @return   bool                         Always true or exception
1916   */
1917  function set_user_preferences(array $prefarray, $user = null) {
1918      foreach ($prefarray as $name => $value) {
1919          set_user_preference($name, $value, $user);
1920      }
1921      return true;
1922  }
1923  
1924  /**
1925   * Unsets a preference completely by deleting it from the database
1926   *
1927   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1928   *
1929   * @package  core
1930   * @category preference
1931   * @access   public
1932   * @param    string            $name The key to unset as preference for the specified user
1933   * @param    stdClass|int|null $user A moodle user object or id, null means current user
1934   * @throws   coding_exception
1935   * @return   bool                    Always true or exception
1936   */
1937  function unset_user_preference($name, $user = null) {
1938      global $USER, $DB;
1939  
1940      if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1941          throw new coding_exception('Invalid preference name in unset_user_preference() call');
1942      }
1943  
1944      if (is_null($user)) {
1945          $user = $USER;
1946      } else if (isset($user->id)) {
1947          // It is a valid object.
1948      } else if (is_numeric($user)) {
1949          $user = (object)array('id' => (int)$user);
1950      } else {
1951          throw new coding_exception('Invalid $user parameter in unset_user_preference() call');
1952      }
1953  
1954      check_user_preferences_loaded($user);
1955  
1956      if (empty($user->id) or isguestuser($user->id)) {
1957          // No permanent storage for not-logged-in user and guest.
1958          unset($user->preference[$name]);
1959          return true;
1960      }
1961  
1962      // Delete from DB.
1963      $DB->delete_records('user_preferences', array('userid' => $user->id, 'name' => $name));
1964  
1965      // Delete the preference from cache.
1966      unset($user->preference[$name]);
1967  
1968      // Set reload flag for other sessions.
1969      mark_user_preferences_changed($user->id);
1970  
1971      return true;
1972  }
1973  
1974  /**
1975   * Used to fetch user preference(s)
1976   *
1977   * If no arguments are supplied this function will return
1978   * all of the current user preferences as an array.
1979   *
1980   * If a name is specified then this function
1981   * attempts to return that particular preference value.  If
1982   * none is found, then the optional value $default is returned,
1983   * otherwise null.
1984   *
1985   * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1986   *
1987   * @package  core
1988   * @category preference
1989   * @access   public
1990   * @param    string            $name    Name of the key to use in finding a preference value
1991   * @param    mixed|null        $default Value to be returned if the $name key is not set in the user preferences
1992   * @param    stdClass|int|null $user    A moodle user object or id, null means current user
1993   * @throws   coding_exception
1994   * @return   string|mixed|null          A string containing the value of a single preference. An
1995   *                                      array with all of the preferences or null
1996   */
1997  function get_user_preferences($name = null, $default = null, $user = null) {
1998      global $USER;
1999  
2000      if (is_null($name)) {
2001          // All prefs.
2002      } else if (is_numeric($name) or $name === '_lastloaded') {
2003          throw new coding_exception('Invalid preference name in get_user_preferences() call');
2004      }
2005  
2006      if (is_null($user)) {
2007          $user = $USER;
2008      } else if (isset($user->id)) {
2009          // Is a valid object.
2010      } else if (is_numeric($user)) {
2011          $user = (object)array('id' => (int)$user);
2012      } else {
2013          throw new coding_exception('Invalid $user parameter in get_user_preferences() call');
2014      }
2015  
2016      check_user_preferences_loaded($user);
2017  
2018      if (empty($name)) {
2019          // All values.
2020          return $user->preference;
2021      } else if (isset($user->preference[$name])) {
2022          // The single string value.
2023          return $user->preference[$name];
2024      } else {
2025          // Default value (null if not specified).
2026          return $default;
2027      }
2028  }
2029  
2030  // FUNCTIONS FOR HANDLING TIME.
2031  
2032  /**
2033   * Given Gregorian date parts in user time produce a GMT timestamp.
2034   *
2035   * @package core
2036   * @category time
2037   * @param int $year The year part to create timestamp of
2038   * @param int $month The month part to create timestamp of
2039   * @param int $day The day part to create timestamp of
2040   * @param int $hour The hour part to create timestamp of
2041   * @param int $minute The minute part to create timestamp of
2042   * @param int $second The second part to create timestamp of
2043   * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset.
2044   *             if 99 then default user's timezone is used {@link http://docs.moodle.org/dev/Time_API#Timezone}
2045   * @param bool $applydst Toggle Daylight Saving Time, default true, will be
2046   *             applied only if timezone is 99 or string.
2047   * @return int GMT timestamp
2048   */
2049  function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) {
2050      $date = new DateTime('now', core_date::get_user_timezone_object($timezone));
2051      $date->setDate((int)$year, (int)$month, (int)$day);
2052      $date->setTime((int)$hour, (int)$minute, (int)$second);
2053  
2054      $time = $date->getTimestamp();
2055  
2056      // Moodle BC DST stuff.
2057      if (!$applydst) {
2058          $time += dst_offset_on($time, $timezone);
2059      }
2060  
2061      return $time;
2062  
2063  }
2064  
2065  /**
2066   * Format a date/time (seconds) as weeks, days, hours etc as needed
2067   *
2068   * Given an amount of time in seconds, returns string
2069   * formatted nicely as weeks, days, hours etc as needed
2070   *
2071   * @package core
2072   * @category time
2073   * @uses MINSECS
2074   * @uses HOURSECS
2075   * @uses DAYSECS
2076   * @uses YEARSECS
2077   * @param int $totalsecs Time in seconds
2078   * @param stdClass $str Should be a time object
2079   * @return string A nicely formatted date/time string
2080   */
2081  function format_time($totalsecs, $str = null) {
2082  
2083      $totalsecs = abs($totalsecs);
2084  
2085      if (!$str) {
2086          // Create the str structure the slow way.
2087          $str = new stdClass();
2088          $str->day   = get_string('day');
2089          $str->days  = get_string('days');
2090          $str->hour  = get_string('hour');
2091          $str->hours = get_string('hours');
2092          $str->min   = get_string('min');
2093          $str->mins  = get_string('mins');
2094          $str->sec   = get_string('sec');
2095          $str->secs  = get_string('secs');
2096          $str->year  = get_string('year');
2097          $str->years = get_string('years');
2098      }
2099  
2100      $years     = floor($totalsecs/YEARSECS);
2101      $remainder = $totalsecs - ($years*YEARSECS);
2102      $days      = floor($remainder/DAYSECS);
2103      $remainder = $totalsecs - ($days*DAYSECS);
2104      $hours     = floor($remainder/HOURSECS);
2105      $remainder = $remainder - ($hours*HOURSECS);
2106      $mins      = floor($remainder/MINSECS);
2107      $secs      = $remainder - ($mins*MINSECS);
2108  
2109      $ss = ($secs == 1)  ? $str->sec  : $str->secs;
2110      $sm = ($mins == 1)  ? $str->min  : $str->mins;
2111      $sh = ($hours == 1) ? $str->hour : $str->hours;
2112      $sd = ($days == 1)  ? $str->day  : $str->days;
2113      $sy = ($years == 1)  ? $str->year  : $str->years;
2114  
2115      $oyears = '';
2116      $odays = '';
2117      $ohours = '';
2118      $omins = '';
2119      $osecs = '';
2120  
2121      if ($years) {
2122          $oyears  = $years .' '. $sy;
2123      }
2124      if ($days) {
2125          $odays  = $days .' '. $sd;
2126      }
2127      if ($hours) {
2128          $ohours = $hours .' '. $sh;
2129      }
2130      if ($mins) {
2131          $omins  = $mins .' '. $sm;
2132      }
2133      if ($secs) {
2134          $osecs  = $secs .' '. $ss;
2135      }
2136  
2137      if ($years) {
2138          return trim($oyears .' '. $odays);
2139      }
2140      if ($days) {
2141          return trim($odays .' '. $ohours);
2142      }
2143      if ($hours) {
2144          return trim($ohours .' '. $omins);
2145      }
2146      if ($mins) {
2147          return trim($omins .' '. $osecs);
2148      }
2149      if ($secs) {
2150          return $osecs;
2151      }
2152      return get_string('now');
2153  }
2154  
2155  /**
2156   * Returns a formatted string that represents a date in user time.
2157   *
2158   * @package core
2159   * @category time
2160   * @param int $date the timestamp in UTC, as obtained from the database.
2161   * @param string $format strftime format. You should probably get this using
2162   *        get_string('strftime...', 'langconfig');
2163   * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
2164   *        not 99 then daylight saving will not be added.
2165   *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
2166   * @param bool $fixday If true (default) then the leading zero from %d is removed.
2167   *        If false then the leading zero is maintained.
2168   * @param bool $fixhour If true (default) then the leading zero from %I is removed.
2169   * @return string the formatted date/time.
2170   */
2171  function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
2172      $calendartype = \core_calendar\type_factory::get_calendar_instance();
2173      return $calendartype->timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour);
2174  }
2175  
2176  /**
2177   * Returns a formatted date ensuring it is UTF-8.
2178   *
2179   * If we are running under Windows convert to Windows encoding and then back to UTF-8
2180   * (because it's impossible to specify UTF-8 to fetch locale info in Win32).
2181   *
2182   * @param int $date the timestamp - since Moodle 2.9 this is a real UTC timestamp
2183   * @param string $format strftime format.
2184   * @param int|float|string $tz the user timezone
2185   * @return string the formatted date/time.
2186   * @since Moodle 2.3.3
2187   */
2188  function date_format_string($date, $format, $tz = 99) {
2189      global $CFG;
2190  
2191      $localewincharset = null;
2192      // Get the calendar type user is using.
2193      if ($CFG->ostype == 'WINDOWS') {
2194          $calendartype = \core_calendar\type_factory::get_calendar_instance();
2195          $localewincharset = $calendartype->locale_win_charset();
2196      }
2197  
2198      if ($localewincharset) {
2199          $format = core_text::convert($format, 'utf-8', $localewincharset);
2200      }
2201  
2202      date_default_timezone_set(core_date::get_user_timezone($tz));
2203      $datestring = strftime($format, $date);
2204      core_date::set_default_server_timezone();
2205  
2206      if ($localewincharset) {
2207          $datestring = core_text::convert($datestring, $localewincharset, 'utf-8');
2208      }
2209  
2210      return $datestring;
2211  }
2212  
2213  /**
2214   * Given a $time timestamp in GMT (seconds since epoch),
2215   * returns an array that represents the Gregorian date in user time
2216   *
2217   * @package core
2218   * @category time
2219   * @param int $time Timestamp in GMT
2220   * @param float|int|string $timezone user timezone
2221   * @return array An array that represents the date in user time
2222   */
2223  function usergetdate($time, $timezone=99) {
2224      date_default_timezone_set(core_date::get_user_timezone($timezone));
2225      $result = getdate($time);
2226      core_date::set_default_server_timezone();
2227  
2228      return $result;
2229  }
2230  
2231  /**
2232   * Given a GMT timestamp (seconds since epoch), offsets it by
2233   * the timezone.  eg 3pm in India is 3pm GMT - 7 * 3600 seconds
2234   *
2235   * NOTE: this function does not include DST properly,
2236   *       you should use the PHP date stuff instead!
2237   *
2238   * @package core
2239   * @category time
2240   * @param int $date Timestamp in GMT
2241   * @param float|int|string $timezone user timezone
2242   * @return int
2243   */
2244  function usertime($date, $timezone=99) {
2245      $userdate = new DateTime('@' . $date);
2246      $userdate->setTimezone(core_date::get_user_timezone_object($timezone));
2247      $dst = dst_offset_on($date, $timezone);
2248  
2249      return $date - $userdate->getOffset() + $dst;
2250  }
2251  
2252  /**
2253   * Given a time, return the GMT timestamp of the most recent midnight
2254   * for the current user.
2255   *
2256   * @package core
2257   * @category time
2258   * @param int $date Timestamp in GMT
2259   * @param float|int|string $timezone user timezone
2260   * @return int Returns a GMT timestamp
2261   */
2262  function usergetmidnight($date, $timezone=99) {
2263  
2264      $userdate = usergetdate($date, $timezone);
2265  
2266      // Time of midnight of this user's day, in GMT.
2267      return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone);
2268  
2269  }
2270  
2271  /**
2272   * Returns a string that prints the user's timezone
2273   *
2274   * @package core
2275   * @category time
2276   * @param float|int|string $timezone user timezone
2277   * @return string
2278   */
2279  function usertimezone($timezone=99) {
2280      $tz = core_date::get_user_timezone($timezone);
2281      return core_date::get_localised_timezone($tz);
2282  }
2283  
2284  /**
2285   * Returns a float or a string which denotes the user's timezone
2286   * 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)
2287   * means that for this timezone there are also DST rules to be taken into account
2288   * Checks various settings and picks the most dominant of those which have a value
2289   *
2290   * @package core
2291   * @category time
2292   * @param float|int|string $tz timezone to calculate GMT time offset before
2293   *        calculating user timezone, 99 is default user timezone
2294   *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
2295   * @return float|string
2296   */
2297  function get_user_timezone($tz = 99) {
2298      global $USER, $CFG;
2299  
2300      $timezones = array(
2301          $tz,
2302          isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99,
2303          isset($USER->timezone) ? $USER->timezone : 99,
2304          isset($CFG->timezone) ? $CFG->timezone : 99,
2305          );
2306  
2307      $tz = 99;
2308  
2309      // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array.
2310      while (((empty($tz) && !is_numeric($tz)) || $tz == 99) && $next = each($timezones)) {
2311          $tz = $next['value'];
2312      }
2313      return is_numeric($tz) ? (float) $tz : $tz;
2314  }
2315  
2316  /**
2317   * Calculates the Daylight Saving Offset for a given date/time (timestamp)
2318   * - Note: Daylight saving only works for string timezones and not for float.
2319   *
2320   * @package core
2321   * @category time
2322   * @param int $time must NOT be compensated at all, it has to be a pure timestamp
2323   * @param int|float|string $strtimezone user timezone
2324   * @return int
2325   */
2326  function dst_offset_on($time, $strtimezone = null) {
2327      $tz = core_date::get_user_timezone($strtimezone);
2328      $date = new DateTime('@' . $time);
2329      $date->setTimezone(new DateTimeZone($tz));
2330      if ($date->format('I') == '1') {
2331          if ($tz === 'Australia/Lord_Howe') {
2332              return 1800;
2333          }
2334          return 3600;
2335      }
2336      return 0;
2337  }
2338  
2339  /**
2340   * Calculates when the day appears in specific month
2341   *
2342   * @package core
2343   * @category time
2344   * @param int $startday starting day of the month
2345   * @param int $weekday The day when week starts (normally taken from user preferences)
2346   * @param int $month The month whose day is sought
2347   * @param int $year The year of the month whose day is sought
2348   * @return int
2349   */
2350  function find_day_in_month($startday, $weekday, $month, $year) {
2351      $calendartype = \core_calendar\type_factory::get_calendar_instance();
2352  
2353      $daysinmonth = days_in_month($month, $year);
2354      $daysinweek = count($calendartype->get_weekdays());
2355  
2356      if ($weekday == -1) {
2357          // Don't care about weekday, so return:
2358          //    abs($startday) if $startday != -1
2359          //    $daysinmonth otherwise.
2360          return ($startday == -1) ? $daysinmonth : abs($startday);
2361      }
2362  
2363      // From now on we 're looking for a specific weekday.
2364      // Give "end of month" its actual value, since we know it.
2365      if ($startday == -1) {
2366          $startday = -1 * $daysinmonth;
2367      }
2368  
2369      // Starting from day $startday, the sign is the direction.
2370      if ($startday < 1) {
2371          $startday = abs($startday);
2372          $lastmonthweekday = dayofweek($daysinmonth, $month, $year);
2373  
2374          // This is the last such weekday of the month.
2375          $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday;
2376          if ($lastinmonth > $daysinmonth) {
2377              $lastinmonth -= $daysinweek;
2378          }
2379  
2380          // Find the first such weekday <= $startday.
2381          while ($lastinmonth > $startday) {
2382              $lastinmonth -= $daysinweek;
2383          }
2384  
2385          return $lastinmonth;
2386      } else {
2387          $indexweekday = dayofweek($startday, $month, $year);
2388  
2389          $diff = $weekday - $indexweekday;
2390          if ($diff < 0) {
2391              $diff += $daysinweek;
2392          }
2393  
2394          // This is the first such weekday of the month equal to or after $startday.
2395          $firstfromindex = $startday + $diff;
2396  
2397          return $firstfromindex;
2398      }
2399  }
2400  
2401  /**
2402   * Calculate the number of days in a given month
2403   *
2404   * @package core
2405   * @category time
2406   * @param int $month The month whose day count is sought
2407   * @param int $year The year of the month whose day count is sought
2408   * @return int
2409   */
2410  function days_in_month($month, $year) {
2411      $calendartype = \core_calendar\type_factory::get_calendar_instance();
2412      return $calendartype->get_num_days_in_month($year, $month);
2413  }
2414  
2415  /**
2416   * Calculate the position in the week of a specific calendar day
2417   *
2418   * @package core
2419   * @category time
2420   * @param int $day The day of the date whose position in the week is sought
2421   * @param int $month The month of the date whose position in the week is sought
2422   * @param int $year The year of the date whose position in the week is sought
2423   * @return int
2424   */
2425  function dayofweek($day, $month, $year) {
2426      $calendartype = \core_calendar\type_factory::get_calendar_instance();
2427      return $calendartype->get_weekday($year, $month, $day);
2428  }
2429  
2430  // USER AUTHENTICATION AND LOGIN.
2431  
2432  /**
2433   * Returns full login url.
2434   *
2435   * @return string login url
2436   */
2437  function get_login_url() {
2438      global $CFG;
2439  
2440      $url = "$CFG->wwwroot/login/index.php";
2441  
2442      if (!empty($CFG->loginhttps)) {
2443          $url = str_replace('http:', 'https:', $url);
2444      }
2445  
2446      return $url;
2447  }
2448  
2449  /**
2450   * This function checks that the current user is logged in and has the
2451   * required privileges
2452   *
2453   * This function checks that the current user is logged in, and optionally
2454   * whether they are allowed to be in a particular course and view a particular
2455   * course module.
2456   * If they are not logged in, then it redirects them to the site login unless
2457   * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which
2458   * case they are automatically logged in as guests.
2459   * If $courseid is given and the user is not enrolled in that course then the
2460   * user is redirected to the course enrolment page.
2461   * If $cm is given and the course module is hidden and the user is not a teacher
2462   * in the course then the user is redirected to the course home page.
2463   *
2464   * When $cm parameter specified, this function sets page layout to 'module'.
2465   * You need to change it manually later if some other layout needed.
2466   *
2467   * @package    core_access
2468   * @category   access
2469   *
2470   * @param mixed $courseorid id of the course or course object
2471   * @param bool $autologinguest default true
2472   * @param object $cm course module object
2473   * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2474   *             true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2475   *             in order to keep redirects working properly. MDL-14495
2476   * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2477   * @return mixed Void, exit, and die depending on path
2478   * @throws coding_exception
2479   * @throws require_login_exception
2480   */
2481  function require_login($courseorid = null, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
2482      global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT;
2483  
2484      // Must not redirect when byteserving already started.
2485      if (!empty($_SERVER['HTTP_RANGE'])) {
2486          $preventredirect = true;
2487      }
2488  
2489      if (AJAX_SCRIPT) {
2490          // We cannot redirect for AJAX scripts either.
2491          $preventredirect = true;
2492      }
2493  
2494      // Setup global $COURSE, themes, language and locale.
2495      if (!empty($courseorid)) {
2496          if (is_object($courseorid)) {
2497              $course = $courseorid;
2498          } else if ($courseorid == SITEID) {
2499              $course = clone($SITE);
2500          } else {
2501              $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST);
2502          }
2503          if ($cm) {
2504              if ($cm->course != $course->id) {
2505                  throw new coding_exception('course and cm parameters in require_login() call do not match!!');
2506              }
2507              // Make sure we have a $cm from get_fast_modinfo as this contains activity access details.
2508              if (!($cm instanceof cm_info)) {
2509                  // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
2510                  // db queries so this is not really a performance concern, however it is obviously
2511                  // better if you use get_fast_modinfo to get the cm before calling this.
2512                  $modinfo = get_fast_modinfo($course);
2513                  $cm = $modinfo->get_cm($cm->id);
2514              }
2515          }
2516      } else {
2517          // Do not touch global $COURSE via $PAGE->set_course(),
2518          // the reasons is we need to be able to call require_login() at any time!!
2519          $course = $SITE;
2520          if ($cm) {
2521              throw new coding_exception('cm parameter in require_login() requires valid course parameter!');
2522          }
2523      }
2524  
2525      // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false.
2526      // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future
2527      // risk leading the user back to the AJAX request URL.
2528      if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) {
2529          $setwantsurltome = false;
2530      }
2531  
2532      // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour.
2533      if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !empty($CFG->dbsessions)) {
2534          if ($preventredirect) {
2535              throw new require_login_session_timeout_exception();
2536          } else {
2537              if ($setwantsurltome) {
2538                  $SESSION->wantsurl = qualified_me();
2539              }
2540              redirect(get_login_url());
2541          }
2542      }
2543  
2544      // If the user is not even logged in yet then make sure they are.
2545      if (!isloggedin()) {
2546          if ($autologinguest and !empty($CFG->guestloginbutton) and !empty($CFG->autologinguests)) {
2547              if (!$guest = get_complete_user_data('id', $CFG->siteguest)) {
2548                  // Misconfigured site guest, just redirect to login page.
2549                  redirect(get_login_url());
2550                  exit; // Never reached.
2551              }
2552              $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang;
2553              complete_user_login($guest);
2554              $USER->autologinguest = true;
2555              $SESSION->lang = $lang;
2556          } else {
2557              // NOTE: $USER->site check was obsoleted by session test cookie, $USER->confirmed test is in login/index.php.
2558              if ($preventredirect) {
2559                  throw new require_login_exception('You are not logged in');
2560              }
2561  
2562              if ($setwantsurltome) {
2563                  $SESSION->wantsurl = qualified_me();
2564              }
2565  
2566              $referer = get_local_referer(false);
2567              if (!empty($referer)) {
2568                  $SESSION->fromurl = $referer;
2569              }
2570  
2571              // Give auth plugins an opportunity to authenticate or redirect to an external login page
2572              $authsequence = get_enabled_auth_plugins(true); // auths, in sequence
2573              foreach($authsequence as $authname) {
2574                  $authplugin = get_auth_plugin($authname);
2575                  $authplugin->pre_loginpage_hook();
2576                  if (isloggedin()) {
2577                      break;
2578                  }
2579              }
2580  
2581              // If we're still not logged in then go to the login page
2582              if (!isloggedin()) {
2583                  redirect(get_login_url());
2584                  exit; // Never reached.
2585              }
2586          }
2587      }
2588  
2589      // Loginas as redirection if needed.
2590      if ($course->id != SITEID and \core\session\manager::is_loggedinas()) {
2591          if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) {
2592              if ($USER->loginascontext->instanceid != $course->id) {
2593                  print_error('loginasonecourse', '', $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid);
2594              }
2595          }
2596      }
2597  
2598      // Check whether the user should be changing password (but only if it is REALLY them).
2599      if (get_user_preferences('auth_forcepasswordchange') && !\core\session\manager::is_loggedinas()) {
2600          $userauth = get_auth_plugin($USER->auth);
2601          if ($userauth->can_change_password() and !$preventredirect) {
2602              if ($setwantsurltome) {
2603                  $SESSION->wantsurl = qualified_me();
2604              }
2605              if ($changeurl = $userauth->change_password_url()) {
2606                  // Use plugin custom url.
2607                  redirect($changeurl);
2608              } else {
2609                  // Use moodle internal method.
2610                  if (empty($CFG->loginhttps)) {
2611                      redirect($CFG->wwwroot .'/login/change_password.php');
2612                  } else {
2613                      $wwwroot = str_replace('http:', 'https:', $CFG->wwwroot);
2614                      redirect($wwwroot .'/login/change_password.php');
2615                  }
2616              }
2617          } else {
2618              print_error('nopasswordchangeforced', 'auth');
2619          }
2620      }
2621  
2622      // Check that the user account is properly set up.
2623      if (user_not_fully_set_up($USER)) {
2624          if ($preventredirect) {
2625              throw new require_login_exception('User not fully set-up');
2626          }
2627          if ($setwantsurltome) {
2628              $SESSION->wantsurl = qualified_me();
2629          }
2630          redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&amp;course='. SITEID);
2631      }
2632  
2633      // Make sure the USER has a sesskey set up. Used for CSRF protection.
2634      sesskey();
2635  
2636      // Do not bother admins with any formalities.
2637      if (is_siteadmin()) {
2638          // Set the global $COURSE.
2639          if ($cm) {
2640              $PAGE->set_cm($cm, $course);
2641              $PAGE->set_pagelayout('incourse');
2642          } else if (!empty($courseorid)) {
2643              $PAGE->set_course($course);
2644          }
2645          // Set accesstime or the user will appear offline which messes up messaging.
2646          user_accesstime_log($course->id);
2647          return;
2648      }
2649  
2650      // Check that the user has agreed to a site policy if there is one - do not test in case of admins.
2651      if (!$USER->policyagreed and !is_siteadmin()) {
2652          if (!empty($CFG->sitepolicy) and !isguestuser()) {
2653              if ($preventredirect) {
2654                  throw new require_login_exception('Policy not agreed');
2655              }
2656              if ($setwantsurltome) {
2657                  $SESSION->wantsurl = qualified_me();
2658              }
2659              redirect($CFG->wwwroot .'/user/policy.php');
2660          } else if (!empty($CFG->sitepolicyguest) and isguestuser()) {
2661              if ($preventredirect) {
2662                  throw new require_login_exception('Policy not agreed');
2663              }
2664              if ($setwantsurltome) {
2665                  $SESSION->wantsurl = qualified_me();
2666              }
2667              redirect($CFG->wwwroot .'/user/policy.php');
2668          }
2669      }
2670  
2671      // Fetch the system context, the course context, and prefetch its child contexts.
2672      $sysctx = context_system::instance();
2673      $coursecontext = context_course::instance($course->id, MUST_EXIST);
2674      if ($cm) {
2675          $cmcontext = context_module::instance($cm->id, MUST_EXIST);
2676      } else {
2677          $cmcontext = null;
2678      }
2679  
2680      // If the site is currently under maintenance, then print a message.
2681      if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:config', $sysctx)) {
2682          if ($preventredirect) {
2683              throw new require_login_exception('Maintenance in progress');
2684          }
2685  
2686          print_maintenance_message();
2687      }
2688  
2689      // Make sure the course itself is not hidden.
2690      if ($course->id == SITEID) {
2691          // Frontpage can not be hidden.
2692      } else {
2693          if (is_role_switched($course->id)) {
2694              // When switching roles ignore the hidden flag - user had to be in course to do the switch.
2695          } else {
2696              if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
2697                  // Originally there was also test of parent category visibility, BUT is was very slow in complex queries
2698                  // involving "my courses" now it is also possible to simply hide all courses user is not enrolled in :-).
2699                  if ($preventredirect) {
2700                      throw new require_login_exception('Course is hidden');
2701                  }
2702                  $PAGE->set_context(null);
2703                  // We need to override the navigation URL as the course won't have been added to the navigation and thus
2704                  // the navigation will mess up when trying to find it.
2705                  navigation_node::override_active_url(new moodle_url('/'));
2706                  notice(get_string('coursehidden'), $CFG->wwwroot .'/');
2707              }
2708          }
2709      }
2710  
2711      // Is the user enrolled?
2712      if ($course->id == SITEID) {
2713          // Everybody is enrolled on the frontpage.
2714      } else {
2715          if (\core\session\manager::is_loggedinas()) {
2716              // Make sure the REAL person can access this course first.
2717              $realuser = \core\session\manager::get_realuser();
2718              if (!is_enrolled($coursecontext, $realuser->id, '', true) and
2719                  !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) {
2720                  if ($preventredirect) {
2721                      throw new require_login_exception('Invalid course login-as access');
2722                  }
2723                  $PAGE->set_context(null);
2724                  echo $OUTPUT->header();
2725                  notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/');
2726              }
2727          }
2728  
2729          $access = false;
2730  
2731          if (is_role_switched($course->id)) {
2732              // Ok, user had to be inside this course before the switch.
2733              $access = true;
2734  
2735          } else if (is_viewing($coursecontext, $USER)) {
2736              // Ok, no need to mess with enrol.
2737              $access = true;
2738  
2739          } else {
2740              if (isset($USER->enrol['enrolled'][$course->id])) {
2741                  if ($USER->enrol['enrolled'][$course->id] > time()) {
2742                      $access = true;
2743                      if (isset($USER->enrol['tempguest'][$course->id])) {
2744                          unset($USER->enrol['tempguest'][$course->id]);
2745                          remove_temp_course_roles($coursecontext);
2746                      }
2747                  } else {
2748                      // Expired.
2749                      unset($USER->enrol['enrolled'][$course->id]);
2750                  }
2751              }
2752              if (isset($USER->enrol['tempguest'][$course->id])) {
2753                  if ($USER->enrol['tempguest'][$course->id] == 0) {
2754                      $access = true;
2755                  } else if ($USER->enrol['tempguest'][$course->id] > time()) {
2756                      $access = true;
2757                  } else {
2758                      // Expired.
2759                      unset($USER->enrol['tempguest'][$course->id]);
2760                      remove_temp_course_roles($coursecontext);
2761                  }
2762              }
2763  
2764              if (!$access) {
2765                  // Cache not ok.
2766                  $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id);
2767                  if ($until !== false) {
2768                      // Active participants may always access, a timestamp in the future, 0 (always) or false.
2769                      if ($until == 0) {
2770                          $until = ENROL_MAX_TIMESTAMP;
2771                      }
2772                      $USER->enrol['enrolled'][$course->id] = $until;
2773                      $access = true;
2774  
2775                  } else {
2776                      $params = array('courseid' => $course->id, 'status' => ENROL_INSTANCE_ENABLED);
2777                      $instances = $DB->get_records('enrol', $params, 'sortorder, id ASC');
2778                      $enrols = enrol_get_plugins(true);
2779                      // First ask all enabled enrol instances in course if they want to auto enrol user.
2780                      foreach ($instances as $instance) {
2781                          if (!isset($enrols[$instance->enrol])) {
2782                              continue;
2783                          }
2784                          // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false.
2785                          $until = $enrols[$instance->enrol]->try_autoenrol($instance);
2786                          if ($until !== false) {
2787                              if ($until == 0) {
2788                                  $until = ENROL_MAX_TIMESTAMP;
2789                              }
2790                              $USER->enrol['enrolled'][$course->id] = $until;
2791                              $access = true;
2792                              break;
2793                          }
2794                      }
2795                      // If not enrolled yet try to gain temporary guest access.
2796                      if (!$access) {
2797                          foreach ($instances as $instance) {
2798                              if (!isset($enrols[$instance->enrol])) {
2799                                  continue;
2800                              }
2801                              // Get a duration for the guest access, a timestamp in the future or false.
2802                              $until = $enrols[$instance->enrol]->try_guestaccess($instance);
2803                              if ($until !== false and $until > time()) {
2804                                  $USER->enrol['tempguest'][$course->id] = $until;
2805                                  $access = true;
2806                                  break;
2807                              }
2808                          }
2809                      }
2810                  }
2811              }
2812          }
2813  
2814          if (!$access) {
2815              if ($preventredirect) {
2816                  throw new require_login_exception('Not enrolled');
2817              }
2818              if ($setwantsurltome) {
2819                  $SESSION->wantsurl = qualified_me();
2820              }
2821              redirect($CFG->wwwroot .'/enrol/index.php?id='. $course->id);
2822          }
2823      }
2824  
2825      // Check visibility of activity to current user; includes visible flag, conditional availability, etc.
2826      if ($cm && !$cm->uservisible) {
2827          if ($preventredirect) {
2828              throw new require_login_exception('Activity is hidden');
2829          }
2830          if ($course->id != SITEID) {
2831              $url = new moodle_url('/course/view.php', array('id' => $course->id));
2832          } else {
2833              $url = new moodle_url('/');
2834          }
2835          redirect($url, get_string('activityiscurrentlyhidden'));
2836      }
2837  
2838      // Set the global $COURSE.
2839      if ($cm) {
2840          $PAGE->set_cm($cm, $course);
2841          $PAGE->set_pagelayout('incourse');
2842      } else if (!empty($courseorid)) {
2843          $PAGE->set_course($course);
2844      }
2845  
2846      // Finally access granted, update lastaccess times.
2847      user_accesstime_log($course->id);
2848  }
2849  
2850  
2851  /**
2852   * This function just makes sure a user is logged out.
2853   *
2854   * @package    core_access
2855   * @category   access
2856   */
2857  function require_logout() {
2858      global $USER, $DB;
2859  
2860      if (!isloggedin()) {
2861          // This should not happen often, no need for hooks or events here.
2862          \core\session\manager::terminate_current();
2863          return;
2864      }
2865  
2866      // Execute hooks before action.
2867      $authplugins = array();
2868      $authsequence = get_enabled_auth_plugins();
2869      foreach ($authsequence as $authname) {
2870          $authplugins[$authname] = get_auth_plugin($authname);
2871          $authplugins[$authname]->prelogout_hook();
2872      }
2873  
2874      // Store info that gets removed during logout.
2875      $sid = session_id();
2876      $event = \core\event\user_loggedout::create(
2877          array(
2878              'userid' => $USER->id,
2879              'objectid' => $USER->id,
2880              'other' => array('sessionid' => $sid),
2881          )
2882      );
2883      if ($session = $DB->get_record('sessions', array('sid'=>$sid))) {
2884          $event->add_record_snapshot('sessions', $session);
2885      }
2886  
2887      // Clone of $USER object to be used by auth plugins.
2888      $user = fullclone($USER);
2889  
2890      // Delete session record and drop $_SESSION content.
2891      \core\session\manager::terminate_current();
2892  
2893      // Trigger event AFTER action.
2894      $event->trigger();
2895  
2896      // Hook to execute auth plugins redirection after event trigger.
2897      foreach ($authplugins as $authplugin) {
2898          $authplugin->postlogout_hook($user);
2899      }
2900  }
2901  
2902  /**
2903   * Weaker version of require_login()
2904   *
2905   * This is a weaker version of {@link require_login()} which only requires login
2906   * when called from within a course rather than the site page, unless
2907   * the forcelogin option is turned on.
2908   * @see require_login()
2909   *
2910   * @package    core_access
2911   * @category   access
2912   *
2913   * @param mixed $courseorid The course object or id in question
2914   * @param bool $autologinguest Allow autologin guests if that is wanted
2915   * @param object $cm Course activity module if known
2916   * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2917   *             true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2918   *             in order to keep redirects working properly. MDL-14495
2919   * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2920   * @return void
2921   * @throws coding_exception
2922   */
2923  function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
2924      global $CFG, $PAGE, $SITE;
2925      $issite = ((is_object($courseorid) and $courseorid->id == SITEID)
2926            or (!is_object($courseorid) and $courseorid == SITEID));
2927      if ($issite && !empty($cm) && !($cm instanceof cm_info)) {
2928          // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
2929          // db queries so this is not really a performance concern, however it is obviously
2930          // better if you use get_fast_modinfo to get the cm before calling this.
2931          if (is_object($courseorid)) {
2932              $course = $courseorid;
2933          } else {
2934              $course = clone($SITE);
2935          }
2936          $modinfo = get_fast_modinfo($course);
2937          $cm = $modinfo->get_cm($cm->id);
2938      }
2939      if (!empty($CFG->forcelogin)) {
2940          // Login required for both SITE and courses.
2941          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2942  
2943      } else if ($issite && !empty($cm) and !$cm->uservisible) {
2944          // Always login for hidden activities.
2945          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2946  
2947      } else if ($issite) {
2948          // Login for SITE not required.
2949          // We still need to instatiate PAGE vars properly so that things that rely on it like navigation function correctly.
2950          if (!empty($courseorid)) {
2951              if (is_object($courseorid)) {
2952                  $course = $courseorid;
2953              } else {
2954                  $course = clone $SITE;
2955              }
2956              if ($cm) {
2957                  if ($cm->course != $course->id) {
2958                      throw new coding_exception('course and cm parameters in require_course_login() call do not match!!');
2959                  }
2960                  $PAGE->set_cm($cm, $course);
2961                  $PAGE->set_pagelayout('incourse');
2962              } else {
2963                  $PAGE->set_course($course);
2964              }
2965          } else {
2966              // If $PAGE->course, and hence $PAGE->context, have not already been set up properly, set them up now.
2967              $PAGE->set_course($PAGE->course);
2968          }
2969          user_accesstime_log(SITEID);
2970          return;
2971  
2972      } else {
2973          // Course login always required.
2974          require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2975      }
2976  }
2977  
2978  /**
2979   * Require key login. Function terminates with error if key not found or incorrect.
2980   *
2981   * @uses NO_MOODLE_COOKIES
2982   * @uses PARAM_ALPHANUM
2983   * @param string $script unique script identifier
2984   * @param int $instance optional instance id
2985   * @return int Instance ID
2986   */
2987  function require_user_key_login($script, $instance=null) {
2988      global $DB;
2989  
2990      if (!NO_MOODLE_COOKIES) {
2991          print_error('sessioncookiesdisable');
2992      }
2993  
2994      // Extra safety.
2995      \core\session\manager::write_close();
2996  
2997      $keyvalue = required_param('key', PARAM_ALPHANUM);
2998  
2999      if (!$key = $DB->get_record('user_private_key', array('script' => $script, 'value' => $keyvalue, 'instance' => $instance))) {
3000          print_error('invalidkey');
3001      }
3002  
3003      if (!empty($key->validuntil) and $key->validuntil < time()) {
3004          print_error('expiredkey');
3005      }
3006  
3007      if ($key->iprestriction) {
3008          $remoteaddr = getremoteaddr(null);
3009          if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) {
3010              print_error('ipmismatch');
3011          }
3012      }
3013  
3014      if (!$user = $DB->get_record('user', array('id' => $key->userid))) {
3015          print_error('invaliduserid');
3016      }
3017  
3018      // Emulate normal session.
3019      enrol_check_plugins($user);
3020      \core\session\manager::set_user($user);
3021  
3022      // Note we are not using normal login.
3023      if (!defined('USER_KEY_LOGIN')) {
3024          define('USER_KEY_LOGIN', true);
3025      }
3026  
3027      // Return instance id - it might be empty.
3028      return $key->instance;
3029  }
3030  
3031  /**
3032   * Creates a new private user access key.
3033   *
3034   * @param string $script unique target identifier
3035   * @param int $userid
3036   * @param int $instance optional instance id
3037   * @param string $iprestriction optional ip restricted access
3038   * @param int $validuntil key valid only until given data
3039   * @return string access key value
3040   */
3041  function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3042      global $DB;
3043  
3044      $key = new stdClass();
3045      $key->script        = $script;
3046      $key->userid        = $userid;
3047      $key->instance      = $instance;
3048      $key->iprestriction = $iprestriction;
3049      $key->validuntil    = $validuntil;
3050      $key->timecreated   = time();
3051  
3052      // Something long and unique.
3053      $key->value         = md5($userid.'_'.time().random_string(40));
3054      while ($DB->record_exists('user_private_key', array('value' => $key->value))) {
3055          // Must be unique.
3056          $key->value     = md5($userid.'_'.time().random_string(40));
3057      }
3058      $DB->insert_record('user_private_key', $key);
3059      return $key->value;
3060  }
3061  
3062  /**
3063   * Delete the user's new private user access keys for a particular script.
3064   *
3065   * @param string $script unique target identifier
3066   * @param int $userid
3067   * @return void
3068   */
3069  function delete_user_key($script, $userid) {
3070      global $DB;
3071      $DB->delete_records('user_private_key', array('script' => $script, 'userid' => $userid));
3072  }
3073  
3074  /**
3075   * Gets a private user access key (and creates one if one doesn't exist).
3076   *
3077   * @param string $script unique target identifier
3078   * @param int $userid
3079   * @param int $instance optional instance id
3080   * @param string $iprestriction optional ip restricted access
3081   * @param int $validuntil key valid only until given date
3082   * @return string access key value
3083   */
3084  function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3085      global $DB;
3086  
3087      if ($key = $DB->get_record('user_private_key', array('script' => $script, 'userid' => $userid,
3088                                                           'instance' => $instance, 'iprestriction' => $iprestriction,
3089                                                           'validuntil' => $validuntil))) {
3090          return $key->value;
3091      } else {
3092          return create_user_key($script, $userid, $instance, $iprestriction, $validuntil);
3093      }
3094  }
3095  
3096  
3097  /**
3098   * Modify the user table by setting the currently logged in user's last login to now.
3099   *
3100   * @return bool Always returns true
3101   */
3102  function update_user_login_times() {
3103      global $USER, $DB;
3104  
3105      if (isguestuser()) {
3106          // Do not update guest access times/ips for performance.
3107          return true;
3108      }
3109  
3110      $now = time();
3111  
3112      $user = new stdClass();
3113      $user->id = $USER->id;
3114  
3115      // Make sure all users that logged in have some firstaccess.
3116      if ($USER->firstaccess == 0) {
3117          $USER->firstaccess = $user->firstaccess = $now;
3118      }
3119  
3120      // Store the previous current as lastlogin.
3121      $USER->lastlogin = $user->lastlogin = $USER->currentlogin;
3122  
3123      $USER->currentlogin = $user->currentlogin = $now;
3124  
3125      // Function user_accesstime_log() may not update immediately, better do it here.
3126      $USER->lastaccess = $user->lastaccess = $now;
3127      $USER->lastip = $user->lastip = getremoteaddr();
3128  
3129      // Note: do not call user_update_user() here because this is part of the login process,
3130      //       the login event means that these fields were updated.
3131      $DB->update_record('user', $user);
3132      return true;
3133  }
3134  
3135  /**
3136   * Determines if a user has completed setting up their account.
3137   *
3138   * @param stdClass $user A {@link $USER} object to test for the existence of a valid name and email
3139   * @return bool
3140   */
3141  function user_not_fully_set_up($user) {
3142      if (isguestuser($user)) {
3143          return false;
3144      }
3145      return (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user));
3146  }
3147  
3148  /**
3149   * Check whether the user has exceeded the bounce threshold
3150   *
3151   * @param stdClass $user A {@link $USER} object
3152   * @return bool true => User has exceeded bounce threshold
3153   */
3154  function over_bounce_threshold($user) {
3155      global $CFG, $DB;
3156  
3157      if (empty($CFG->handlebounces)) {
3158          return false;
3159      }
3160  
3161      if (empty($user->id)) {
3162          // No real (DB) user, nothing to do here.
3163          return false;
3164      }
3165  
3166      // Set sensible defaults.
3167      if (empty($CFG->minbounces)) {
3168          $CFG->minbounces = 10;
3169      }
3170      if (empty($CFG->bounceratio)) {
3171          $CFG->bounceratio = .20;
3172      }
3173      $bouncecount = 0;
3174      $sendcount = 0;
3175      if ($bounce = $DB->get_record('user_preferences', array ('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3176          $bouncecount = $bounce->value;
3177      }
3178      if ($send = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3179          $sendcount = $send->value;
3180      }
3181      return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio);
3182  }
3183  
3184  /**
3185   * Used to increment or reset email sent count
3186   *
3187   * @param stdClass $user object containing an id
3188   * @param bool $reset will reset the count to 0
3189   * @return void
3190   */
3191  function set_send_count($user, $reset=false) {
3192      global $DB;
3193  
3194      if (empty($user->id)) {
3195          // No real (DB) user, nothing to do here.
3196          return;
3197      }
3198  
3199      if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3200          $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3201          $DB->update_record('user_preferences', $pref);
3202      } else if (!empty($reset)) {
3203          // If it's not there and we're resetting, don't bother. Make a new one.
3204          $pref = new stdClass();
3205          $pref->name   = 'email_send_count';
3206          $pref->value  = 1;
3207          $pref->userid = $user->id;
3208          $DB->insert_record('user_preferences', $pref, false);
3209      }
3210  }
3211  
3212  /**
3213   * Increment or reset user's email bounce count
3214   *
3215   * @param stdClass $user object containing an id
3216   * @param bool $reset will reset the count to 0
3217   */
3218  function set_bounce_count($user, $reset=false) {
3219      global $DB;
3220  
3221      if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3222          $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3223          $DB->update_record('user_preferences', $pref);
3224      } else if (!empty($reset)) {
3225          // If it's not there and we're resetting, don't bother. Make a new one.
3226          $pref = new stdClass();
3227          $pref->name   = 'email_bounce_count';
3228          $pref->value  = 1;
3229          $pref->userid = $user->id;
3230          $DB->insert_record('user_preferences', $pref, false);
3231      }
3232  }
3233  
3234  /**
3235   * Determines if the logged in user is currently moving an activity
3236   *
3237   * @param int $courseid The id of the course being tested
3238   * @return bool
3239   */
3240  function ismoving($courseid) {
3241      global $USER;
3242  
3243      if (!empty($USER->activitycopy)) {
3244          return ($USER->activitycopycourse == $courseid);
3245      }
3246      return false;
3247  }
3248  
3249  /**
3250   * Returns a persons full name
3251   *
3252   * Given an object containing all of the users name values, this function returns a string with the full name of the person.
3253   * The result may depend on system settings or language.  'override' will force both names to be used even if system settings
3254   * specify one.
3255   *
3256   * @param stdClass $user A {@link $USER} object to get full name of.
3257   * @param bool $override If true then the name will be firstname followed by lastname rather than adhering to fullnamedisplay.
3258   * @return string
3259   */
3260  function fullname($user, $override=false) {
3261      global $CFG, $SESSION;
3262  
3263      if (!isset($user->firstname) and !isset($user->lastname)) {
3264          return '';
3265      }
3266  
3267      // Get all of the name fields.
3268      $allnames = get_all_user_name_fields();
3269      if ($CFG->debugdeveloper) {
3270          foreach ($allnames as $allname) {
3271              if (!property_exists($user, $allname)) {
3272                  // If all the user name fields are not set in the user object, then notify the programmer that it needs to be fixed.
3273                  debugging('You need to update your sql to include additional name fields in the user object.', DEBUG_DEVELOPER);
3274                  // Message has been sent, no point in sending the message multiple times.
3275                  break;
3276              }
3277          }
3278      }
3279  
3280      if (!$override) {
3281          if (!empty($CFG->forcefirstname)) {
3282              $user->firstname = $CFG->forcefirstname;
3283          }
3284          if (!empty($CFG->forcelastname)) {
3285              $user->lastname = $CFG->forcelastname;
3286          }
3287      }
3288  
3289      if (!empty($SESSION->fullnamedisplay)) {
3290          $CFG->fullnamedisplay = $SESSION->fullnamedisplay;
3291      }
3292  
3293      $template = null;
3294      // If the fullnamedisplay setting is available, set the template to that.
3295      if (isset($CFG->fullnamedisplay)) {
3296          $template = $CFG->fullnamedisplay;
3297      }
3298      // If the template is empty, or set to language, return the language string.
3299      if ((empty($template) || $template == 'language') && !$override) {
3300          return get_string('fullnamedisplay', null, $user);
3301      }
3302  
3303      // Check to see if we are displaying according to the alternative full name format.
3304      if ($override) {
3305          if (empty($CFG->alternativefullnameformat) || $CFG->alternativefullnameformat == 'language') {
3306              // Default to show just the user names according to the fullnamedisplay string.
3307              return get_string('fullnamedisplay', null, $user);
3308          } else {
3309              // If the override is true, then change the template to use the complete name.
3310              $template = $CFG->alternativefullnameformat;
3311          }
3312      }
3313  
3314      $requirednames = array();
3315      // With each name, see if it is in the display name template, and add it to the required names array if it is.
3316      foreach ($allnames as $allname) {
3317          if (strpos($template, $allname) !== false) {
3318              $requirednames[] = $allname;
3319          }
3320      }
3321  
3322      $displayname = $template;
3323      // Switch in the actual data into the template.
3324      foreach ($requirednames as $altname) {
3325          if (isset($user->$altname)) {
3326              // Using empty() on the below if statement causes breakages.
3327              if ((string)$user->$altname == '') {
3328                  $displayname = str_replace($altname, 'EMPTY', $displayname);
3329              } else {
3330                  $displayname = str_replace($altname, $user->$altname, $displayname);
3331              }
3332          } else {
3333              $displayname = str_replace($altname, 'EMPTY', $displayname);
3334          }
3335      }
3336      // Tidy up any misc. characters (Not perfect, but gets most characters).
3337      // Don't remove the "u" at the end of the first expression unless you want garbled characters when combining hiragana or
3338      // katakana and parenthesis.
3339      $patterns = array();
3340      // This regular expression replacement is to fix problems such as 'James () Kirk' Where 'Tiberius' (middlename) has not been
3341      // filled in by a user.
3342      // The special characters are Japanese brackets that are common enough to make allowances for them (not covered by :punct:).
3343      $patterns[] = '/[[:punct:]「」]*EMPTY[[:punct:]「」]*/u';
3344      // This regular expression is to remove any double spaces in the display name.
3345      $patterns[] = '/\s{2,}/u';
3346      foreach ($patterns as $pattern) {
3347          $displayname = preg_replace($pattern, ' ', $displayname);
3348      }
3349  
3350      // Trimming $displayname will help the next check to ensure that we don't have a display name with spaces.
3351      $displayname = trim($displayname);
3352      if (empty($displayname)) {
3353          // Going with just the first name if no alternate fields are filled out. May be changed later depending on what
3354          // people in general feel is a good setting to fall back on.
3355          $displayname = $user->firstname;
3356      }
3357      return $displayname;
3358  }
3359  
3360  /**
3361   * A centralised location for the all name fields. Returns an array / sql string snippet.
3362   *
3363   * @param bool $returnsql True for an sql select field snippet.
3364   * @param string $tableprefix table query prefix to use in front of each field.
3365   * @param string $prefix prefix added to the name fields e.g. authorfirstname.
3366   * @param string $fieldprefix sql field prefix e.g. id AS userid.
3367   * @param bool $order moves firstname and lastname to the top of the array / start of the string.
3368   * @return array|string All name fields.
3369   */
3370  function get_all_user_name_fields($returnsql = false, $tableprefix = null, $prefix = null, $fieldprefix = null, $order = false) {
3371      // This array is provided in this order because when called by fullname() (above) if firstname is before
3372      // firstnamephonetic str_replace() will change the wrong placeholder.
3373      $alternatenames = array('firstnamephonetic' => 'firstnamephonetic',
3374                              'lastnamephonetic' => 'lastnamephonetic',
3375                              'middlename' => 'middlename',
3376                              'alternatename' => 'alternatename',
3377                              'firstname' => 'firstname',
3378                              'lastname' => 'lastname');
3379  
3380      // Let's add a prefix to the array of user name fields if provided.
3381      if ($prefix) {
3382          foreach ($alternatenames as $key => $altname) {
3383              $alternatenames[$key] = $prefix . $altname;
3384          }
3385      }
3386  
3387      // If we want the end result to have firstname and lastname at the front / top of the result.
3388      if ($order) {
3389          // Move the last two elements (firstname, lastname) off the array and put them at the top.
3390          for ($i = 0; $i < 2; $i++) {
3391              // Get the last element.
3392              $lastelement = end($alternatenames);
3393              // Remove it from the array.
3394              unset($alternatenames[$lastelement]);
3395              // Put the element back on the top of the array.
3396              $alternatenames = array_merge(array($lastelement => $lastelement), $alternatenames);
3397          }
3398      }
3399  
3400      // Create an sql field snippet if requested.
3401      if ($returnsql) {
3402          if ($tableprefix) {
3403              if ($fieldprefix) {
3404                  foreach ($alternatenames as $key => $altname) {
3405                      $alternatenames[$key] = $tableprefix . '.' . $altname . ' AS ' . $fieldprefix . $altname;
3406                  }
3407              } else {
3408                  foreach ($alternatenames as $key => $altname) {
3409                      $alternatenames[$key] = $tableprefix . '.' . $altname;
3410                  }
3411              }
3412          }
3413          $alternatenames = implode(',', $alternatenames);
3414      }
3415      return $alternatenames;
3416  }
3417  
3418  /**
3419   * Reduces lines of duplicated code for getting user name fields.
3420   *
3421   * See also {@link user_picture::unalias()}
3422   *
3423   * @param object $addtoobject Object to add user name fields to.
3424   * @param object $secondobject Object that contains user name field information.
3425   * @param string $prefix prefix to be added to all fields (including $additionalfields) e.g. authorfirstname.
3426   * @param array $additionalfields Additional fields to be matched with data in the second object.
3427   * The key can be set to the user table field name.
3428   * @return object User name fields.
3429   */
3430  function username_load_fields_from_object($addtoobject, $secondobject, $prefix = null, $additionalfields = null) {
3431      $fields = get_all_user_name_fields(false, null, $prefix);
3432      if ($additionalfields) {
3433          // Additional fields can specify their own 'alias' such as 'id' => 'userid'. This checks to see if
3434          // the key is a number and then sets the key to the array value.
3435          foreach ($additionalfields as $key => $value) {
3436              if (is_numeric($key)) {
3437                  $additionalfields[$value] = $prefix . $value;
3438                  unset($additionalfields[$key]);
3439              } else {
3440                  $additionalfields[$key] = $prefix . $value;
3441              }
3442          }
3443          $fields = array_merge($fields, $additionalfields);
3444      }
3445      foreach ($fields as $key => $field) {
3446          // Important that we have all of the user name fields present in the object that we are sending back.
3447          $addtoobject->$key = '';
3448          if (isset($secondobject->$field)) {
3449              $addtoobject->$key = $secondobject->$field;
3450          }
3451      }
3452      return $addtoobject;
3453  }
3454  
3455  /**
3456   * Returns an array of values in order of occurance in a provided string.
3457   * The key in the result is the character postion in the string.
3458   *
3459   * @param array $values Values to be found in the string format
3460   * @param string $stringformat The string which may contain values being searched for.
3461   * @return array An array of values in order according to placement in the string format.
3462   */
3463  function order_in_string($values, $stringformat) {
3464      $valuearray = array();
3465      foreach ($values as $value) {
3466          $pattern = "/$value\b/";
3467          // Using preg_match as strpos() may match values that are similar e.g. firstname and firstnamephonetic.
3468          if (preg_match($pattern, $stringformat)) {
3469              $replacement = "thing";
3470              // Replace the value with something more unique to ensure we get the right position when using strpos().
3471              $newformat = preg_replace($pattern, $replacement, $stringformat);
3472              $position = strpos($newformat, $replacement);
3473              $valuearray[$position] = $value;
3474          }
3475      }
3476      ksort($valuearray);
3477      return $valuearray;
3478  }
3479  
3480  /**
3481   * Checks if current user is shown any extra fields when listing users.
3482   *
3483   * @param object $context Context
3484   * @param array $already Array of fields that we're going to show anyway
3485   *   so don't bother listing them
3486   * @return array Array of field names from user table, not including anything
3487   *   listed in $already
3488   */
3489  function get_extra_user_fields($context, $already = array()) {
3490      global $CFG;
3491  
3492      // Only users with permission get the extra fields.
3493      if (!has_capability('moodle/site:viewuseridentity', $context)) {
3494          return array();
3495      }
3496  
3497      // Split showuseridentity on comma.
3498      if (empty($CFG->showuseridentity)) {
3499          // Explode gives wrong result with empty string.
3500          $extra = array();
3501      } else {
3502          $extra =  explode(',', $CFG->showuseridentity);
3503      }
3504      $renumber = false;
3505      foreach ($extra as $key => $field) {
3506          if (in_array($field, $already)) {
3507              unset($extra[$key]);
3508              $renumber = true;
3509          }
3510      }
3511      if ($renumber) {
3512          // For consistency, if entries are removed from array, renumber it
3513          // so they are numbered as you would expect.
3514          $extra = array_merge($extra);
3515      }
3516      return $extra;
3517  }
3518  
3519  /**
3520   * If the current user is to be shown extra user fields when listing or
3521   * selecting users, returns a string suitable for including in an SQL select
3522   * clause to retrieve those fields.
3523   *
3524   * @param context $context Context
3525   * @param string $alias Alias of user table, e.g. 'u' (default none)
3526   * @param string $prefix Prefix for field names using AS, e.g. 'u_' (default none)
3527   * @param array $already Array of fields that we're going to include anyway so don't list them (default none)
3528   * @return string Partial SQL select clause, beginning with comma, for example ',u.idnumber,u.department' unless it is blank
3529   */
3530  function get_extra_user_fields_sql($context, $alias='', $prefix='', $already = array()) {
3531      $fields = get_extra_user_fields($context, $already);
3532      $result = '';
3533      // Add punctuation for alias.
3534      if ($alias !== '') {
3535          $alias .= '.';
3536      }
3537      foreach ($fields as $field) {
3538          $result .= ', ' . $alias . $field;
3539          if ($prefix) {
3540              $result .= ' AS ' . $prefix . $field;
3541          }
3542      }
3543      return $result;
3544  }
3545  
3546  /**
3547   * Returns the display name of a field in the user table. Works for most fields that are commonly displayed to users.
3548   * @param string $field Field name, e.g. 'phone1'
3549   * @return string Text description taken from language file, e.g. 'Phone number'
3550   */
3551  function get_user_field_name($field) {
3552      // Some fields have language strings which are not the same as field name.
3553      switch ($field) {
3554          case 'url' : {
3555              return get_string('webpage');
3556          }
3557          case 'icq' : {
3558              return get_string('icqnumber');
3559          }
3560          case 'skype' : {
3561              return get_string('skypeid');
3562          }
3563          case 'aim' : {
3564              return get_string('aimid');
3565          }
3566          case 'yahoo' : {
3567              return get_string('yahooid');
3568          }
3569          case 'msn' : {
3570              return get_string('msnid');
3571          }
3572      }
3573      // Otherwise just use the same lang string.
3574      return get_string($field);
3575  }
3576  
3577  /**
3578   * Returns whether a given authentication plugin exists.
3579   *
3580   * @param string $auth Form of authentication to check for. Defaults to the global setting in {@link $CFG}.
3581   * @return boolean Whether the plugin is available.
3582   */
3583  function exists_auth_plugin($auth) {
3584      global $CFG;
3585  
3586      if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) {
3587          return is_readable("{$CFG->dirroot}/auth/$auth/auth.php");
3588      }
3589      return false;
3590  }
3591  
3592  /**
3593   * Checks if a given plugin is in the list of enabled authentication plugins.
3594   *
3595   * @param string $auth Authentication plugin.
3596   * @return boolean Whether the plugin is enabled.
3597   */
3598  function is_enabled_auth($auth) {
3599      if (empty($auth)) {
3600          return false;
3601      }
3602  
3603      $enabled = get_enabled_auth_plugins();
3604  
3605      return in_array($auth, $enabled);
3606  }
3607  
3608  /**
3609   * Returns an authentication plugin instance.
3610   *
3611   * @param string $auth name of authentication plugin
3612   * @return auth_plugin_base An instance of the required authentication plugin.
3613   */
3614  function get_auth_plugin($auth) {
3615      global $CFG;
3616  
3617      // Check the plugin exists first.
3618      if (! exists_auth_plugin($auth)) {
3619          print_error('authpluginnotfound', 'debug', '', $auth);
3620      }
3621  
3622      // Return auth plugin instance.
3623      require_once("{$CFG->dirroot}/auth/$auth/auth.php");
3624      $class = "auth_plugin_$auth";
3625      return new $class;
3626  }
3627  
3628  /**
3629   * Returns array of active auth plugins.
3630   *
3631   * @param bool $fix fix $CFG->auth if needed
3632   * @return array
3633   */
3634  function get_enabled_auth_plugins($fix=false) {
3635      global $CFG;
3636  
3637      $default = array('manual', 'nologin');
3638  
3639      if (empty($CFG->auth)) {
3640          $auths = array();
3641      } else {
3642          $auths = explode(',', $CFG->auth);
3643      }
3644  
3645      if ($fix) {
3646          $auths = array_unique($auths);
3647          foreach ($auths as $k => $authname) {
3648              if (!exists_auth_plugin($authname) or in_array($authname, $default)) {
3649                  unset($auths[$k]);
3650              }
3651          }
3652          $newconfig = implode(',', $auths);
3653          if (!isset($CFG->auth) or $newconfig != $CFG->auth) {
3654              set_config('auth', $newconfig);
3655          }
3656      }
3657  
3658      return (array_merge($default, $auths));
3659  }
3660  
3661  /**
3662   * Returns true if an internal authentication method is being used.
3663   * if method not specified then, global default is assumed
3664   *
3665   * @param string $auth Form of authentication required
3666   * @return bool
3667   */
3668  function is_internal_auth($auth) {
3669      // Throws error if bad $auth.
3670      $authplugin = get_auth_plugin($auth);
3671      return $authplugin->is_internal();
3672  }
3673  
3674  /**
3675   * Returns true if the user is a 'restored' one.
3676   *
3677   * Used in the login process to inform the user and allow him/her to reset the password
3678   *
3679   * @param string $username username to be checked
3680   * @return bool
3681   */
3682  function is_restored_user($username) {
3683      global $CFG, $DB;
3684  
3685      return $DB->record_exists('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'password' => 'restored'));
3686  }
3687  
3688  /**
3689   * Returns an array of user fields
3690   *
3691   * @return array User field/column names
3692   */
3693  function get_user_fieldnames() {
3694      global $DB;
3695  
3696      $fieldarray = $DB->get_columns('user');
3697      unset($fieldarray['id']);
3698      $fieldarray = array_keys($fieldarray);
3699  
3700      return $fieldarray;
3701  }
3702  
3703  /**
3704   * Creates a bare-bones user record
3705   *
3706   * @todo Outline auth types and provide code example
3707   *
3708   * @param string $username New user's username to add to record
3709   * @param string $password New user's password to add to record
3710   * @param string $auth Form of authentication required
3711   * @return stdClass A complete user object
3712   */
3713  function create_user_record($username, $password, $auth = 'manual') {
3714      global $CFG, $DB;
3715      require_once($CFG->dirroot.'/user/profile/lib.php');
3716      require_once($CFG->dirroot.'/user/lib.php');
3717  
3718      // Just in case check text case.
3719      $username = trim(core_text::strtolower($username));
3720  
3721      $authplugin = get_auth_plugin($auth);
3722      $customfields = $authplugin->get_custom_user_profile_fields();
3723      $newuser = new stdClass();
3724      if ($newinfo = $authplugin->get_userinfo($username)) {
3725          $newinfo = truncate_userinfo($newinfo);
3726          foreach ($newinfo as $key => $value) {
3727              if (in_array($key, $authplugin->userfields) || (in_array($key, $customfields))) {
3728                  $newuser->$key = $value;
3729              }
3730          }
3731      }
3732  
3733      if (!empty($newuser->email)) {
3734          if (email_is_not_allowed($newuser->email)) {
3735              unset($newuser->email);
3736          }
3737      }
3738  
3739      if (!isset($newuser->city)) {
3740          $newuser->city = '';
3741      }
3742  
3743      $newuser->auth = $auth;
3744      $newuser->username = $username;
3745  
3746      // Fix for MDL-8480
3747      // user CFG lang for user if $newuser->lang is empty
3748      // or $user->lang is not an installed language.
3749      if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) {
3750          $newuser->lang = $CFG->lang;
3751      }
3752      $newuser->confirmed = 1;
3753      $newuser->lastip = getremoteaddr();
3754      $newuser->timecreated = time();
3755      $newuser->timemodified = $newuser->timecreated;
3756      $newuser->mnethostid = $CFG->mnet_localhost_id;
3757  
3758      $newuser->id = user_create_user($newuser, false, false);
3759  
3760      // Save user profile data.
3761      profile_save_data($newuser);
3762  
3763      $user = get_complete_user_data('id', $newuser->id);
3764      if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})) {
3765          set_user_preference('auth_forcepasswordchange', 1, $user);
3766      }
3767      // Set the password.
3768      update_internal_user_password($user, $password);
3769  
3770      // Trigger event.
3771      \core\event\user_created::create_from_userid($newuser->id)->trigger();
3772  
3773      return $user;
3774  }
3775  
3776  /**
3777   * Will update a local user record from an external source (MNET users can not be updated using this method!).
3778   *
3779   * @param string $username user's username to update the record
3780   * @return stdClass A complete user object
3781   */
3782  function update_user_record($username) {
3783      global $DB, $CFG;
3784      // Just in case check text case.
3785      $username = trim(core_text::strtolower($username));
3786  
3787      $oldinfo = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id), '*', MUST_EXIST);
3788      return update_user_record_by_id($oldinfo->id);
3789  }
3790  
3791  /**
3792   * Will update a local user record from an external source (MNET users can not be updated using this method!).
3793   *
3794   * @param int $id user id
3795   * @return stdClass A complete user object
3796   */
3797  function update_user_record_by_id($id) {
3798      global $DB, $CFG;
3799      require_once($CFG->dirroot."/user/profile/lib.php");
3800      require_once($CFG->dirroot.'/user/lib.php');
3801  
3802      $params = array('mnethostid' => $CFG->mnet_localhost_id, 'id' => $id, 'deleted' => 0);
3803      $oldinfo = $DB->get_record('user', $params, '*', MUST_EXIST);
3804  
3805      $newuser = array();
3806      $userauth = get_auth_plugin($oldinfo->auth);
3807  
3808      if ($newinfo = $userauth->get_userinfo($oldinfo->username)) {
3809          $newinfo = truncate_userinfo($newinfo);
3810          $customfields = $userauth->get_custom_user_profile_fields();
3811  
3812          foreach ($newinfo as $key => $value) {
3813              $iscustom = in_array($key, $customfields);
3814              if (!$iscustom) {
3815                  $key = strtolower($key);
3816              }
3817              if ((!property_exists($oldinfo, $key) && !$iscustom) or $key === 'username' or $key === 'id'
3818                      or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') {
3819                  // Unknown or must not be changed.
3820                  continue;
3821              }
3822              $confval = $userauth->config->{'field_updatelocal_' . $key};
3823              $lockval = $userauth->config->{'field_lock_' . $key};
3824              if (empty($confval) || empty($lockval)) {
3825                  continue;
3826              }
3827              if ($confval === 'onlogin') {
3828                  // MDL-4207 Don't overwrite modified user profile values with
3829                  // empty LDAP values when 'unlocked if empty' is set. The purpose
3830                  // of the setting 'unlocked if empty' is to allow the user to fill
3831                  // in a value for the selected field _if LDAP is giving
3832                  // nothing_ for this field. Thus it makes sense to let this value
3833                  // stand in until LDAP is giving a value for this field.
3834                  if (!(empty($value) && $lockval === 'unlockedifempty')) {
3835                      if ($iscustom || (in_array($key, $userauth->userfields) &&
3836                              ((string)$oldinfo->$key !== (string)$value))) {
3837                          $newuser[$key] = (string)$value;
3838                      }
3839                  }
3840              }
3841          }
3842          if ($newuser) {
3843              $newuser['id'] = $oldinfo->id;
3844              $newuser['timemodified'] = time();
3845              user_update_user((object) $newuser, false, false);
3846  
3847              // Save user profile data.
3848              profile_save_data((object) $newuser);
3849  
3850              // Trigger event.
3851              \core\event\user_updated::create_from_userid($newuser['id'])->trigger();
3852          }
3853      }
3854  
3855      return get_complete_user_data('id', $oldinfo->id);
3856  }
3857  
3858  /**
3859   * Will truncate userinfo as it comes from auth_get_userinfo (from external auth) which may have large fields.
3860   *
3861   * @param array $info Array of user properties to truncate if needed
3862   * @return array The now truncated information that was passed in
3863   */
3864  function truncate_userinfo(array $info) {
3865      // Define the limits.
3866      $limit = array(
3867          'username'    => 100,
3868          'idnumber'    => 255,
3869          'firstname'   => 100,
3870          'lastname'    => 100,
3871          'email'       => 100,
3872          'icq'         =>  15,
3873          'phone1'      =>  20,
3874          'phone2'      =>  20,
3875          'institution' => 255,
3876          'department'  => 255,
3877          'address'     => 255,
3878          'city'        => 120,
3879          'country'     =>   2,
3880          'url'         => 255,
3881      );
3882  
3883      // Apply where needed.
3884      foreach (array_keys($info) as $key) {
3885          if (!empty($limit[$key])) {
3886              $info[$key] = trim(core_text::substr($info[$key], 0, $limit[$key]));
3887          }
3888      }
3889  
3890      return $info;
3891  }
3892  
3893  /**
3894   * Marks user deleted in internal user database and notifies the auth plugin.
3895   * Also unenrols user from all roles and does other cleanup.
3896   *
3897   * Any plugin that needs to purge user data should register the 'user_deleted' event.
3898   *
3899   * @param stdClass $user full user object before delete
3900   * @return boolean success
3901   * @throws coding_exception if invalid $user parameter detected
3902   */
3903  function delete_user(stdClass $user) {
3904      global $CFG, $DB;
3905      require_once($CFG->libdir.'/grouplib.php');
3906      require_once($CFG->libdir.'/gradelib.php');
3907      require_once($CFG->dirroot.'/message/lib.php');
3908      require_once($CFG->dirroot.'/user/lib.php');
3909  
3910      // Make sure nobody sends bogus record type as parameter.
3911      if (!property_exists($user, 'id') or !property_exists($user, 'username')) {
3912          throw new coding_exception('Invalid $user parameter in delete_user() detected');
3913      }
3914  
3915      // Better not trust the parameter and fetch the latest info this will be very expensive anyway.
3916      if (!$user = $DB->get_record('user', array('id' => $user->id))) {
3917          debugging('Attempt to delete unknown user account.');
3918          return false;
3919      }
3920  
3921      // There must be always exactly one guest record, originally the guest account was identified by username only,
3922      // now we use $CFG->siteguest for performance reasons.
3923      if ($user->username === 'guest' or isguestuser($user)) {
3924          debugging('Guest user account can not be deleted.');
3925          return false;
3926      }
3927  
3928      // Admin can be theoretically from different auth plugin, but we want to prevent deletion of internal accoutns only,
3929      // if anything goes wrong ppl may force somebody to be admin via config.php setting $CFG->siteadmins.
3930      if ($user->auth === 'manual' and is_siteadmin($user)) {
3931          debugging('Local administrator accounts can not be deleted.');
3932          return false;
3933      }
3934  
3935      // Allow plugins to use this user object before we completely delete it.
3936      if ($pluginsfunction = get_plugins_with_function('pre_user_delete')) {
3937          foreach ($pluginsfunction as $plugintype => $plugins) {
3938              foreach ($plugins as $pluginfunction) {
3939                  $pluginfunction($user);
3940              }
3941          }
3942      }
3943  
3944      // Keep user record before updating it, as we have to pass this to user_deleted event.
3945      $olduser = clone $user;
3946  
3947      // Keep a copy of user context, we need it for event.
3948      $usercontext = context_user::instance($user->id);
3949  
3950      // Delete all grades - backup is kept in grade_grades_history table.
3951      grade_user_delete($user->id);
3952  
3953      // Move unread messages from this user to read.
3954      message_move_userfrom_unread2read($user->id);
3955  
3956      // TODO: remove from cohorts using standard API here.
3957  
3958      // Remove user tags.
3959      core_tag_tag::remove_all_item_tags('core', 'user', $user->id);
3960  
3961      // Unconditionally unenrol from all courses.
3962      enrol_user_delete($user);
3963  
3964      // Unenrol from all roles in all contexts.
3965      // This might be slow but it is really needed - modules might do some extra cleanup!
3966      role_unassign_all(array('userid' => $user->id));
3967  
3968      // Now do a brute force cleanup.
3969  
3970      // Remove from all cohorts.
3971      $DB->delete_records('cohort_members', array('userid' => $user->id));
3972  
3973      // Remove from all groups.
3974      $DB->delete_records('groups_members', array('userid' => $user->id));
3975  
3976      // Brute force unenrol from all courses.
3977      $DB->delete_records('user_enrolments', array('userid' => $user->id));
3978  
3979      // Purge user preferences.
3980      $DB->delete_records('user_preferences', array('userid' => $user->id));
3981  
3982      // Purge user extra profile info.
3983      $DB->delete_records('user_info_data', array('userid' => $user->id));
3984  
3985      // Purge log of previous password hashes.
3986      $DB->delete_records('user_password_history', array('userid' => $user->id));
3987  
3988      // Last course access not necessary either.
3989      $DB->delete_records('user_lastaccess', array('userid' => $user->id));
3990      // Remove all user tokens.
3991      $DB->delete_records('external_tokens', array('userid' => $user->id));
3992  
3993      // Unauthorise the user for all services.
3994      $DB->delete_records('external_services_users', array('userid' => $user->id));
3995  
3996      // Remove users private keys.
3997      $DB->delete_records('user_private_key', array('userid' => $user->id));
3998  
3999      // Remove users customised pages.
4000      $DB->delete_records('my_pages', array('userid' => $user->id, 'private' => 1));
4001  
4002      // Force logout - may fail if file based sessions used, sorry.
4003      \core\session\manager::kill_user_sessions($user->id);
4004  
4005      // Generate username from email address, or a fake email.
4006      $delemail = !empty($user->email) ? $user->email : $user->username . '.' . $user->id . '@unknownemail.invalid';
4007      $delname = clean_param($delemail . "." . time(), PARAM_USERNAME);
4008  
4009      // Workaround for bulk deletes of users with the same email address.
4010      while ($DB->record_exists('user', array('username' => $delname))) { // No need to use mnethostid here.
4011          $delname++;
4012      }
4013  
4014      // Mark internal user record as "deleted".
4015      $updateuser = new stdClass();
4016      $updateuser->id           = $user->id;
4017      $updateuser->deleted      = 1;
4018      $updateuser->username     = $delname;            // Remember it just in case.
4019      $updateuser->email        = md5($user->username);// Store hash of username, useful importing/restoring users.
4020      $updateuser->idnumber     = '';                  // Clear this field to free it up.
4021      $updateuser->picture      = 0;
4022      $updateuser->timemodified = time();
4023  
4024      // Don't trigger update event, as user is being deleted.
4025      user_update_user($updateuser, false, false);
4026  
4027      // Now do a final accesslib cleanup - removes all role assignments in user context and context itself.
4028      context_helper::delete_instance(CONTEXT_USER, $user->id);
4029  
4030      // Any plugin that needs to cleanup should register this event.
4031      // Trigger event.
4032      $event = \core\event\user_deleted::create(
4033              array(
4034                  'objectid' => $user->id,
4035                  'relateduserid' => $user->id,
4036                  'context' => $usercontext,
4037                  'other' => array(
4038                      'username' => $user->username,
4039                      'email' => $user->email,
4040                      'idnumber' => $user->idnumber,
4041                      'picture' => $user->picture,
4042                      'mnethostid' => $user->mnethostid
4043                      )
4044                  )
4045              );
4046      $event->add_record_snapshot('user', $olduser);
4047      $event->trigger();
4048  
4049      // We will update the user's timemodified, as it will be passed to the user_deleted event, which
4050      // should know about this updated property persisted to the user's table.
4051      $user->timemodified = $updateuser->timemodified;
4052  
4053      // Notify auth plugin - do not block the delete even when plugin fails.
4054      $authplugin = get_auth_plugin($user->auth);
4055      $authplugin->user_delete($user);
4056  
4057      return true;
4058  }
4059  
4060  /**
4061   * Retrieve the guest user object.
4062   *
4063   * @return stdClass A {@link $USER} object
4064   */
4065  function guest_user() {
4066      global $CFG, $DB;
4067  
4068      if ($newuser = $DB->get_record('user', array('id' => $CFG->siteguest))) {
4069          $newuser->confirmed = 1;
4070          $newuser->lang = $CFG->lang;
4071          $newuser->lastip = getremoteaddr();
4072      }
4073  
4074      return $newuser;
4075  }
4076  
4077  /**
4078   * Authenticates a user against the chosen authentication mechanism
4079   *
4080   * Given a username and password, this function looks them
4081   * up using the currently selected authentication mechanism,
4082   * and if the authentication is successful, it returns a
4083   * valid $user object from the 'user' table.
4084   *
4085   * Uses auth_ functions from the currently active auth module
4086   *
4087   * After authenticate_user_login() returns success, you will need to
4088   * log that the user has logged in, and call complete_user_login() to set
4089   * the session up.
4090   *
4091   * Note: this function works only with non-mnet accounts!
4092   *
4093   * @param string $username  User's username (or also email if $CFG->authloginviaemail enabled)
4094   * @param string $password  User's password
4095   * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO
4096   * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists)
4097   * @return stdClass|false A {@link $USER} object or false if error
4098   */
4099  function authenticate_user_login($username, $password, $ignorelockout=false, &$failurereason=null) {
4100      global $CFG, $DB;
4101      require_once("$CFG->libdir/authlib.php");
4102  
4103      if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) {
4104          // we have found the user
4105  
4106      } else if (!empty($CFG->authloginviaemail)) {
4107          if ($email = clean_param($username, PARAM_EMAIL)) {
4108              $select = "mnethostid = :mnethostid AND LOWER(email) = LOWER(:email) AND deleted = 0";
4109              $params = array('mnethostid' => $CFG->mnet_localhost_id, 'email' => $email);
4110              $users = $DB->get_records_select('user', $select, $params, 'id', 'id', 0, 2);
4111              if (count($users) === 1) {
4112                  // Use email for login only if unique.
4113                  $user = reset($users);
4114                  $user = get_complete_user_data('id', $user->id);
4115                  $username = $user->username;
4116              }
4117              unset($users);
4118          }
4119      }
4120  
4121      $authsenabled = get_enabled_auth_plugins();
4122  
4123      if ($user) {
4124          // Use manual if auth not set.
4125          $auth = empty($user->auth) ? 'manual' : $user->auth;
4126  
4127          if (in_array($user->auth, $authsenabled)) {
4128              $authplugin = get_auth_plugin($user->auth);
4129              $authplugin->pre_user_login_hook($user);
4130          }
4131  
4132          if (!empty($user->suspended)) {
4133              $failurereason = AUTH_LOGIN_SUSPENDED;
4134  
4135              // Trigger login failed event.
4136              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4137                      'other' => array('username' => $username, 'reason' => $failurereason)));
4138              $event->trigger();
4139              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4140              return false;
4141          }
4142          if ($auth=='nologin' or !is_enabled_auth($auth)) {
4143              // Legacy way to suspend user.
4144              $failurereason = AUTH_LOGIN_SUSPENDED;
4145  
4146              // Trigger login failed event.
4147              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4148                      'other' => array('username' => $username, 'reason' => $failurereason)));
4149              $event->trigger();
4150              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Disabled Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4151              return false;
4152          }
4153          $auths = array($auth);
4154  
4155      } else {
4156          // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user().
4157          if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id,  'deleted' => 1))) {
4158              $failurereason = AUTH_LOGIN_NOUSER;
4159  
4160              // Trigger login failed event.
4161              $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4162                      'reason' => $failurereason)));
4163              $event->trigger();
4164              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Deleted Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4165              return false;
4166          }
4167  
4168          // User does not exist.
4169          $auths = $authsenabled;
4170          $user = new stdClass();
4171          $user->id = 0;
4172      }
4173  
4174      if ($ignorelockout) {
4175          // Some other mechanism protects against brute force password guessing, for example login form might include reCAPTCHA
4176          // or this function is called from a SSO script.
4177      } else if ($user->id) {
4178          // Verify login lockout after other ways that may prevent user login.
4179          if (login_is_lockedout($user)) {
4180              $failurereason = AUTH_LOGIN_LOCKOUT;
4181  
4182              // Trigger login failed event.
4183              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4184                      'other' => array('username' => $username, 'reason' => $failurereason)));
4185              $event->trigger();
4186  
4187              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Login lockout:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4188              return false;
4189          }
4190      } else {
4191          // We can not lockout non-existing accounts.
4192      }
4193  
4194      foreach ($auths as $auth) {
4195          $authplugin = get_auth_plugin($auth);
4196  
4197          // On auth fail fall through to the next plugin.
4198          if (!$authplugin->user_login($username, $password)) {
4199              continue;
4200          }
4201  
4202          // Successful authentication.
4203          if ($user->id) {
4204              // User already exists in database.
4205              if (empty($user->auth)) {
4206                  // For some reason auth isn't set yet.
4207                  $DB->set_field('user', 'auth', $auth, array('id' => $user->id));
4208                  $user->auth = $auth;
4209              }
4210  
4211              // If the existing hash is using an out-of-date algorithm (or the legacy md5 algorithm), then we should update to
4212              // the current hash algorithm while we have access to the user's password.
4213              update_internal_user_password($user, $password);
4214  
4215              if ($authplugin->is_synchronised_with_external()) {
4216                  // Update user record from external DB.
4217                  $user = update_user_record_by_id($user->id);
4218              }
4219          } else {
4220              // The user is authenticated but user creation may be disabled.
4221              if (!empty($CFG->authpreventaccountcreation)) {
4222                  $failurereason = AUTH_LOGIN_UNAUTHORISED;
4223  
4224                  // Trigger login failed event.
4225                  $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4226                          'reason' => $failurereason)));
4227                  $event->trigger();
4228  
4229                  error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Unknown user, can not create new accounts:  $username  ".
4230                          $_SERVER['HTTP_USER_AGENT']);
4231                  return false;
4232              } else {
4233                  $user = create_user_record($username, $password, $auth);
4234              }
4235          }
4236  
4237          $authplugin->sync_roles($user);
4238  
4239          foreach ($authsenabled as $hau) {
4240              $hauth = get_auth_plugin($hau);
4241              $hauth->user_authenticated_hook($user, $username, $password);
4242          }
4243  
4244          if (empty($user->id)) {
4245              $failurereason = AUTH_LOGIN_NOUSER;
4246              // Trigger login failed event.
4247              $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4248                      'reason' => $failurereason)));
4249              $event->trigger();
4250              return false;
4251          }
4252  
4253          if (!empty($user->suspended)) {
4254              // Just in case some auth plugin suspended account.
4255              $failurereason = AUTH_LOGIN_SUSPENDED;
4256              // Trigger login failed event.
4257              $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4258                      'other' => array('username' => $username, 'reason' => $failurereason)));
4259              $event->trigger();
4260              error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4261              return false;
4262          }
4263  
4264          login_attempt_valid($user);
4265          $failurereason = AUTH_LOGIN_OK;
4266          return $user;
4267      }
4268  
4269      // Failed if all the plugins have failed.
4270      if (debugging('', DEBUG_ALL)) {
4271          error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Failed Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4272      }
4273  
4274      if ($user->id) {
4275          login_attempt_failed($user);
4276          $failurereason = AUTH_LOGIN_FAILED;
4277          // Trigger login failed event.
4278          $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4279                  'other' => array('username' => $username, 'reason' => $failurereason)));
4280          $event->trigger();
4281      } else {
4282          $failurereason = AUTH_LOGIN_NOUSER;
4283          // Trigger login failed event.
4284          $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4285                  'reason' => $failurereason)));
4286          $event->trigger();
4287      }
4288  
4289      return false;
4290  }
4291  
4292  /**
4293   * Call to complete the user login process after authenticate_user_login()
4294   * has succeeded. It will setup the $USER variable and other required bits
4295   * and pieces.
4296   *
4297   * NOTE:
4298   * - It will NOT log anything -- up to the caller to decide what to log.
4299   * - this function does not set any cookies any more!
4300   *
4301   * @param stdClass $user
4302   * @return stdClass A {@link $USER} object - BC only, do not use
4303   */
4304  function complete_user_login($user) {
4305      global $CFG, $USER, $SESSION;
4306  
4307      \core\session\manager::login_user($user);
4308  
4309      // Reload preferences from DB.
4310      unset($USER->preference);
4311      check_user_preferences_loaded($USER);
4312  
4313      // Update login times.
4314      update_user_login_times();
4315  
4316      // Extra session prefs init.
4317      set_login_session_preferences();
4318  
4319      // Trigger login event.
4320      $event = \core\event\user_loggedin::create(
4321          array(
4322              'userid' => $USER->id,
4323              'objectid' => $USER->id,
4324              'other' => array('username' => $USER->username),
4325          )
4326      );
4327      $event->trigger();
4328  
4329      if (isguestuser()) {
4330          // No need to continue when user is THE guest.
4331          return $USER;
4332      }
4333  
4334      if (CLI_SCRIPT) {
4335          // We can redirect to password change URL only in browser.
4336          return $USER;
4337      }
4338  
4339      // Select password change url.
4340      $userauth = get_auth_plugin($USER->auth);
4341  
4342      // Check whether the user should be changing password.
4343      if (get_user_preferences('auth_forcepasswordchange', false)) {
4344          if ($userauth->can_change_password()) {
4345              if ($changeurl = $userauth->change_password_url()) {
4346                  redirect($changeurl);
4347              } else {
4348                  $SESSION->wantsurl = core_login_get_return_url();
4349                  redirect($CFG->httpswwwroot.'/login/change_password.php');
4350              }
4351          } else {
4352              print_error('nopasswordchangeforced', 'auth');
4353          }
4354      }
4355      return $USER;
4356  }
4357  
4358  /**
4359   * Check a password hash to see if it was hashed using the legacy hash algorithm (md5).
4360   *
4361   * @param string $password String to check.
4362   * @return boolean True if the $password matches the format of an md5 sum.
4363   */
4364  function password_is_legacy_hash($password) {
4365      return (bool) preg_match('/^[0-9a-f]{32}$/', $password);
4366  }
4367  
4368  /**
4369   * Compare password against hash stored in user object to determine if it is valid.
4370   *
4371   * If necessary it also updates the stored hash to the current format.
4372   *
4373   * @param stdClass $user (Password property may be updated).
4374   * @param string $password Plain text password.
4375   * @return bool True if password is valid.
4376   */
4377  function validate_internal_user_password($user, $password) {
4378      global $CFG;
4379  
4380      if ($user->password === AUTH_PASSWORD_NOT_CACHED) {
4381          // Internal password is not used at all, it can not validate.
4382          return false;
4383      }
4384  
4385      // If hash isn't a legacy (md5) hash, validate using the library function.
4386      if (!password_is_legacy_hash($user->password)) {
4387          return password_verify($password, $user->password);
4388      }
4389  
4390      // Otherwise we need to check for a legacy (md5) hash instead. If the hash
4391      // is valid we can then update it to the new algorithm.
4392  
4393      $sitesalt = isset($CFG->passwordsaltmain) ? $CFG->passwordsaltmain : '';
4394      $validated = false;
4395  
4396      if ($user->password === md5($password.$sitesalt)
4397              or $user->password === md5($password)
4398              or $user->password === md5(addslashes($password).$sitesalt)
4399              or $user->password === md5(addslashes($password))) {
4400          // Note: we are intentionally using the addslashes() here because we
4401          //       need to accept old password hashes of passwords with magic quotes.
4402          $validated = true;
4403  
4404      } else {
4405          for ($i=1; $i<=20; $i++) { // 20 alternative salts should be enough, right?
4406              $alt = 'passwordsaltalt'.$i;
4407              if (!empty($CFG->$alt)) {
4408                  if ($user->password === md5($password.$CFG->$alt) or $user->password === md5(addslashes($password).$CFG->$alt)) {
4409                      $validated = true;
4410                      break;
4411                  }
4412              }
4413          }
4414      }
4415  
4416      if ($validated) {
4417          // If the password matches the existing md5 hash, update to the
4418          // current hash algorithm while we have access to the user's password.
4419          update_internal_user_password($user, $password);
4420      }
4421  
4422      return $validated;
4423  }
4424  
4425  /**
4426   * Calculate hash for a plain text password.
4427   *
4428   * @param string $password Plain text password to be hashed.
4429   * @param bool $fasthash If true, use a low cost factor when generating the hash
4430   *                       This is much faster to generate but makes the hash
4431   *                       less secure. It is used when lots of hashes need to
4432   *                       be generated quickly.
4433   * @return string The hashed password.
4434   *
4435   * @throws moodle_exception If a problem occurs while generating the hash.
4436   */
4437  function hash_internal_user_password($password, $fasthash = false) {
4438      global $CFG;
4439  
4440      // Set the cost factor to 4 for fast hashing, otherwise use default cost.
4441      $options = ($fasthash) ? array('cost' => 4) : array();
4442  
4443      $generatedhash = password_hash($password, PASSWORD_DEFAULT, $options);
4444  
4445      if ($generatedhash === false || $generatedhash === null) {
4446          throw new moodle_exception('Failed to generate password hash.');
4447      }
4448  
4449      return $generatedhash;
4450  }
4451  
4452  /**
4453   * Update password hash in user object (if necessary).
4454   *
4455   * The password is updated if:
4456   * 1. The password has changed (the hash of $user->password is different
4457   *    to the hash of $password).
4458   * 2. The existing hash is using an out-of-date algorithm (or the legacy
4459   *    md5 algorithm).
4460   *
4461   * Updating the password will modify the $user object and the database
4462   * record to use the current hashing algorithm.
4463   *
4464   * @param stdClass $user User object (password property may be updated).
4465   * @param string $password Plain text password.
4466   * @param bool $fasthash If true, use a low cost factor when generating the hash
4467   *                       This is much faster to generate but makes the hash
4468   *                       less secure. It is used when lots of hashes need to
4469   *                       be generated quickly.
4470   * @return bool Always returns true.
4471   */
4472  function update_internal_user_password($user, $password, $fasthash = false) {
4473      global $CFG, $DB;
4474  
4475      // Figure out what the hashed password should be.
4476      if (!isset($user->auth)) {
4477          debugging('User record in update_internal_user_password() must include field auth',
4478                  DEBUG_DEVELOPER);
4479          $user->auth = $DB->get_field('user', 'auth', array('id' => $user->id));
4480      }
4481      $authplugin = get_auth_plugin($user->auth);
4482      if ($authplugin->prevent_local_passwords()) {
4483          $hashedpassword = AUTH_PASSWORD_NOT_CACHED;
4484      } else {
4485          $hashedpassword = hash_internal_user_password($password, $fasthash);
4486      }
4487  
4488      $algorithmchanged = false;
4489  
4490      if ($hashedpassword === AUTH_PASSWORD_NOT_CACHED) {
4491          // Password is not cached, update it if not set to AUTH_PASSWORD_NOT_CACHED.
4492          $passwordchanged = ($user->password !== $hashedpassword);
4493  
4494      } else if (isset($user->password)) {
4495          // If verification fails then it means the password has changed.
4496          $passwordchanged = !password_verify($password, $user->password);
4497          $algorithmchanged = password_needs_rehash($user->password, PASSWORD_DEFAULT);
4498      } else {
4499          // While creating new user, password in unset in $user object, to avoid
4500          // saving it with user_create()
4501          $passwordchanged = true;
4502      }
4503  
4504      if ($passwordchanged || $algorithmchanged) {
4505          $DB->set_field('user', 'password',  $hashedpassword, array('id' => $user->id));
4506          $user->password = $hashedpassword;
4507  
4508          // Trigger event.
4509          $user = $DB->get_record('user', array('id' => $user->id));
4510          \core\event\user_password_updated::create_from_user($user)->trigger();
4511      }
4512  
4513      return true;
4514  }
4515  
4516  /**
4517   * Get a complete user record, which includes all the info in the user record.
4518   *
4519   * Intended for setting as $USER session variable
4520   *
4521   * @param string $field The user field to be checked for a given value.
4522   * @param string $value The value to match for $field.
4523   * @param int $mnethostid
4524   * @return mixed False, or A {@link $USER} object.
4525   */
4526  function get_complete_user_data($field, $value, $mnethostid = null) {
4527      global $CFG, $DB;
4528  
4529      if (!$field || !$value) {
4530          return false;
4531      }
4532  
4533      // Build the WHERE clause for an SQL query.
4534      $params = array('fieldval' => $value);
4535      $constraints = "$field = :fieldval AND deleted <> 1";
4536  
4537      // If we are loading user data based on anything other than id,
4538      // we must also restrict our search based on mnet host.
4539      if ($field != 'id') {
4540          if (empty($mnethostid)) {
4541              // If empty, we restrict to local users.
4542              $mnethostid = $CFG->mnet_localhost_id;
4543          }
4544      }
4545      if (!empty($mnethostid)) {
4546          $params['mnethostid'] = $mnethostid;
4547          $constraints .= " AND mnethostid = :mnethostid";
4548      }
4549  
4550      // Get all the basic user data.
4551      if (! $user = $DB->get_record_select('user', $constraints, $params)) {
4552          return false;
4553      }
4554  
4555      // Get various settings and preferences.
4556  
4557      // Preload preference cache.
4558      check_user_preferences_loaded($user);
4559  
4560      // Load course enrolment related stuff.
4561      $user->lastcourseaccess    = array(); // During last session.
4562      $user->currentcourseaccess = array(); // During current session.
4563      if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid' => $user->id))) {
4564          foreach ($lastaccesses as $lastaccess) {
4565              $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess;
4566          }
4567      }
4568  
4569      $sql = "SELECT g.id, g.courseid
4570                FROM {groups} g, {groups_members} gm
4571               WHERE gm.groupid=g.id AND gm.userid=?";
4572  
4573      // This is a special hack to speedup calendar display.
4574      $user->groupmember = array();
4575      if (!isguestuser($user)) {
4576          if ($groups = $DB->get_records_sql($sql, array($user->id))) {
4577              foreach ($groups as $group) {
4578                  if (!array_key_exists($group->courseid, $user->groupmember)) {
4579                      $user->groupmember[$group->courseid] = array();
4580                  }
4581                  $user->groupmember[$group->courseid][$group->id] = $group->id;
4582              }
4583          }
4584      }
4585  
4586      // Add the custom profile fields to the user record.
4587      $user->profile = array();
4588      if (!isguestuser($user)) {
4589          require_once($CFG->dirroot.'/user/profile/lib.php');
4590          profile_load_custom_fields($user);
4591      }
4592  
4593      // Rewrite some variables if necessary.
4594      if (!empty($user->description)) {
4595          // No need to cart all of it around.
4596          $user->description = true;
4597      }
4598      if (isguestuser($user)) {
4599          // Guest language always same as site.
4600          $user->lang = $CFG->lang;
4601          // Name always in current language.
4602          $user->firstname = get_string('guestuser');
4603          $user->lastname = ' ';
4604      }
4605  
4606      return $user;
4607  }
4608  
4609  /**
4610   * Validate a password against the configured password policy
4611   *
4612   * @param string $password the password to be checked against the password policy
4613   * @param string $errmsg the error message to display when the password doesn't comply with the policy.
4614   * @return bool true if the password is valid according to the policy. false otherwise.
4615   */
4616  function check_password_policy($password, &$errmsg) {
4617      global $CFG;
4618  
4619      if (empty($CFG->passwordpolicy)) {
4620          return true;
4621      }
4622  
4623      $errmsg = '';
4624      if (core_text::strlen($password) < $CFG->minpasswordlength) {
4625          $errmsg .= '<div>'. get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength) .'</div>';
4626  
4627      }
4628      if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) {
4629          $errmsg .= '<div>'. get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits) .'</div>';
4630  
4631      }
4632      if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) {
4633          $errmsg .= '<div>'. get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower) .'</div>';
4634  
4635      }
4636      if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) {
4637          $errmsg .= '<div>'. get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper) .'</div>';
4638  
4639      }
4640      if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) {
4641          $errmsg .= '<div>'. get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum) .'</div>';
4642      }
4643      if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) {
4644          $errmsg .= '<div>'. get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars) .'</div>';
4645      }
4646  
4647      if ($errmsg == '') {
4648          return true;
4649      } else {
4650          return false;
4651      }
4652  }
4653  
4654  
4655  /**
4656   * When logging in, this function is run to set certain preferences for the current SESSION.
4657   */
4658  function set_login_session_preferences() {
4659      global $SESSION;
4660  
4661      $SESSION->justloggedin = true;
4662  
4663      unset($SESSION->lang);
4664      unset($SESSION->forcelang);
4665      unset($SESSION->load_navigation_admin);
4666  }
4667  
4668  
4669  /**
4670   * Delete a course, including all related data from the database, and any associated files.
4671   *
4672   * @param mixed $courseorid The id of the course or course object to delete.
4673   * @param bool $showfeedback Whether to display notifications of each action the function performs.
4674   * @return bool true if all the removals succeeded. false if there were any failures. If this
4675   *             method returns false, some of the removals will probably have succeeded, and others
4676   *             failed, but you have no way of knowing which.
4677   */
4678  function delete_course($courseorid, $showfeedback = true) {
4679      global $DB;
4680  
4681      if (is_object($courseorid)) {
4682          $courseid = $courseorid->id;
4683          $course   = $courseorid;
4684      } else {
4685          $courseid = $courseorid;
4686          if (!$course = $DB->get_record('course', array('id' => $courseid))) {
4687              return false;
4688          }
4689      }
4690      $context = context_course::instance($courseid);
4691  
4692      // Frontpage course can not be deleted!!
4693      if ($courseid == SITEID) {
4694          return false;
4695      }
4696  
4697      // Allow plugins to use this course before we completely delete it.
4698      if ($pluginsfunction = get_plugins_with_function('pre_course_delete')) {
4699          foreach ($pluginsfunction as $plugintype => $plugins) {
4700              foreach ($plugins as $pluginfunction) {
4701                  $pluginfunction($course);
4702              }
4703          }
4704      }
4705  
4706      // Make the course completely empty.
4707      remove_course_contents($courseid, $showfeedback);
4708  
4709      // Delete the course and related context instance.
4710      context_helper::delete_instance(CONTEXT_COURSE, $courseid);
4711  
4712      $DB->delete_records("course", array("id" => $courseid));
4713      $DB->delete_records("course_format_options", array("courseid" => $courseid));
4714  
4715      // Reset all course related caches here.
4716      if (class_exists('format_base', false)) {
4717          format_base::reset_course_cache($courseid);
4718      }
4719  
4720      // Trigger a course deleted event.
4721      $event = \core\event\course_deleted::create(array(
4722          'objectid' => $course->id,
4723          'context' => $context,
4724          'other' => array(
4725              'shortname' => $course->shortname,
4726              'fullname' => $course->fullname,
4727              'idnumber' => $course->idnumber
4728              )
4729      ));
4730      $event->add_record_snapshot('course', $course);
4731      $event->trigger();
4732  
4733      return true;
4734  }
4735  
4736  /**
4737   * Clear a course out completely, deleting all content but don't delete the course itself.
4738   *
4739   * This function does not verify any permissions.
4740   *
4741   * Please note this function also deletes all user enrolments,
4742   * enrolment instances and role assignments by default.
4743   *
4744   * $options:
4745   *  - 'keep_roles_and_enrolments' - false by default
4746   *  - 'keep_groups_and_groupings' - false by default
4747   *
4748   * @param int $courseid The id of the course that is being deleted
4749   * @param bool $showfeedback Whether to display notifications of each action the function performs.
4750   * @param array $options extra options
4751   * @return bool true if all the removals succeeded. false if there were any failures. If this
4752   *             method returns false, some of the removals will probably have succeeded, and others
4753   *             failed, but you have no way of knowing which.
4754   */
4755  function remove_course_contents($courseid, $showfeedback = true, array $options = null) {
4756      global $CFG, $DB, $OUTPUT;
4757  
4758      require_once($CFG->libdir.'/badgeslib.php');
4759      require_once($CFG->libdir.'/completionlib.php');
4760      require_once($CFG->libdir.'/questionlib.php');
4761      require_once($CFG->libdir.'/gradelib.php');
4762      require_once($CFG->dirroot.'/group/lib.php');
4763      require_once($CFG->dirroot.'/comment/lib.php');
4764      require_once($CFG->dirroot.'/rating/lib.php');
4765      require_once($CFG->dirroot.'/notes/lib.php');
4766  
4767      // Handle course badges.
4768      badges_handle_course_deletion($courseid);
4769  
4770      // NOTE: these concatenated strings are suboptimal, but it is just extra info...
4771      $strdeleted = get_string('deleted').' - ';
4772  
4773      // Some crazy wishlist of stuff we should skip during purging of course content.
4774      $options = (array)$options;
4775  
4776      $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
4777      $coursecontext = context_course::instance($courseid);
4778      $fs = get_file_storage();
4779  
4780      // Delete course completion information, this has to be done before grades and enrols.
4781      $cc = new completion_info($course);
4782      $cc->clear_criteria();
4783      if ($showfeedback) {
4784          echo $OUTPUT->notification($strdeleted.get_string('completion', 'completion'), 'notifysuccess');
4785      }
4786  
4787      // Remove all data from gradebook - this needs to be done before course modules
4788      // because while deleting this information, the system may need to reference
4789      // the course modules that own the grades.
4790      remove_course_grades($courseid, $showfeedback);
4791      remove_grade_letters($coursecontext, $showfeedback);
4792  
4793      // Delete course blocks in any all child contexts,
4794      // they may depend on modules so delete them first.
4795      $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
4796      foreach ($childcontexts as $childcontext) {
4797          blocks_delete_all_for_context($childcontext->id);
4798      }
4799      unset($childcontexts);
4800      blocks_delete_all_for_context($coursecontext->id);
4801      if ($showfeedback) {
4802          echo $OUTPUT->notification($strdeleted.get_string('type_block_plural', 'plugin'), 'notifysuccess');
4803      }
4804  
4805      // Get the list of all modules that are properly installed.
4806      $allmodules = $DB->get_records_menu('modules', array(), '', 'name, id');
4807  
4808      // Delete every instance of every module,
4809      // this has to be done before deleting of course level stuff.
4810      $locations = core_component::get_plugin_list('mod');
4811      foreach ($locations as $modname => $moddir) {
4812          if ($modname === 'NEWMODULE') {
4813              continue;
4814          }
4815          if (array_key_exists($modname, $allmodules)) {
4816              $sql = "SELECT cm.*, m.id AS modinstance, m.name, '$modname' AS modname
4817                FROM {".$modname."} m
4818                     LEFT JOIN {course_modules} cm ON cm.instance = m.id AND cm.module = :moduleid
4819               WHERE m.course = :courseid";
4820              $instances = $DB->get_records_sql($sql, array('courseid' => $course->id,
4821                  'modulename' => $modname, 'moduleid' => $allmodules[$modname]));
4822  
4823              include_once("$moddir/lib.php");                 // Shows php warning only if plugin defective.
4824              $moddelete = $modname .'_delete_instance';       // Delete everything connected to an instance.
4825              $moddeletecourse = $modname .'_delete_course';   // Delete other stray stuff (uncommon).
4826  
4827              if ($instances) {
4828                  foreach ($instances as $cm) {
4829                      if ($cm->id) {
4830                          // Delete activity context questions and question categories.
4831                          question_delete_activity($cm,  $showfeedback);
4832                          // Notify the competency subsystem.
4833                          \core_competency\api::hook_course_module_deleted($cm);
4834                      }
4835                      if (function_exists($moddelete)) {
4836                          // This purges all module data in related tables, extra user prefs, settings, etc.
4837                          $moddelete($cm->modinstance);
4838                      } else {
4839                          // NOTE: we should not allow installation of modules with missing delete support!
4840                          debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!");
4841                          $DB->delete_records($modname, array('id' => $cm->modinstance));
4842                      }
4843  
4844                      if ($cm->id) {
4845                          // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition.
4846                          context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4847                          $DB->delete_records('course_modules', array('id' => $cm->id));
4848                      }
4849                  }
4850              }
4851              if (function_exists($moddeletecourse)) {
4852                  // Execute optional course cleanup callback. Deprecated since Moodle 3.2. TODO MDL-53297 remove in 3.6.
4853                  debugging("Callback delete_course is deprecated. Function $moddeletecourse should be converted " .
4854                      'to observer of event \core\event\course_content_deleted', DEBUG_DEVELOPER);
4855                  $moddeletecourse($course, $showfeedback);
4856              }
4857              if ($instances and $showfeedback) {
4858                  echo $OUTPUT->notification($strdeleted.get_string('pluginname', $modname), 'notifysuccess');
4859              }
4860          } else {
4861              // Ooops, this module is not properly installed, force-delete it in the next block.
4862          }
4863      }
4864  
4865      // We have tried to delete everything the nice way - now let's force-delete any remaining module data.
4866  
4867      // Remove all data from availability and completion tables that is associated
4868      // with course-modules belonging to this course. Note this is done even if the
4869      // features are not enabled now, in case they were enabled previously.
4870      $DB->delete_records_select('course_modules_completion',
4871             'coursemoduleid IN (SELECT id from {course_modules} WHERE course=?)',
4872             array($courseid));
4873  
4874      // Remove course-module data that has not been removed in modules' _delete_instance callbacks.
4875      $cms = $DB->get_records('course_modules', array('course' => $course->id));
4876      $allmodulesbyid = array_flip($allmodules);
4877      foreach ($cms as $cm) {
4878          if (array_key_exists($cm->module, $allmodulesbyid)) {
4879              try {
4880                  $DB->delete_records($allmodulesbyid[$cm->module], array('id' => $cm->instance));
4881              } catch (Exception $e) {
4882                  // Ignore weird or missing table problems.
4883              }
4884          }
4885          context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4886          $DB->delete_records('course_modules', array('id' => $cm->id));
4887      }
4888  
4889      if ($showfeedback) {
4890          echo $OUTPUT->notification($strdeleted.get_string('type_mod_plural', 'plugin'), 'notifysuccess');
4891      }
4892  
4893      // Cleanup the rest of plugins. Deprecated since Moodle 3.2. TODO MDL-53297 remove in 3.6.
4894      $cleanuplugintypes = array('report', 'coursereport', 'format');
4895      $callbacks = get_plugins_with_function('delete_course', 'lib.php');
4896      foreach ($cleanuplugintypes as $type) {
4897          if (!empty($callbacks[$type])) {
4898              foreach ($callbacks[$type] as $pluginfunction) {
4899                  debugging("Callback delete_course is deprecated. Function $pluginfunction should be converted " .
4900                      'to observer of event \core\event\course_content_deleted', DEBUG_DEVELOPER);
4901                  $pluginfunction($course->id, $showfeedback);
4902              }
4903              if ($showfeedback) {
4904                  echo $OUTPUT->notification($strdeleted.get_string('type_'.$type.'_plural', 'plugin'), 'notifysuccess');
4905              }
4906          }
4907      }
4908  
4909      // Delete questions and question categories.
4910      question_delete_course($course, $showfeedback);
4911      if ($showfeedback) {
4912          echo $OUTPUT->notification($strdeleted.get_string('questions', 'question'), 'notifysuccess');
4913      }
4914  
4915      // Make sure there are no subcontexts left - all valid blocks and modules should be already gone.
4916      $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
4917      foreach ($childcontexts as $childcontext) {
4918          $childcontext->delete();
4919      }
4920      unset($childcontexts);
4921  
4922      // Remove all roles and enrolments by default.
4923      if (empty($options['keep_roles_and_enrolments'])) {
4924          // This hack is used in restore when deleting contents of existing course.
4925          role_unassign_all(array('contextid' => $coursecontext->id, 'component' => ''), true);
4926          enrol_course_delete($course);
4927          if ($showfeedback) {
4928              echo $OUTPUT->notification($strdeleted.get_string('type_enrol_plural', 'plugin'), 'notifysuccess');
4929          }
4930      }
4931  
4932      // Delete any groups, removing members and grouping/course links first.
4933      if (empty($options['keep_groups_and_groupings'])) {
4934          groups_delete_groupings($course->id, $showfeedback);
4935          groups_delete_groups($course->id, $showfeedback);
4936      }
4937  
4938      // Filters be gone!
4939      filter_delete_all_for_context($coursecontext->id);
4940  
4941      // Notes, you shall not pass!
4942      note_delete_all($course->id);
4943  
4944      // Die comments!
4945      comment::delete_comments($coursecontext->id);
4946  
4947      // Ratings are history too.
4948      $delopt = new stdclass();
4949      $delopt->contextid = $coursecontext->id;
4950      $rm = new rating_manager();
4951      $rm->delete_ratings($delopt);
4952  
4953      // Delete course tags.
4954      core_tag_tag::remove_all_item_tags('core', 'course', $course->id);
4955  
4956      // Notify the competency subsystem.
4957      \core_competency\api::hook_course_deleted($course);
4958  
4959      // Delete calendar events.
4960      $DB->delete_records('event', array('courseid' => $course->id));
4961      $fs->delete_area_files($coursecontext->id, 'calendar');
4962  
4963      // Delete all related records in other core tables that may have a courseid
4964      // This array stores the tables that need to be cleared, as
4965      // table_name => column_name that contains the course id.
4966      $tablestoclear = array(
4967          'backup_courses' => 'courseid',  // Scheduled backup stuff.
4968          'user_lastaccess' => 'courseid', // User access info.
4969      );
4970      foreach ($tablestoclear as $table => $col) {
4971          $DB->delete_records($table, array($col => $course->id));
4972      }
4973  
4974      // Delete all course backup files.
4975      $fs->delete_area_files($coursecontext->id, 'backup');
4976  
4977      // Cleanup course record - remove links to deleted stuff.
4978      $oldcourse = new stdClass();
4979      $oldcourse->id               = $course->id;
4980      $oldcourse->summary          = '';
4981      $oldcourse->cacherev         = 0;
4982      $oldcourse->legacyfiles      = 0;
4983      if (!empty($options['keep_groups_and_groupings'])) {
4984          $oldcourse->defaultgroupingid = 0;
4985      }
4986      $DB->update_record('course', $oldcourse);
4987  
4988      // Delete course sections.
4989      $DB->delete_records('course_sections', array('course' => $course->id));
4990  
4991      // Delete legacy, section and any other course files.
4992      $fs->delete_area_files($coursecontext->id, 'course'); // Files from summary and section.
4993  
4994      // Delete all remaining stuff linked to context such as files, comments, ratings, etc.
4995      if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) {
4996          // Easy, do not delete the context itself...
4997          $coursecontext->delete_content();
4998      } else {
4999          // Hack alert!!!!
5000          // We can not drop all context stuff because it would bork enrolments and roles,
5001          // there might be also files used by enrol plugins...
5002      }
5003  
5004      // Delete legacy files - just in case some files are still left there after conversion to new file api,
5005      // also some non-standard unsupported plugins may try to store something there.
5006      fulldelete($CFG->dataroot.'/'.$course->id);
5007  
5008      // Delete from cache to reduce the cache size especially makes sense in case of bulk course deletion.
5009      $cachemodinfo = cache::make('core', 'coursemodinfo');
5010      $cachemodinfo->delete($courseid);
5011  
5012      // Trigger a course content deleted event.
5013      $event = \core\event\course_content_deleted::create(array(
5014          'objectid' => $course->id,
5015          'context' => $coursecontext,
5016          'other' => array('shortname' => $course->shortname,
5017                           'fullname' => $course->fullname,
5018                           'options' => $options) // Passing this for legacy reasons.
5019      ));
5020      $event->add_record_snapshot('course', $course);
5021      $event->trigger();
5022  
5023      return true;
5024  }
5025  
5026  /**
5027   * Change dates in module - used from course reset.
5028   *
5029   * @param string $modname forum, assignment, etc
5030   * @param array $fields array of date fields from mod table
5031   * @param int $timeshift time difference
5032   * @param int $courseid
5033   * @param int $modid (Optional) passed if specific mod instance in course needs to be updated.
5034   * @return bool success
5035   */
5036  function shift_course_mod_dates($modname, $fields, $timeshift, $courseid, $modid = 0) {
5037      global $CFG, $DB;
5038      include_once($CFG->dirroot.'/mod/'.$modname.'/lib.php');
5039  
5040      $return = true;
5041      $params = array($timeshift, $courseid);
5042      foreach ($fields as $field) {
5043          $updatesql = "UPDATE {".$modname."}
5044                            SET $field = $field + ?
5045                          WHERE course=? AND $field<>0";
5046          if ($modid) {
5047              $updatesql .= ' AND id=?';
5048              $params[] = $modid;
5049          }
5050          $return = $DB->execute($updatesql, $params) && $return;
5051      }
5052  
5053      $refreshfunction = $modname.'_refresh_events';
5054      if (function_exists($refreshfunction)) {
5055          $refreshfunction($courseid);
5056      }
5057  
5058      return $return;
5059  }
5060  
5061  /**
5062   * This function will empty a course of user data.
5063   * It will retain the activities and the structure of the course.
5064   *
5065   * @param object $data an object containing all the settings including courseid (without magic quotes)
5066   * @return array status array of array component, item, error
5067   */
5068  function reset_course_userdata($data) {
5069      global $CFG, $DB;
5070      require_once($CFG->libdir.'/gradelib.php');
5071      require_once($CFG->libdir.'/completionlib.php');
5072      require_once($CFG->dirroot.'/group/lib.php');
5073  
5074      $data->courseid = $data->id;
5075      $context = context_course::instance($data->courseid);
5076  
5077      $eventparams = array(
5078          'context' => $context,
5079          'courseid' => $data->id,
5080          'other' => array(
5081              'reset_options' => (array) $data
5082          )
5083      );
5084      $event = \core\event\course_reset_started::create($eventparams);
5085      $event->trigger();
5086  
5087      // Calculate the time shift of dates.
5088      if (!empty($data->reset_start_date)) {
5089          // Time part of course startdate should be zero.
5090          $data->timeshift = $data->reset_start_date - usergetmidnight($data->reset_start_date_old);
5091      } else {
5092          $data->timeshift = 0;
5093      }
5094  
5095      // Result array: component, item, error.
5096      $status = array();
5097  
5098      // Start the resetting.
5099      $componentstr = get_string('general');
5100  
5101      // Move the course start time.
5102      if (!empty($data->reset_start_date) and $data->timeshift) {
5103          // Change course start data.
5104          $DB->set_field('course', 'startdate', $data->reset_start_date, array('id' => $data->courseid));
5105          // Update all course and group events - do not move activity events.
5106          $updatesql = "UPDATE {event}
5107                           SET timestart = timestart + ?
5108                         WHERE courseid=? AND instance=0";
5109          $DB->execute($updatesql, array($data->timeshift, $data->courseid));
5110  
5111          // Update any date activity restrictions.
5112          if ($CFG->enableavailability) {
5113              \availability_date\condition::update_all_dates($data->courseid, $data->timeshift);
5114          }
5115  
5116          $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false);
5117      }
5118  
5119      if (!empty($data->reset_events)) {
5120          $DB->delete_records('event', array('courseid' => $data->courseid));
5121          $status[] = array('component' => $componentstr, 'item' => get_string('deleteevents', 'calendar'), 'error' => false);
5122      }
5123  
5124      if (!empty($data->reset_notes)) {
5125          require_once($CFG->dirroot.'/notes/lib.php');
5126          note_delete_all($data->courseid);
5127          $status[] = array('component' => $componentstr, 'item' => get_string('deletenotes', 'notes'), 'error' => false);
5128      }
5129  
5130      if (!empty($data->delete_blog_associations)) {
5131          require_once($CFG->dirroot.'/blog/lib.php');
5132          blog_remove_associations_for_course($data->courseid);
5133          $status[] = array('component' => $componentstr, 'item' => get_string('deleteblogassociations', 'blog'), 'error' => false);
5134      }
5135  
5136      if (!empty($data->reset_completion)) {
5137          // Delete course and activity completion information.
5138          $course = $DB->get_record('course', array('id' => $data->courseid));
5139          $cc = new completion_info($course);
5140          $cc->delete_all_completion_data();
5141          $status[] = array('component' => $componentstr,
5142                  'item' => get_string('deletecompletiondata', 'completion'), 'error' => false);
5143      }
5144  
5145      if (!empty($data->reset_competency_ratings)) {
5146          \core_competency\api::hook_course_reset_competency_ratings($data->courseid);
5147          $status[] = array('component' => $componentstr,
5148              'item' => get_string('deletecompetencyratings', 'core_competency'), 'error' => false);
5149      }
5150  
5151      $componentstr = get_string('roles');
5152  
5153      if (!empty($data->reset_roles_overrides)) {
5154          $children = $context->get_child_contexts();
5155          foreach ($children as $child) {
5156              $DB->delete_records('role_capabilities', array('contextid' => $child->id));
5157          }
5158          $DB->delete_records('role_capabilities', array('contextid' => $context->id));
5159          // Force refresh for logged in users.
5160          $context->mark_dirty();
5161          $status[] = array('component' => $componentstr, 'item' => get_string('deletecourseoverrides', 'role'), 'error' => false);
5162      }
5163  
5164      if (!empty($data->reset_roles_local)) {
5165          $children = $context->get_child_contexts();
5166          foreach ($children as $child) {
5167              role_unassign_all(array('contextid' => $child->id));
5168          }
5169          // Force refresh for logged in users.
5170          $context->mark_dirty();
5171          $status[] = array('component' => $componentstr, 'item' => get_string('deletelocalroles', 'role'), 'error' => false);
5172      }
5173  
5174      // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc.
5175      $data->unenrolled = array();
5176      if (!empty($data->unenrol_users)) {
5177          $plugins = enrol_get_plugins(true);
5178          $instances = enrol_get_instances($data->courseid, true);
5179          foreach ($instances as $key => $instance) {
5180              if (!isset($plugins[$instance->enrol])) {
5181                  unset($instances[$key]);
5182                  continue;
5183              }
5184          }
5185  
5186          foreach ($data->unenrol_users as $withroleid) {
5187              if ($withroleid) {
5188                  $sql = "SELECT ue.*
5189                            FROM {user_enrolments} ue
5190                            JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5191                            JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5192                            JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)";
5193                  $params = array('courseid' => $data->courseid, 'roleid' => $withroleid, 'courselevel' => CONTEXT_COURSE);
5194  
5195              } else {
5196                  // Without any role assigned at course context.
5197                  $sql = "SELECT ue.*
5198                            FROM {user_enrolments} ue
5199                            JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5200                            JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5201                       LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid)
5202                           WHERE ra.id IS null";
5203                  $params = array('courseid' => $data->courseid, 'courselevel' => CONTEXT_COURSE);
5204              }
5205  
5206              $rs = $DB->get_recordset_sql($sql, $params);
5207              foreach ($rs as $ue) {
5208                  if (!isset($instances[$ue->enrolid])) {
5209                      continue;
5210                  }
5211                  $instance = $instances[$ue->enrolid];
5212                  $plugin = $plugins[$instance->enrol];
5213                  if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) {
5214                      continue;
5215                  }
5216  
5217                  $plugin->unenrol_user($instance, $ue->userid);
5218                  $data->unenrolled[$ue->userid] = $ue->userid;
5219              }
5220              $rs->close();
5221          }
5222      }
5223      if (!empty($data->unenrolled)) {
5224          $status[] = array(
5225              'component' => $componentstr,
5226              'item' => get_string('unenrol', 'enrol').' ('.count($data->unenrolled).')',
5227              'error' => false
5228          );
5229      }
5230  
5231      $componentstr = get_string('groups');
5232  
5233      // Remove all group members.
5234      if (!empty($data->reset_groups_members)) {
5235          groups_delete_group_members($data->courseid);
5236          $status[] = array('component' => $componentstr, 'item' => get_string('removegroupsmembers', 'group'), 'error' => false);
5237      }
5238  
5239      // Remove all groups.
5240      if (!empty($data->reset_groups_remove)) {
5241          groups_delete_groups($data->courseid, false);
5242          $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroups', 'group'), 'error' => false);
5243      }
5244  
5245      // Remove all grouping members.
5246      if (!empty($data->reset_groupings_members)) {
5247          groups_delete_groupings_groups($data->courseid, false);
5248          $status[] = array('component' => $componentstr, 'item' => get_string('removegroupingsmembers', 'group'), 'error' => false);
5249      }
5250  
5251      // Remove all groupings.
5252      if (!empty($data->reset_groupings_remove)) {
5253          groups_delete_groupings($data->courseid, false);
5254          $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroupings', 'group'), 'error' => false);
5255      }
5256  
5257      // Look in every instance of every module for data to delete.
5258      $unsupportedmods = array();
5259      if ($allmods = $DB->get_records('modules') ) {
5260          foreach ($allmods as $mod) {
5261              $modname = $mod->name;
5262              $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php';
5263              $moddeleteuserdata = $modname.'_reset_userdata';   // Function to delete user data.
5264              if (file_exists($modfile)) {
5265                  if (!$DB->count_records($modname, array('course' => $data->courseid))) {
5266                      continue; // Skip mods with no instances.
5267                  }
5268                  include_once($modfile);
5269                  if (function_exists($moddeleteuserdata)) {
5270                      $modstatus = $moddeleteuserdata($data);
5271                      if (is_array($modstatus)) {
5272                          $status = array_merge($status, $modstatus);
5273                      } else {
5274                          debugging('Module '.$modname.' returned incorrect staus - must be an array!');
5275                      }
5276                  } else {
5277                      $unsupportedmods[] = $mod;
5278                  }
5279              } else {
5280                  debugging('Missing lib.php in '.$modname.' module!');
5281              }
5282          }
5283      }
5284  
5285      // Mention unsupported mods.
5286      if (!empty($unsupportedmods)) {
5287          foreach ($unsupportedmods as $mod) {
5288              $status[] = array(
5289                  'component' => get_string('modulenameplural', $mod->name),
5290                  'item' => '',
5291                  'error' => get_string('resetnotimplemented')
5292              );
5293          }
5294      }
5295  
5296      $componentstr = get_string('gradebook', 'grades');
5297      // Reset gradebook,.
5298      if (!empty($data->reset_gradebook_items)) {
5299          remove_course_grades($data->courseid, false);
5300          grade_grab_course_grades($data->courseid);
5301          grade_regrade_final_grades($data->courseid);
5302          $status[] = array('component' => $componentstr, 'item' => get_string('removeallcourseitems', 'grades'), 'error' => false);
5303  
5304      } else if (!empty($data->reset_gradebook_grades)) {
5305          grade_course_reset($data->courseid);
5306          $status[] = array('component' => $componentstr, 'item' => get_string('removeallcoursegrades', 'grades'), 'error' => false);
5307      }
5308      // Reset comments.
5309      if (!empty($data->reset_comments)) {
5310          require_once($CFG->dirroot.'/comment/lib.php');
5311          comment::reset_course_page_comments($context);
5312      }
5313  
5314      $event = \core\event\course_reset_ended::create($eventparams);
5315      $event->trigger();
5316  
5317      return $status;
5318  }
5319  
5320  /**
5321   * Generate an email processing address.
5322   *
5323   * @param int $modid
5324   * @param string $modargs
5325   * @return string Returns email processing address
5326   */
5327  function generate_email_processing_address($modid, $modargs) {
5328      global $CFG;
5329  
5330      $header = $CFG->mailprefix . substr(base64_encode(pack('C', $modid)), 0, 2).$modargs;
5331      return $header . substr(md5($header.get_site_identifier()), 0, 16).'@'.$CFG->maildomain;
5332  }
5333  
5334  /**
5335   * ?
5336   *
5337   * @todo Finish documenting this function
5338   *
5339   * @param string $modargs
5340   * @param string $body Currently unused
5341   */
5342  function moodle_process_email($modargs, $body) {
5343      global $DB;
5344  
5345      // The first char should be an unencoded letter. We'll take this as an action.
5346      switch ($modargs{0}) {
5347          case 'B': { // Bounce.
5348              list(, $userid) = unpack('V', base64_decode(substr($modargs, 1, 8)));
5349              if ($user = $DB->get_record("user", array('id' => $userid), "id,email")) {
5350                  // Check the half md5 of their email.
5351                  $md5check = substr(md5($user->email), 0, 16);
5352                  if ($md5check == substr($modargs, -16)) {
5353                      set_bounce_count($user);
5354                  }
5355                  // Else maybe they've already changed it?
5356              }
5357          }
5358          break;
5359          // Maybe more later?
5360      }
5361  }
5362  
5363  // CORRESPONDENCE.
5364  
5365  /**
5366   * Get mailer instance, enable buffering, flush buffer or disable buffering.
5367   *
5368   * @param string $action 'get', 'buffer', 'close' or 'flush'
5369   * @return moodle_phpmailer|null mailer instance if 'get' used or nothing
5370   */
5371  function get_mailer($action='get') {
5372      global $CFG;
5373  
5374      /** @var moodle_phpmailer $mailer */
5375      static $mailer  = null;
5376      static $counter = 0;
5377  
5378      if (!isset($CFG->smtpmaxbulk)) {
5379          $CFG->smtpmaxbulk = 1;
5380      }
5381  
5382      if ($action == 'get') {
5383          $prevkeepalive = false;
5384  
5385          if (isset($mailer) and $mailer->Mailer == 'smtp') {
5386              if ($counter < $CFG->smtpmaxbulk and !$mailer->isError()) {
5387                  $counter++;
5388                  // Reset the mailer.
5389                  $mailer->Priority         = 3;
5390                  $mailer->CharSet          = 'UTF-8'; // Our default.
5391                  $mailer->ContentType      = "text/plain";
5392                  $mailer->Encoding         = "8bit";
5393                  $mailer->From             = "root@localhost";
5394                  $mailer->FromName         = "Root User";
5395                  $mailer->Sender           = "";
5396                  $mailer->Subject          = "";
5397                  $mailer->Body             = "";
5398                  $mailer->AltBody          = "";
5399                  $mailer->ConfirmReadingTo = "";
5400  
5401                  $mailer->clearAllRecipients();
5402                  $mailer->clearReplyTos();
5403                  $mailer->clearAttachments();
5404                  $mailer->clearCustomHeaders();
5405                  return $mailer;
5406              }
5407  
5408              $prevkeepalive = $mailer->SMTPKeepAlive;
5409              get_mailer('flush');
5410          }
5411  
5412          require_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php');
5413          $mailer = new moodle_phpmailer();
5414  
5415          $counter = 1;
5416  
5417          if ($CFG->smtphosts == 'qmail') {
5418              // Use Qmail system.
5419              $mailer->isQmail();
5420  
5421          } else if (empty($CFG->smtphosts)) {
5422              // Use PHP mail() = sendmail.
5423              $mailer->isMail();
5424  
5425          } else {
5426              // Use SMTP directly.
5427              $mailer->isSMTP();
5428              if (!empty($CFG->debugsmtp)) {
5429                  $mailer->SMTPDebug = true;
5430              }
5431              // Specify main and backup servers.
5432              $mailer->Host          = $CFG->smtphosts;
5433              // Specify secure connection protocol.
5434              $mailer->SMTPSecure    = $CFG->smtpsecure;
5435              // Use previous keepalive.
5436              $mailer->SMTPKeepAlive = $prevkeepalive;
5437  
5438              if ($CFG->smtpuser) {
5439                  // Use SMTP authentication.
5440                  $mailer->SMTPAuth = true;
5441                  $mailer->Username = $CFG->smtpuser;
5442                  $mailer->Password = $CFG->smtppass;
5443              }
5444          }
5445  
5446          return $mailer;
5447      }
5448  
5449      $nothing = null;
5450  
5451      // Keep smtp session open after sending.
5452      if ($action == 'buffer') {
5453          if (!empty($CFG->smtpmaxbulk)) {
5454              get_mailer('flush');
5455              $m = get_mailer();
5456              if ($m->Mailer == 'smtp') {
5457                  $m->SMTPKeepAlive = true;
5458              }
5459          }
5460          return $nothing;
5461      }
5462  
5463      // Close smtp session, but continue buffering.
5464      if ($action == 'flush') {
5465          if (isset($mailer) and $mailer->Mailer == 'smtp') {
5466              if (!empty($mailer->SMTPDebug)) {
5467                  echo '<pre>'."\n";
5468              }
5469              $mailer->SmtpClose();
5470              if (!empty($mailer->SMTPDebug)) {
5471                  echo '</pre>';
5472              }
5473          }
5474          return $nothing;
5475      }
5476  
5477      // Close smtp session, do not buffer anymore.
5478      if ($action == 'close') {
5479          if (isset($mailer) and $mailer->Mailer == 'smtp') {
5480              get_mailer('flush');
5481              $mailer->SMTPKeepAlive = false;
5482          }
5483          $mailer = null; // Better force new instance.
5484          return $nothing;
5485      }
5486  }
5487  
5488  /**
5489   * A helper function to test for email diversion
5490   *
5491   * @param string $email
5492   * @return bool Returns true if the email should be diverted
5493   */
5494  function email_should_be_diverted($email) {
5495      global $CFG;
5496  
5497      if (empty($CFG->divertallemailsto)) {
5498          return false;
5499      }
5500  
5501      if (empty($CFG->divertallemailsexcept)) {
5502          return true;
5503      }
5504  
5505      $patterns = array_map('trim', explode(',', $CFG->divertallemailsexcept));
5506      foreach ($patterns as $pattern) {
5507          if (preg_match("/$pattern/", $email)) {
5508              return false;
5509          }
5510      }
5511  
5512      return true;
5513  }
5514  
5515  /**
5516   * Generate a unique email Message-ID using the moodle domain and install path
5517   *
5518   * @param string $localpart An optional unique message id prefix.
5519   * @return string The formatted ID ready for appending to the email headers.
5520   */
5521  function generate_email_messageid($localpart = null) {
5522      global $CFG;
5523  
5524      $urlinfo = parse_url($CFG->wwwroot);
5525      $base = '@' . $urlinfo['host'];
5526  
5527      // If multiple moodles are on the same domain we want to tell them
5528      // apart so we add the install path to the local part. This means
5529      // that the id local part should never contain a / character so
5530      // we can correctly parse the id to reassemble the wwwroot.
5531      if (isset($urlinfo['path'])) {
5532          $base = $urlinfo['path'] . $base;
5533      }
5534  
5535      if (empty($localpart)) {
5536          $localpart = uniqid('', true);
5537      }
5538  
5539      // Because we may have an option /installpath suffix to the local part
5540      // of the id we need to escape any / chars which are in the $localpart.
5541      $localpart = str_replace('/', '%2F', $localpart);
5542  
5543      return '<' . $localpart . $base . '>';
5544  }
5545  
5546  /**
5547   * Send an email to a specified user
5548   *
5549   * @param stdClass $user  A {@link $USER} object
5550   * @param stdClass $from A {@link $USER} object
5551   * @param string $subject plain text subject line of the email
5552   * @param string $messagetext plain text version of the message
5553   * @param string $messagehtml complete html version of the message (optional)
5554   * @param string $attachment a file on the filesystem, either relative to $CFG->dataroot or a full path to a file in $CFG->tempdir
5555   * @param string $attachname the name of the file (extension indicates MIME)
5556   * @param bool $usetrueaddress determines whether $from email address should
5557   *          be sent out. Will be overruled by user profile setting for maildisplay
5558   * @param string $replyto Email address to reply to
5559   * @param string $replytoname Name of reply to recipient
5560   * @param int $wordwrapwidth custom word wrap width, default 79
5561   * @return bool Returns true if mail was sent OK and false if there was an error.
5562   */
5563  function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '', $attachment = '', $attachname = '',
5564                         $usetrueaddress = true, $replyto = '', $replytoname = '', $wordwrapwidth = 79) {
5565  
5566      global $CFG, $PAGE, $SITE;
5567  
5568      if (empty($user) or empty($user->id)) {
5569          debugging('Can not send email to null user', DEBUG_DEVELOPER);
5570          return false;
5571      }
5572  
5573      if (empty($user->email)) {
5574          debugging('Can not send email to user without email: '.$user->id, DEBUG_DEVELOPER);
5575          return false;
5576      }
5577  
5578      if (!empty($user->deleted)) {
5579          debugging('Can not send email to deleted user: '.$user->id, DEBUG_DEVELOPER);
5580          return false;
5581      }
5582  
5583      if (defined('BEHAT_SITE_RUNNING')) {
5584          // Fake email sending in behat.
5585          return true;
5586      }
5587  
5588      if (!empty($CFG->noemailever)) {
5589          // Hidden setting for development sites, set in config.php if needed.
5590          debugging('Not sending email due to $CFG->noemailever config setting', DEBUG_NORMAL);
5591          return true;
5592      }
5593  
5594      if (email_should_be_diverted($user->email)) {
5595          $subject = "[DIVERTED {$user->email}] $subject";
5596          $user = clone($user);
5597          $user->email = $CFG->divertallemailsto;
5598      }
5599  
5600      // Skip mail to suspended users.
5601      if ((isset($user->auth) && $user->auth=='nologin') or (isset($user->suspended) && $user->suspended)) {
5602          return true;
5603      }
5604  
5605      if (!validate_email($user->email)) {
5606          // We can not send emails to invalid addresses - it might create security issue or confuse the mailer.
5607          debugging("email_to_user: User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending.");
5608          return false;
5609      }
5610  
5611      if (over_bounce_threshold($user)) {
5612          debugging("email_to_user: User $user->id (".fullname($user).") is over bounce threshold! Not sending.");
5613          return false;
5614      }
5615  
5616      // TLD .invalid  is specifically reserved for invalid domain names.
5617      // For More information, see {@link http://tools.ietf.org/html/rfc2606#section-2}.
5618      if (substr($user->email, -8) == '.invalid') {
5619          debugging("email_to_user: User $user->id (".fullname($user).") email domain ($user->email) is invalid! Not sending.");
5620          return true; // This is not an error.
5621      }
5622  
5623      // If the user is a remote mnet user, parse the email text for URL to the
5624      // wwwroot and modify the url to direct the user's browser to login at their
5625      // home site (identity provider - idp) before hitting the link itself.
5626      if (is_mnet_remote_user($user)) {
5627          require_once($CFG->dirroot.'/mnet/lib.php');
5628  
5629          $jumpurl = mnet_get_idp_jump_url($user);
5630          $callback = partial('mnet_sso_apply_indirection', $jumpurl);
5631  
5632          $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%",
5633                  $callback,
5634                  $messagetext);
5635          $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%",
5636                  $callback,
5637                  $messagehtml);
5638      }
5639      $mail = get_mailer();
5640  
5641      if (!empty($mail->SMTPDebug)) {
5642          echo '<pre>' . "\n";
5643      }
5644  
5645      $temprecipients = array();
5646      $tempreplyto = array();
5647  
5648      $supportuser = core_user::get_support_user();
5649  
5650      // Make up an email address for handling bounces.
5651      if (!empty($CFG->handlebounces)) {
5652          $modargs = 'B'.base64_encode(pack('V', $user->id)).substr(md5($user->email), 0, 16);
5653          $mail->Sender = generate_email_processing_address(0, $modargs);
5654      } else {
5655          $mail->Sender = $supportuser->email;
5656      }
5657  
5658      if (!empty($CFG->emailonlyfromnoreplyaddress)) {
5659          $usetrueaddress = false;
5660          if (empty($replyto) && $from->maildisplay) {
5661              $replyto = $from->email;
5662              $replytoname = fullname($from);
5663          }
5664      }
5665  
5666      if (is_string($from)) { // So we can pass whatever we want if there is need.
5667          $mail->From     = $CFG->noreplyaddress;
5668          $mail->FromName = $from;
5669      } else if ($usetrueaddress and $from->maildisplay) {
5670          $mail->From     = $from->email;
5671          $mail->FromName = fullname($from);
5672      } else {
5673          $mail->From     = $CFG->noreplyaddress;
5674          $mail->FromName = fullname($from);
5675          if (empty($replyto)) {
5676              $tempreplyto[] = array($CFG->noreplyaddress, get_string('noreplyname'));
5677          }
5678      }
5679  
5680      if (!empty($replyto)) {
5681          $tempreplyto[] = array($replyto, $replytoname);
5682      }
5683  
5684      $temprecipients[] = array($user->email, fullname($user));
5685  
5686      // Set word wrap.
5687      $mail->WordWrap = $wordwrapwidth;
5688  
5689      if (!empty($from->customheaders)) {
5690          // Add custom headers.
5691          if (is_array($from->customheaders)) {
5692              foreach ($from->customheaders as $customheader) {
5693                  $mail->addCustomHeader($customheader);
5694              }
5695          } else {
5696              $mail->addCustomHeader($from->customheaders);
5697          }
5698      }
5699  
5700      // If the X-PHP-Originating-Script email header is on then also add an additional
5701      // header with details of where exactly in moodle the email was triggered from,
5702      // either a call to message_send() or to email_to_user().
5703      if (ini_get('mail.add_x_header')) {
5704  
5705          $stack = debug_backtrace(false);
5706          $origin = $stack[0];
5707  
5708          foreach ($stack as $depth => $call) {
5709              if ($call['function'] == 'message_send') {
5710                  $origin = $call;
5711              }
5712          }
5713  
5714          $originheader = $CFG->wwwroot . ' => ' . gethostname() . ':'
5715               . str_replace($CFG->dirroot . '/', '', $origin['file']) . ':' . $origin['line'];
5716          $mail->addCustomHeader('X-Moodle-Originating-Script: ' . $originheader);
5717      }
5718  
5719      if (!empty($from->priority)) {
5720          $mail->Priority = $from->priority;
5721      }
5722  
5723      $renderer = $PAGE->get_renderer('core');
5724      $context = array(
5725          'sitefullname' => $SITE->fullname,
5726          'siteshortname' => $SITE->shortname,
5727          'sitewwwroot' => $CFG->wwwroot,
5728          'subject' => $subject,
5729          'to' => $user->email,
5730          'toname' => fullname($user),
5731          'from' => $mail->From,
5732          'fromname' => $mail->FromName,
5733      );
5734      if (!empty($tempreplyto[0])) {
5735          $context['replyto'] = $tempreplyto[0][0];
5736          $context['replytoname'] = $tempreplyto[0][1];
5737      }
5738      if ($user->id > 0) {
5739          $context['touserid'] = $user->id;
5740          $context['tousername'] = $user->username;
5741      }
5742  
5743      if (!empty($user->mailformat) && $user->mailformat == 1) {
5744          // Only process html templates if the user preferences allow html email.
5745  
5746          if ($messagehtml) {
5747              // If html has been given then pass it through the template.
5748              $context['body'] = $messagehtml;
5749              $messagehtml = $renderer->render_from_template('core/email_html', $context);
5750  
5751          } else {
5752              // If no html has been given, BUT there is an html wrapping template then
5753              // auto convert the text to html and then wrap it.
5754              $autohtml = trim(text_to_html($messagetext));
5755              $context['body'] = $autohtml;
5756              $temphtml = $renderer->render_from_template('core/email_html', $context);
5757              if ($autohtml != $temphtml) {
5758                  $messagehtml = $temphtml;
5759              }
5760          }
5761      }
5762  
5763      $context['body'] = $messagetext;
5764      $mail->Subject = $renderer->render_from_template('core/email_subject', $context);
5765      $mail->FromName = $renderer->render_from_template('core/email_fromname', $context);
5766      $messagetext = $renderer->render_from_template('core/email_text', $context);
5767  
5768      // Autogenerate a MessageID if it's missing.
5769      if (empty($mail->MessageID)) {
5770          $mail->MessageID = generate_email_messageid();
5771      }
5772  
5773      if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) {
5774          // Don't ever send HTML to users who don't want it.
5775          $mail->isHTML(true);
5776          $mail->Encoding = 'quoted-printable';
5777          $mail->Body    =  $messagehtml;
5778          $mail->AltBody =  "\n$messagetext\n";
5779      } else {
5780          $mail->IsHTML(false);
5781          $mail->Body =  "\n$messagetext\n";
5782      }
5783  
5784      if ($attachment && $attachname) {
5785          if (preg_match( "~\\.\\.~" , $attachment )) {
5786              // Security check for ".." in dir path.
5787              $temprecipients[] = array($supportuser->email, fullname($supportuser, true));
5788              $mail->addStringAttachment('Error in attachment.  User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain');
5789          } else {
5790              require_once($CFG->libdir.'/filelib.php');
5791              $mimetype = mimeinfo('type', $attachname);
5792  
5793              $attachmentpath = $attachment;
5794  
5795              // Before doing the comparison, make sure that the paths are correct (Windows uses slashes in the other direction).
5796              $attachpath = str_replace('\\', '/', $attachmentpath);
5797              // Make sure both variables are normalised before comparing.
5798              $temppath = str_replace('\\', '/', realpath($CFG->tempdir));
5799  
5800              // If the attachment is a full path to a file in the tempdir, use it as is,
5801              // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons).
5802              if (strpos($attachpath, $temppath) !== 0) {
5803                  $attachmentpath = $CFG->dataroot . '/' . $attachmentpath;
5804              }
5805  
5806              $mail->addAttachment($attachmentpath, $attachname, 'base64', $mimetype);
5807          }
5808      }
5809  
5810      // Check if the email should be sent in an other charset then the default UTF-8.
5811      if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) {
5812  
5813          // Use the defined site mail charset or eventually the one preferred by the recipient.
5814          $charset = $CFG->sitemailcharset;
5815          if (!empty($CFG->allowusermailcharset)) {
5816              if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) {
5817                  $charset = $useremailcharset;
5818              }
5819          }
5820  
5821          // Convert all the necessary strings if the charset is supported.
5822          $charsets = get_list_of_charsets();
5823          unset($charsets['UTF-8']);
5824          if (in_array($charset, $charsets)) {
5825              $mail->CharSet  = $charset;
5826              $mail->FromName = core_text::convert($mail->FromName, 'utf-8', strtolower($charset));
5827              $mail->Subject  = core_text::convert($mail->Subject, 'utf-8', strtolower($charset));
5828              $mail->Body     = core_text::convert($mail->Body, 'utf-8', strtolower($charset));
5829              $mail->AltBody  = core_text::convert($mail->AltBody, 'utf-8', strtolower($charset));
5830  
5831              foreach ($temprecipients as $key => $values) {
5832                  $temprecipients[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
5833              }
5834              foreach ($tempreplyto as $key => $values) {
5835                  $tempreplyto[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
5836              }
5837          }
5838      }
5839  
5840      foreach ($temprecipients as $values) {
5841          $mail->addAddress($values[0], $values[1]);
5842      }
5843      foreach ($tempreplyto as $values) {
5844          $mail->addReplyTo($values[0], $values[1]);
5845      }
5846  
5847      if ($mail->send()) {
5848          set_send_count($user);
5849          if (!empty($mail->SMTPDebug)) {
5850              echo '</pre>';
5851          }
5852          return true;
5853      } else {
5854          // Trigger event for failing to send email.
5855          $event = \core\event\email_failed::create(array(
5856              'context' => context_system::instance(),
5857              'userid' => $from->id,
5858              'relateduserid' => $user->id,
5859              'other' => array(
5860                  'subject' => $subject,
5861                  'message' => $messagetext,
5862                  'errorinfo' => $mail->ErrorInfo
5863              )
5864          ));
5865          $event->trigger();
5866          if (CLI_SCRIPT) {
5867              mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo);
5868          }
5869          if (!empty($mail->SMTPDebug)) {
5870              echo '</pre>';
5871          }
5872          return false;
5873      }
5874  }
5875  
5876  /**
5877   * Generate a signoff for emails based on support settings
5878   *
5879   * @return string
5880   */
5881  function generate_email_signoff() {
5882      global $CFG;
5883  
5884      $signoff = "\n";
5885      if (!empty($CFG->supportname)) {
5886          $signoff .= $CFG->supportname."\n";
5887      }
5888      if (!empty($CFG->supportemail)) {
5889          $signoff .= $CFG->supportemail."\n";
5890      }
5891      if (!empty($CFG->supportpage)) {
5892          $signoff .= $CFG->supportpage."\n";
5893      }
5894      return $signoff;
5895  }
5896  
5897  /**
5898   * Sets specified user's password and send the new password to the user via email.
5899   *
5900   * @param stdClass $user A {@link $USER} object
5901   * @param bool $fasthash If true, use a low cost factor when generating the hash for speed.
5902   * @return bool|string Returns "true" if mail was sent OK and "false" if there was an error
5903   */
5904  function setnew_password_and_mail($user, $fasthash = false) {
5905      global $CFG, $DB;
5906  
5907      // We try to send the mail in language the user understands,
5908      // unfortunately the filter_string() does not support alternative langs yet
5909      // so multilang will not work properly for site->fullname.
5910      $lang = empty($user->lang) ? $CFG->lang : $user->lang;
5911  
5912      $site  = get_site();
5913  
5914      $supportuser = core_user::get_support_user();
5915  
5916      $newpassword = generate_password();
5917  
5918      update_internal_user_password($user, $newpassword, $fasthash);
5919  
5920      $a = new stdClass();
5921      $a->firstname   = fullname($user, true);
5922      $a->sitename    = format_string($site->fullname);
5923      $a->username    = $user->username;
5924      $a->newpassword = $newpassword;
5925      $a->link        = $CFG->wwwroot .'/login/';
5926      $a->signoff     = generate_email_signoff();
5927  
5928      $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang);
5929  
5930      $subject = format_string($site->fullname) .': '. (string)new lang_string('newusernewpasswordsubj', '', $a, $lang);
5931  
5932      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
5933      return email_to_user($user, $supportuser, $subject, $message);
5934  
5935  }
5936  
5937  /**
5938   * Resets specified user's password and send the new password to the user via email.
5939   *
5940   * @param stdClass $user A {@link $USER} object
5941   * @return bool Returns true if mail was sent OK and false if there was an error.
5942   */
5943  function reset_password_and_mail($user) {
5944      global $CFG;
5945  
5946      $site  = get_site();
5947      $supportuser = core_user::get_support_user();
5948  
5949      $userauth = get_auth_plugin($user->auth);
5950      if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) {
5951          trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth.");
5952          return false;
5953      }
5954  
5955      $newpassword = generate_password();
5956  
5957      if (!$userauth->user_update_password($user, $newpassword)) {
5958          print_error("cannotsetpassword");
5959      }
5960  
5961      $a = new stdClass();
5962      $a->firstname   = $user->firstname;
5963      $a->lastname    = $user->lastname;
5964      $a->sitename    = format_string($site->fullname);
5965      $a->username    = $user->username;
5966      $a->newpassword = $newpassword;
5967      $a->link        = $CFG->httpswwwroot .'/login/change_password.php';
5968      $a->signoff     = generate_email_signoff();
5969  
5970      $message = get_string('newpasswordtext', '', $a);
5971  
5972      $subject  = format_string($site->fullname) .': '. get_string('changedpassword');
5973  
5974      unset_user_preference('create_password', $user); // Prevent cron from generating the password.
5975  
5976      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
5977      return email_to_user($user, $supportuser, $subject, $message);
5978  }
5979  
5980  /**
5981   * Send email to specified user with confirmation text and activation link.
5982   *
5983   * @param stdClass $user A {@link $USER} object
5984   * @return bool Returns true if mail was sent OK and false if there was an error.
5985   */
5986  function send_confirmation_email($user) {
5987      global $CFG;
5988  
5989      $site = get_site();
5990      $supportuser = core_user::get_support_user();
5991  
5992      $data = new stdClass();
5993      $data->firstname = fullname($user);
5994      $data->sitename  = format_string($site->fullname);
5995      $data->admin     = generate_email_signoff();
5996  
5997      $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname));
5998  
5999      $username = urlencode($user->username);
6000      $username = str_replace('.', '%2E', $username); // Prevent problems with trailing dots.
6001      $data->link  = $CFG->wwwroot .'/login/confirm.php?data='. $user->secret .'/'. $username;
6002      $message     = get_string('emailconfirmation', '', $data);
6003      $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true);
6004  
6005      $user->mailformat = 1;  // Always send HTML version as well.
6006  
6007      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6008      return email_to_user($user, $supportuser, $subject, $message, $messagehtml);
6009  }
6010  
6011  /**
6012   * Sends a password change confirmation email.
6013   *
6014   * @param stdClass $user A {@link $USER} object
6015   * @param stdClass $resetrecord An object tracking metadata regarding password reset request
6016   * @return bool Returns true if mail was sent OK and false if there was an error.
6017   */
6018  function send_password_change_confirmation_email($user, $resetrecord) {
6019      global $CFG;
6020  
6021      $site = get_site();
6022      $supportuser = core_user::get_support_user();
6023      $pwresetmins = isset($CFG->pwresettime) ? floor($CFG->pwresettime / MINSECS) : 30;
6024  
6025      $data = new stdClass();
6026      $data->firstname = $user->firstname;
6027      $data->lastname  = $user->lastname;
6028      $data->username  = $user->username;
6029      $data->sitename  = format_string($site->fullname);
6030      $data->link      = $CFG->httpswwwroot .'/login/forgot_password.php?token='. $resetrecord->token;
6031      $data->admin     = generate_email_signoff();
6032      $data->resetminutes = $pwresetmins;
6033  
6034      $message = get_string('emailresetconfirmation', '', $data);
6035      $subject = get_string('emailresetconfirmationsubject', '', format_string($site->fullname));
6036  
6037      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6038      return email_to_user($user, $supportuser, $subject, $message);
6039  
6040  }
6041  
6042  /**
6043   * Sends an email containinginformation on how to change your password.
6044   *
6045   * @param stdClass $user A {@link $USER} object
6046   * @return bool Returns true if mail was sent OK and false if there was an error.
6047   */
6048  function send_password_change_info($user) {
6049      global $CFG;
6050  
6051      $site = get_site();
6052      $supportuser = core_user::get_support_user();
6053      $systemcontext = context_system::instance();
6054  
6055      $data = new stdClass();
6056      $data->firstname = $user->firstname;
6057      $data->lastname  = $user->lastname;
6058      $data->sitename  = format_string($site->fullname);
6059      $data->admin     = generate_email_signoff();
6060  
6061      $userauth = get_auth_plugin($user->auth);
6062  
6063      if (!is_enabled_auth($user->auth) or $user->auth == 'nologin') {
6064          $message = get_string('emailpasswordchangeinfodisabled', '', $data);
6065          $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
6066          // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6067          return email_to_user($user, $supportuser, $subject, $message);
6068      }
6069  
6070      if ($userauth->can_change_password() and $userauth->change_password_url()) {
6071          // We have some external url for password changing.
6072          $data->link .= $userauth->change_password_url();
6073  
6074      } else {
6075          // No way to change password, sorry.
6076          $data->link = '';
6077      }
6078  
6079      if (!empty($data->link) and has_capability('moodle/user:changeownpassword', $systemcontext, $user->id)) {
6080          $message = get_string('emailpasswordchangeinfo', '', $data);
6081          $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
6082      } else {
6083          $message = get_string('emailpasswordchangeinfofail', '', $data);
6084          $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
6085      }
6086  
6087      // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6088      return email_to_user($user, $supportuser, $subject, $message);
6089  
6090  }
6091  
6092  /**
6093   * Check that an email is allowed.  It returns an error message if there was a problem.
6094   *
6095   * @param string $email Content of email
6096   * @return string|false
6097   */
6098  function email_is_not_allowed($email) {
6099      global $CFG;
6100  
6101      if (!empty($CFG->allowemailaddresses)) {
6102          $allowed = explode(' ', $CFG->allowemailaddresses);
6103          foreach ($allowed as $allowedpattern) {
6104              $allowedpattern = trim($allowedpattern);
6105              if (!$allowedpattern) {
6106                  continue;
6107              }
6108              if (strpos($allowedpattern, '.') === 0) {
6109                  if (strpos(strrev($email), strrev($allowedpattern)) === 0) {
6110                      // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6111                      return false;
6112                  }
6113  
6114              } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) {
6115                  return false;
6116              }
6117          }
6118          return get_string('emailonlyallowed', '', $CFG->allowemailaddresses);
6119  
6120      } else if (!empty($CFG->denyemailaddresses)) {
6121          $denied = explode(' ', $CFG->denyemailaddresses);
6122          foreach ($denied as $deniedpattern) {
6123              $deniedpattern = trim($deniedpattern);
6124              if (!$deniedpattern) {
6125                  continue;
6126              }
6127              if (strpos($deniedpattern, '.') === 0) {
6128                  if (strpos(strrev($email), strrev($deniedpattern)) === 0) {
6129                      // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6130                      return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6131                  }
6132  
6133              } else if (strpos(strrev($email), strrev('@'.$deniedpattern)) === 0) {
6134                  return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6135              }
6136          }
6137      }
6138  
6139      return false;
6140  }
6141  
6142  // FILE HANDLING.
6143  
6144  /**
6145   * Returns local file storage instance
6146   *
6147   * @return file_storage
6148   */
6149  function get_file_storage() {
6150      global $CFG;
6151  
6152      static $fs = null;
6153  
6154      if ($fs) {
6155          return $fs;
6156      }
6157  
6158      require_once("$CFG->libdir/filelib.php");
6159  
6160      if (isset($CFG->filedir)) {
6161          $filedir = $CFG->filedir;
6162      } else {
6163          $filedir = $CFG->dataroot.'/filedir';
6164      }
6165  
6166      if (isset($CFG->trashdir)) {
6167          $trashdirdir = $CFG->trashdir;
6168      } else {
6169          $trashdirdir = $CFG->dataroot.'/trashdir';
6170      }
6171  
6172      $fs = new file_storage($filedir, $trashdirdir, "$CFG->tempdir/filestorage", $CFG->directorypermissions, $CFG->filepermissions);
6173  
6174      return $fs;
6175  }
6176  
6177  /**
6178   * Returns local file storage instance
6179   *
6180   * @return file_browser
6181   */
6182  function get_file_browser() {
6183      global $CFG;
6184  
6185      static $fb = null;
6186  
6187      if ($fb) {
6188          return $fb;
6189      }
6190  
6191      require_once("$CFG->libdir/filelib.php");
6192  
6193      $fb = new file_browser();
6194  
6195      return $fb;
6196  }
6197  
6198  /**
6199   * Returns file packer
6200   *
6201   * @param string $mimetype default application/zip
6202   * @return file_packer
6203   */
6204  function get_file_packer($mimetype='application/zip') {
6205      global $CFG;
6206  
6207      static $fp = array();
6208  
6209      if (isset($fp[$mimetype])) {
6210          return $fp[$mimetype];
6211      }
6212  
6213      switch ($mimetype) {
6214          case 'application/zip':
6215          case 'application/vnd.moodle.profiling':
6216              $classname = 'zip_packer';
6217              break;
6218  
6219          case 'application/x-gzip' :
6220              $classname = 'tgz_packer';
6221              break;
6222  
6223          case 'application/vnd.moodle.backup':
6224              $classname = 'mbz_packer';
6225              break;
6226  
6227          default:
6228              return false;
6229      }
6230  
6231      require_once("$CFG->libdir/filestorage/$classname.php");
6232      $fp[$mimetype] = new $classname();
6233  
6234      return $fp[$mimetype];
6235  }
6236  
6237  /**
6238   * Returns current name of file on disk if it exists.
6239   *
6240   * @param string $newfile File to be verified
6241   * @return string Current name of file on disk if true
6242   */
6243  function valid_uploaded_file($newfile) {
6244      if (empty($newfile)) {
6245          return '';
6246      }
6247      if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) {
6248          return $newfile['tmp_name'];
6249      } else {
6250          return '';
6251      }
6252  }
6253  
6254  /**
6255   * Returns the maximum size for uploading files.
6256   *
6257   * There are seven possible upload limits:
6258   * 1. in Apache using LimitRequestBody (no way of checking or changing this)
6259   * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
6260   * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
6261   * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
6262   * 5. by the Moodle admin in $CFG->maxbytes
6263   * 6. by the teacher in the current course $course->maxbytes
6264   * 7. by the teacher for the current module, eg $assignment->maxbytes
6265   *
6266   * These last two are passed to this function as arguments (in bytes).
6267   * Anything defined as 0 is ignored.
6268   * The smallest of all the non-zero numbers is returned.
6269   *
6270   * @todo Finish documenting this function
6271   *
6272   * @param int $sitebytes Set maximum size
6273   * @param int $coursebytes Current course $course->maxbytes (in bytes)
6274   * @param int $modulebytes Current module ->maxbytes (in bytes)
6275   * @param bool $unused This parameter has been deprecated and is not used any more.
6276   * @return int The maximum size for uploading files.
6277   */
6278  function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0, $unused = false) {
6279  
6280      if (! $filesize = ini_get('upload_max_filesize')) {
6281          $filesize = '5M';
6282      }
6283      $minimumsize = get_real_size($filesize);
6284  
6285      if ($postsize = ini_get('post_max_size')) {
6286          $postsize = get_real_size($postsize);
6287          if ($postsize < $minimumsize) {
6288              $minimumsize = $postsize;
6289          }
6290      }
6291  
6292      if (($sitebytes > 0) and ($sitebytes < $minimumsize)) {
6293          $minimumsize = $sitebytes;
6294      }
6295  
6296      if (($coursebytes > 0) and ($coursebytes < $minimumsize)) {
6297          $minimumsize = $coursebytes;
6298      }
6299  
6300      if (($modulebytes > 0) and ($modulebytes < $minimumsize)) {
6301          $minimumsize = $modulebytes;
6302      }
6303  
6304      return $minimumsize;
6305  }
6306  
6307  /**
6308   * Returns the maximum size for uploading files for the current user
6309   *
6310   * This function takes in account {@link get_max_upload_file_size()} the user's capabilities
6311   *
6312   * @param context $context The context in which to check user capabilities
6313   * @param int $sitebytes Set maximum size
6314   * @param int $coursebytes Current course $course->maxbytes (in bytes)
6315   * @param int $modulebytes Current module ->maxbytes (in bytes)
6316   * @param stdClass $user The user
6317   * @param bool $unused This parameter has been deprecated and is not used any more.
6318   * @return int The maximum size for uploading files.
6319   */
6320  function get_user_max_upload_file_size($context, $sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $user = null,
6321          $unused = false) {
6322      global $USER;
6323  
6324      if (empty($user)) {
6325          $user = $USER;
6326      }
6327  
6328      if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) {
6329          return USER_CAN_IGNORE_FILE_SIZE_LIMITS;
6330      }
6331  
6332      return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes);
6333  }
6334  
6335  /**
6336   * Returns an array of possible sizes in local language
6337   *
6338   * Related to {@link get_max_upload_file_size()} - this function returns an
6339   * array of possible sizes in an array, translated to the
6340   * local language.
6341   *
6342   * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes.
6343   *
6344   * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)"
6345   * with the value set to 0. This option will be the first in the list.
6346   *
6347   * @uses SORT_NUMERIC
6348   * @param int $sitebytes Set maximum size
6349   * @param int $coursebytes Current course $course->maxbytes (in bytes)
6350   * @param int $modulebytes Current module ->maxbytes (in bytes)
6351   * @param int|array $custombytes custom upload size/s which will be added to list,
6352   *        Only value/s smaller then maxsize will be added to list.
6353   * @return array
6354   */
6355  function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null) {
6356      global $CFG;
6357  
6358      if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) {
6359          return array();
6360      }
6361  
6362      if ($sitebytes == 0) {
6363          // Will get the minimum of upload_max_filesize or post_max_size.
6364          $sitebytes = get_max_upload_file_size();
6365      }
6366  
6367      $filesize = array();
6368      $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152,
6369                        5242880, 10485760, 20971520, 52428800, 104857600);
6370  
6371      // If custombytes is given and is valid then add it to the list.
6372      if (is_number($custombytes) and $custombytes > 0) {
6373          $custombytes = (int)$custombytes;
6374          if (!in_array($custombytes, $sizelist)) {
6375              $sizelist[] = $custombytes;
6376          }
6377      } else if (is_array($custombytes)) {
6378          $sizelist = array_unique(array_merge($sizelist, $custombytes));
6379      }
6380  
6381      // Allow maxbytes to be selected if it falls outside the above boundaries.
6382      if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) {
6383          // Note: get_real_size() is used in order to prevent problems with invalid values.
6384          $sizelist[] = get_real_size($CFG->maxbytes);
6385      }
6386  
6387      foreach ($sizelist as $sizebytes) {
6388          if ($sizebytes < $maxsize && $sizebytes > 0) {
6389              $filesize[(string)intval($sizebytes)] = display_size($sizebytes);
6390          }
6391      }
6392  
6393      $limitlevel = '';
6394      $displaysize = '';
6395      if ($modulebytes &&
6396          (($modulebytes < $coursebytes || $coursebytes == 0) &&
6397           ($modulebytes < $sitebytes || $sitebytes == 0))) {
6398          $limitlevel = get_string('activity', 'core');
6399          $displaysize = display_size($modulebytes);
6400          $filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list.
6401  
6402      } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) {
6403          $limitlevel = get_string('course', 'core');
6404          $displaysize = display_size($coursebytes);
6405          $filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list.
6406  
6407      } else if ($sitebytes) {
6408          $limitlevel = get_string('site', 'core');
6409          $displaysize = display_size($sitebytes);
6410          $filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list.
6411      }
6412  
6413      krsort($filesize, SORT_NUMERIC);
6414      if ($limitlevel) {
6415          $params = (object) array('contextname' => $limitlevel, 'displaysize' => $displaysize);
6416          $filesize  = array('0' => get_string('uploadlimitwithsize', 'core', $params)) + $filesize;
6417      }
6418  
6419      return $filesize;
6420  }
6421  
6422  /**
6423   * Returns an array with all the filenames in all subdirectories, relative to the given rootdir.
6424   *
6425   * If excludefiles is defined, then that file/directory is ignored
6426   * If getdirs is true, then (sub)directories are included in the output
6427   * If getfiles is true, then files are included in the output
6428   * (at least one of these must be true!)
6429   *
6430   * @todo Finish documenting this function. Add examples of $excludefile usage.
6431   *
6432   * @param string $rootdir A given root directory to start from
6433   * @param string|array $excludefiles If defined then the specified file/directory is ignored
6434   * @param bool $descend If true then subdirectories are recursed as well
6435   * @param bool $getdirs If true then (sub)directories are included in the output
6436   * @param bool $getfiles  If true then files are included in the output
6437   * @return array An array with all the filenames in all subdirectories, relative to the given rootdir
6438   */
6439  function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) {
6440  
6441      $dirs = array();
6442  
6443      if (!$getdirs and !$getfiles) {   // Nothing to show.
6444          return $dirs;
6445      }
6446  
6447      if (!is_dir($rootdir)) {          // Must be a directory.
6448          return $dirs;
6449      }
6450  
6451      if (!$dir = opendir($rootdir)) {  // Can't open it for some reason.
6452          return $dirs;
6453      }
6454  
6455      if (!is_array($excludefiles)) {
6456          $excludefiles = array($excludefiles);
6457      }
6458  
6459      while (false !== ($file = readdir($dir))) {
6460          $firstchar = substr($file, 0, 1);
6461          if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) {
6462              continue;
6463          }
6464          $fullfile = $rootdir .'/'. $file;
6465          if (filetype($fullfile) == 'dir') {
6466              if ($getdirs) {
6467                  $dirs[] = $file;
6468              }
6469              if ($descend) {
6470                  $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles);
6471                  foreach ($subdirs as $subdir) {
6472                      $dirs[] = $file .'/'. $subdir;
6473                  }
6474              }
6475          } else if ($getfiles) {
6476              $dirs[] = $file;
6477          }
6478      }
6479      closedir($dir);
6480  
6481      asort($dirs);
6482  
6483      return $dirs;
6484  }
6485  
6486  
6487  /**
6488   * Adds up all the files in a directory and works out the size.
6489   *
6490   * @param string $rootdir  The directory to start from
6491   * @param string $excludefile A file to exclude when summing directory size
6492   * @return int The summed size of all files and subfiles within the root directory
6493   */
6494  function get_directory_size($rootdir, $excludefile='') {
6495      global $CFG;
6496  
6497      // Do it this way if we can, it's much faster.
6498      if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) {
6499          $command = trim($CFG->pathtodu).' -sk '.escapeshellarg($rootdir);
6500          $output = null;
6501          $return = null;
6502          exec($command, $output, $return);
6503          if (is_array($output)) {
6504              // We told it to return k.
6505              return get_real_size(intval($output[0]).'k');
6506          }
6507      }
6508  
6509      if (!is_dir($rootdir)) {
6510          // Must be a directory.
6511          return 0;
6512      }
6513  
6514      if (!$dir = @opendir($rootdir)) {
6515          // Can't open it for some reason.
6516          return 0;
6517      }
6518  
6519      $size = 0;
6520  
6521      while (false !== ($file = readdir($dir))) {
6522          $firstchar = substr($file, 0, 1);
6523          if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) {
6524              continue;
6525          }
6526          $fullfile = $rootdir .'/'. $file;
6527          if (filetype($fullfile) == 'dir') {
6528              $size += get_directory_size($fullfile, $excludefile);
6529          } else {
6530              $size += filesize($fullfile);
6531          }
6532      }
6533      closedir($dir);
6534  
6535      return $size;
6536  }
6537  
6538  /**
6539   * Converts bytes into display form
6540   *
6541   * @static string $gb Localized string for size in gigabytes
6542   * @static string $mb Localized string for size in megabytes
6543   * @static string $kb Localized string for size in kilobytes
6544   * @static string $b Localized string for size in bytes
6545   * @param int $size  The size to convert to human readable form
6546   * @return string
6547   */
6548  function display_size($size) {
6549  
6550      static $gb, $mb, $kb, $b;
6551  
6552      if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) {
6553          return get_string('unlimited');
6554      }
6555  
6556      if (empty($gb)) {
6557          $gb = get_string('sizegb');
6558          $mb = get_string('sizemb');
6559          $kb = get_string('sizekb');
6560          $b  = get_string('sizeb');
6561      }
6562  
6563      if ($size >= 1073741824) {
6564          $size = round($size / 1073741824 * 10) / 10 . $gb;
6565      } else if ($size >= 1048576) {
6566          $size = round($size / 1048576 * 10) / 10 . $mb;
6567      } else if ($size >= 1024) {
6568          $size = round($size / 1024 * 10) / 10 . $kb;
6569      } else {
6570          $size = intval($size) .' '. $b; // File sizes over 2GB can not work in 32bit PHP anyway.
6571      }
6572      return $size;
6573  }
6574  
6575  /**
6576   * Cleans a given filename by removing suspicious or troublesome characters
6577   *
6578   * @see clean_param()
6579   * @param string $string file name
6580   * @return string cleaned file name
6581   */
6582  function clean_filename($string) {
6583      return clean_param($string, PARAM_FILE);
6584  }
6585  
6586  
6587  // STRING TRANSLATION.
6588  
6589  /**
6590   * Returns the code for the current language
6591   *
6592   * @category string
6593   * @return string
6594   */
6595  function current_language() {
6596      global $CFG, $USER, $SESSION, $COURSE;
6597  
6598      if (!empty($SESSION->forcelang)) {
6599          // Allows overriding course-forced language (useful for admins to check
6600          // issues in courses whose language they don't understand).
6601          // Also used by some code to temporarily get language-related information in a
6602          // specific language (see force_current_language()).
6603          $return = $SESSION->forcelang;
6604  
6605      } else if (!empty($COURSE->id) and $COURSE->id != SITEID and !empty($COURSE->lang)) {
6606          // Course language can override all other settings for this page.
6607          $return = $COURSE->lang;
6608  
6609      } else if (!empty($SESSION->lang)) {
6610          // Session language can override other settings.
6611          $return = $SESSION->lang;
6612  
6613      } else if (!empty($USER->lang)) {
6614          $return = $USER->lang;
6615  
6616      } else if (isset($CFG->lang)) {
6617          $return = $CFG->lang;
6618  
6619      } else {
6620          $return = 'en';
6621      }
6622  
6623      // Just in case this slipped in from somewhere by accident.
6624      $return = str_replace('_utf8', '', $return);
6625  
6626      return $return;
6627  }
6628  
6629  /**
6630   * Returns parent language of current active language if defined
6631   *
6632   * @category string
6633   * @param string $lang null means current language
6634   * @return string
6635   */
6636  function get_parent_language($lang=null) {
6637  
6638      // Let's hack around the current language.
6639      if (!empty($lang)) {
6640          $oldforcelang = force_current_language($lang);
6641      }
6642  
6643      $parentlang = get_string('parentlanguage', 'langconfig');
6644      if ($parentlang === 'en') {
6645          $parentlang = '';
6646      }
6647  
6648      // Let's hack around the current language.
6649      if (!empty($lang)) {
6650          force_current_language($oldforcelang);
6651      }
6652  
6653      return $parentlang;
6654  }
6655  
6656  /**
6657   * Force the current language to get strings and dates localised in the given language.
6658   *
6659   * After calling this function, all strings will be provided in the given language
6660   * until this function is called again, or equivalent code is run.
6661   *
6662   * @param string $language
6663   * @return string previous $SESSION->forcelang value
6664   */
6665  function force_current_language($language) {
6666      global $SESSION;
6667      $sessionforcelang = isset($SESSION->forcelang) ? $SESSION->forcelang : '';
6668      if ($language !== $sessionforcelang) {
6669          // Seting forcelang to null or an empty string disables it's effect.
6670          if (empty($language) || get_string_manager()->translation_exists($language, false)) {
6671              $SESSION->forcelang = $language;
6672              moodle_setlocale();
6673          }
6674      }
6675      return $sessionforcelang;
6676  }
6677  
6678  /**
6679   * Returns current string_manager instance.
6680   *
6681   * The param $forcereload is needed for CLI installer only where the string_manager instance
6682   * must be replaced during the install.php script life time.
6683   *
6684   * @category string
6685   * @param bool $forcereload shall the singleton be released and new instance created instead?
6686   * @return core_string_manager
6687   */
6688  function get_string_manager($forcereload=false) {
6689      global $CFG;
6690  
6691      static $singleton = null;
6692  
6693      if ($forcereload) {
6694          $singleton = null;
6695      }
6696      if ($singleton === null) {
6697          if (empty($CFG->early_install_lang)) {
6698  
6699              if (empty($CFG->langlist)) {
6700                   $translist = array();
6701              } else {
6702                  $translist = explode(',', $CFG->langlist);
6703              }
6704  
6705              if (!empty($CFG->config_php_settings['customstringmanager'])) {
6706                  $classname = $CFG->config_php_settings['customstringmanager'];
6707  
6708                  if (class_exists($classname)) {
6709                      $implements = class_implements($classname);
6710  
6711                      if (isset($implements['core_string_manager'])) {
6712                          $singleton = new $classname($CFG->langotherroot, $CFG->langlocalroot, $translist);
6713                          return $singleton;
6714  
6715                      } else {
6716                          debugging('Unable to instantiate custom string manager: class '.$classname.
6717                              ' does not implement the core_string_manager interface.');
6718                      }
6719  
6720                  } else {
6721                      debugging('Unable to instantiate custom string manager: class '.$classname.' can not be found.');
6722                  }
6723              }
6724  
6725              $singleton = new core_string_manager_standard($CFG->langotherroot, $CFG->langlocalroot, $translist);
6726  
6727          } else {
6728              $singleton = new core_string_manager_install();
6729          }
6730      }
6731  
6732      return $singleton;
6733  }
6734  
6735  /**
6736   * Returns a localized string.
6737   *
6738   * Returns the translated string specified by $identifier as
6739   * for $module.  Uses the same format files as STphp.
6740   * $a is an object, string or number that can be used
6741   * within translation strings
6742   *
6743   * eg 'hello {$a->firstname} {$a->lastname}'
6744   * or 'hello {$a}'
6745   *
6746   * If you would like to directly echo the localized string use
6747   * the function {@link print_string()}
6748   *
6749   * Example usage of this function involves finding the string you would
6750   * like a local equivalent of and using its identifier and module information
6751   * to retrieve it.<br/>
6752   * If you open moodle/lang/en/moodle.php and look near line 278
6753   * you will find a string to prompt a user for their word for 'course'
6754   * <code>
6755   * $string['course'] = 'Course';
6756   * </code>
6757   * So if you want to display the string 'Course'
6758   * in any language that supports it on your site
6759   * you just need to use the identifier 'course'
6760   * <code>
6761   * $mystring = '<strong>'. get_string('course') .'</strong>';
6762   * or
6763   * </code>
6764   * If the string you want is in another file you'd take a slightly
6765   * different approach. Looking in moodle/lang/en/calendar.php you find
6766   * around line 75:
6767   * <code>
6768   * $string['typecourse'] = 'Course event';
6769   * </code>
6770   * If you want to display the string "Course event" in any language
6771   * supported you would use the identifier 'typecourse' and the module 'calendar'
6772   * (because it is in the file calendar.php):
6773   * <code>
6774   * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>';
6775   * </code>
6776   *
6777   * As a last resort, should the identifier fail to map to a string
6778   * the returned string will be [[ $identifier ]]
6779   *
6780   * In Moodle 2.3 there is a new argument to this function $lazyload.
6781   * Setting $lazyload to true causes get_string to return a lang_string object
6782   * rather than the string itself. The fetching of the string is then put off until
6783   * the string object is first used. The object can be used by calling it's out
6784   * method or by casting the object to a string, either directly e.g.
6785   *     (string)$stringobject
6786   * or indirectly by using the string within another string or echoing it out e.g.
6787   *     echo $stringobject
6788   *     return "<p>{$stringobject}</p>";
6789   * It is worth noting that using $lazyload and attempting to use the string as an
6790   * array key will cause a fatal error as objects cannot be used as array keys.
6791   * But you should never do that anyway!
6792   * For more information {@link lang_string}
6793   *
6794   * @category string
6795   * @param string $identifier The key identifier for the localized string
6796   * @param string $component The module where the key identifier is stored,
6797   *      usually expressed as the filename in the language pack without the
6798   *      .php on the end but can also be written as mod/forum or grade/export/xls.
6799   *      If none is specified then moodle.php is used.
6800   * @param string|object|array $a An object, string or number that can be used
6801   *      within translation strings
6802   * @param bool $lazyload If set to true a string object is returned instead of
6803   *      the string itself. The string then isn't calculated until it is first used.
6804   * @return string The localized string.
6805   * @throws coding_exception
6806   */
6807  function get_string($identifier, $component = '', $a = null, $lazyload = false) {
6808      global $CFG;
6809  
6810      // If the lazy load argument has been supplied return a lang_string object
6811      // instead.
6812      // We need to make sure it is true (and a bool) as you will see below there
6813      // used to be a forth argument at one point.
6814      if ($lazyload === true) {
6815          return new lang_string($identifier, $component, $a);
6816      }
6817  
6818      if ($CFG->debugdeveloper && clean_param($identifier, PARAM_STRINGID) === '') {
6819          throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.', DEBUG_DEVELOPER);
6820      }
6821  
6822      // There is now a forth argument again, this time it is a boolean however so
6823      // we can still check for the old extralocations parameter.
6824      if (!is_bool($lazyload) && !empty($lazyload)) {
6825          debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.');
6826      }
6827  
6828      if (strpos($component, '/') !== false) {
6829          debugging('The module name you passed to get_string is the deprecated format ' .
6830                  'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER);
6831          $componentpath = explode('/', $component);
6832  
6833          switch ($componentpath[0]) {
6834              case 'mod':
6835                  $component = $componentpath[1];
6836                  break;
6837              case 'blocks':
6838              case 'block':
6839                  $component = 'block_'.$componentpath[1];
6840                  break;
6841              case 'enrol':
6842                  $component = 'enrol_'.$componentpath[1];
6843                  break;
6844              case 'format':
6845                  $component = 'format_'.$componentpath[1];
6846                  break;
6847              case 'grade':
6848                  $component = 'grade'.$componentpath[1].'_'.$componentpath[2];
6849                  break;
6850          }
6851      }
6852  
6853      $result = get_string_manager()->get_string($identifier, $component, $a);
6854  
6855      // Debugging feature lets you display string identifier and component.
6856      if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
6857          $result .= ' {' . $identifier . '/' . $component . '}';
6858      }
6859      return $result;
6860  }
6861  
6862  /**
6863   * Converts an array of strings to their localized value.
6864   *
6865   * @param array $array An array of strings
6866   * @param string $component The language module that these strings can be found in.
6867   * @return stdClass translated strings.
6868   */
6869  function get_strings($array, $component = '') {
6870      $string = new stdClass;
6871      foreach ($array as $item) {
6872          $string->$item = get_string($item, $component);
6873      }
6874      return $string;
6875  }
6876  
6877  /**
6878   * Prints out a translated string.
6879   *
6880   * Prints out a translated string using the return value from the {@link get_string()} function.
6881   *
6882   * Example usage of this function when the string is in the moodle.php file:<br/>
6883   * <code>
6884   * echo '<strong>';
6885   * print_string('course');
6886   * echo '</strong>';
6887   * </code>
6888   *
6889   * Example usage of this function when the string is not in the moodle.php file:<br/>
6890   * <code>
6891   * echo '<h1>';
6892   * print_string('typecourse', 'calendar');
6893   * echo '</h1>';
6894   * </code>
6895   *
6896   * @category string
6897   * @param string $identifier The key identifier for the localized string
6898   * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used.
6899   * @param string|object|array $a An object, string or number that can be used within translation strings
6900   */
6901  function print_string($identifier, $component = '', $a = null) {
6902      echo get_string($identifier, $component, $a);
6903  }
6904  
6905  /**
6906   * Returns a list of charset codes
6907   *
6908   * Returns a list of charset codes. It's hardcoded, so they should be added manually
6909   * (checking that such charset is supported by the texlib library!)
6910   *
6911   * @return array And associative array with contents in the form of charset => charset
6912   */
6913  function get_list_of_charsets() {
6914  
6915      $charsets = array(
6916          'EUC-JP'     => 'EUC-JP',
6917          'ISO-2022-JP'=> 'ISO-2022-JP',
6918          'ISO-8859-1' => 'ISO-8859-1',
6919          'SHIFT-JIS'  => 'SHIFT-JIS',
6920          'GB2312'     => 'GB2312',
6921          'GB18030'    => 'GB18030', // GB18030 not supported by typo and mbstring.
6922          'UTF-8'      => 'UTF-8');
6923  
6924      asort($charsets);
6925  
6926      return $charsets;
6927  }
6928  
6929  /**
6930   * Returns a list of valid and compatible themes
6931   *
6932   * @return array
6933   */
6934  function get_list_of_themes() {
6935      global $CFG;
6936  
6937      $themes = array();
6938  
6939      if (!empty($CFG->themelist)) {       // Use admin's list of themes.
6940          $themelist = explode(',', $CFG->themelist);
6941      } else {
6942          $themelist = array_keys(core_component::get_plugin_list("theme"));
6943      }
6944  
6945      foreach ($themelist as $key => $themename) {
6946          $theme = theme_config::load($themename);
6947          $themes[$themename] = $theme;
6948      }
6949  
6950      core_collator::asort_objects_by_method($themes, 'get_theme_name');
6951  
6952      return $themes;
6953  }
6954  
6955  /**
6956   * Factory function for emoticon_manager
6957   *
6958   * @return emoticon_manager singleton
6959   */
6960  function get_emoticon_manager() {
6961      static $singleton = null;
6962  
6963      if (is_null($singleton)) {
6964          $singleton = new emoticon_manager();
6965      }
6966  
6967      return $singleton;
6968  }
6969  
6970  /**
6971   * Provides core support for plugins that have to deal with emoticons (like HTML editor or emoticon filter).
6972   *
6973   * Whenever this manager mentiones 'emoticon object', the following data
6974   * structure is expected: stdClass with properties text, imagename, imagecomponent,
6975   * altidentifier and altcomponent
6976   *
6977   * @see admin_setting_emoticons
6978   *
6979   * @copyright 2010 David Mudrak
6980   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6981   */
6982  class emoticon_manager {
6983  
6984      /**
6985       * Returns the currently enabled emoticons
6986       *
6987       * @return array of emoticon objects
6988       */
6989      public function get_emoticons() {
6990          global $CFG;
6991  
6992          if (empty($CFG->emoticons)) {
6993              return array();
6994          }
6995  
6996          $emoticons = $this->decode_stored_config($CFG->emoticons);
6997  
6998          if (!is_array($emoticons)) {
6999              // Something is wrong with the format of stored setting.
7000              debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL);
7001              return array();
7002          }
7003  
7004          return $emoticons;
7005      }
7006  
7007      /**
7008       * Converts emoticon object into renderable pix_emoticon object
7009       *
7010       * @param stdClass $emoticon emoticon object
7011       * @param array $attributes explicit HTML attributes to set
7012       * @return pix_emoticon
7013       */
7014      public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array()) {
7015          $stringmanager = get_string_manager();
7016          if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) {
7017              $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent);
7018          } else {
7019              $alt = s($emoticon->text);
7020          }
7021          return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes);
7022      }
7023  
7024      /**
7025       * Encodes the array of emoticon objects into a string storable in config table
7026       *
7027       * @see self::decode_stored_config()
7028       * @param array $emoticons array of emtocion objects
7029       * @return string
7030       */
7031      public function encode_stored_config(array $emoticons) {
7032          return json_encode($emoticons);
7033      }
7034  
7035      /**
7036       * Decodes the string into an array of emoticon objects
7037       *
7038       * @see self::encode_stored_config()
7039       * @param string $encoded
7040       * @return string|null
7041       */
7042      public function decode_stored_config($encoded) {
7043          $decoded = json_decode($encoded);
7044          if (!is_array($decoded)) {
7045              return null;
7046          }
7047          return $decoded;
7048      }
7049  
7050      /**
7051       * Returns default set of emoticons supported by Moodle
7052       *
7053       * @return array of sdtClasses
7054       */
7055      public function default_emoticons() {
7056          return array(
7057              $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'),
7058              $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'),
7059              $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'),
7060              $this->prepare_emoticon_object(";-)", 's/wink', 'wink'),
7061              $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'),
7062              $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'),
7063              $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'),
7064              $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'),
7065              $this->prepare_emoticon_object("B-)", 's/cool', 'cool'),
7066              $this->prepare_emoticon_object("^-)", 's/approve', 'approve'),
7067              $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'),
7068              $this->prepare_emoticon_object(":o)", 's/clown', 'clown'),
7069              $this->prepare_emoticon_object(":-(", 's/sad', 'sad'),
7070              $this->prepare_emoticon_object(":(", 's/sad', 'sad'),
7071              $this->prepare_emoticon_object("8-.", 's/shy', 'shy'),
7072              $this->prepare_emoticon_object(":-I", 's/blush', 'blush'),
7073              $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'),
7074              $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'),
7075              $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'),
7076              $this->prepare_emoticon_object("8-[", 's/angry', 'angry'),
7077              $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'),
7078              $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'),
7079              $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'),
7080              $this->prepare_emoticon_object("}-]", 's/evil', 'evil'),
7081              $this->prepare_emoticon_object("(h)", 's/heart', 'heart'),
7082              $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'),
7083              $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'),
7084              $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'),
7085              $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'),
7086              $this->prepare_emoticon_object("( )", 's/egg', 'egg'),
7087          );
7088      }
7089  
7090      /**
7091       * Helper method preparing the stdClass with the emoticon properties
7092       *
7093       * @param string|array $text or array of strings
7094       * @param string $imagename to be used by {@link pix_emoticon}
7095       * @param string $altidentifier alternative string identifier, null for no alt
7096       * @param string $altcomponent where the alternative string is defined
7097       * @param string $imagecomponent to be used by {@link pix_emoticon}
7098       * @return stdClass
7099       */
7100      protected function prepare_emoticon_object($text, $imagename, $altidentifier = null,
7101                                                 $altcomponent = 'core_pix', $imagecomponent = 'core') {
7102          return (object)array(
7103              'text'           => $text,
7104              'imagename'      => $imagename,
7105              'imagecomponent' => $imagecomponent,
7106              'altidentifier'  => $altidentifier,
7107              'altcomponent'   => $altcomponent,
7108          );
7109      }
7110  }
7111  
7112  // ENCRYPTION.
7113  
7114  /**
7115   * rc4encrypt
7116   *
7117   * @param string $data        Data to encrypt.
7118   * @return string             The now encrypted data.
7119   */
7120  function rc4encrypt($data) {
7121      return endecrypt(get_site_identifier(), $data, '');
7122  }
7123  
7124  /**
7125   * rc4decrypt
7126   *
7127   * @param string $data        Data to decrypt.
7128   * @return string             The now decrypted data.
7129   */
7130  function rc4decrypt($data) {
7131      return endecrypt(get_site_identifier(), $data, 'de');
7132  }
7133  
7134  /**
7135   * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com]
7136   *
7137   * @todo Finish documenting this function
7138   *
7139   * @param string $pwd The password to use when encrypting or decrypting
7140   * @param string $data The data to be decrypted/encrypted
7141   * @param string $case Either 'de' for decrypt or '' for encrypt
7142   * @return string
7143   */
7144  function endecrypt ($pwd, $data, $case) {
7145  
7146      if ($case == 'de') {
7147          $data = urldecode($data);
7148      }
7149  
7150      $key[] = '';
7151      $box[] = '';
7152      $pwdlength = strlen($pwd);
7153  
7154      for ($i = 0; $i <= 255; $i++) {
7155          $key[$i] = ord(substr($pwd, ($i % $pwdlength), 1));
7156          $box[$i] = $i;
7157      }
7158  
7159      $x = 0;
7160  
7161      for ($i = 0; $i <= 255; $i++) {
7162          $x = ($x + $box[$i] + $key[$i]) % 256;
7163          $tempswap = $box[$i];
7164          $box[$i] = $box[$x];
7165          $box[$x] = $tempswap;
7166      }
7167  
7168      $cipher = '';
7169  
7170      $a = 0;
7171      $j = 0;
7172  
7173      for ($i = 0; $i < strlen($data); $i++) {
7174          $a = ($a + 1) % 256;
7175          $j = ($j + $box[$a]) % 256;
7176          $temp = $box[$a];
7177          $box[$a] = $box[$j];
7178          $box[$j] = $temp;
7179          $k = $box[(($box[$a] + $box[$j]) % 256)];
7180          $cipherby = ord(substr($data, $i, 1)) ^ $k;
7181          $cipher .= chr($cipherby);
7182      }
7183  
7184      if ($case == 'de') {
7185          $cipher = urldecode(urlencode($cipher));
7186      } else {
7187          $cipher = urlencode($cipher);
7188      }
7189  
7190      return $cipher;
7191  }
7192  
7193  // ENVIRONMENT CHECKING.
7194  
7195  /**
7196   * This method validates a plug name. It is much faster than calling clean_param.
7197   *
7198   * @param string $name a string that might be a plugin name.
7199   * @return bool if this string is a valid plugin name.
7200   */
7201  function is_valid_plugin_name($name) {
7202      // This does not work for 'mod', bad luck, use any other type.
7203      return core_component::is_valid_plugin_name('tool', $name);
7204  }
7205  
7206  /**
7207   * Get a list of all the plugins of a given type that define a certain API function
7208   * in a certain file. The plugin component names and function names are returned.
7209   *
7210   * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
7211   * @param string $function the part of the name of the function after the
7212   *      frankenstyle prefix. e.g 'hook' if you are looking for functions with
7213   *      names like report_courselist_hook.
7214   * @param string $file the name of file within the plugin that defines the
7215   *      function. Defaults to lib.php.
7216   * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
7217   *      and the function names as values (e.g. 'report_courselist_hook', 'forum_hook').
7218   */
7219  function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php') {
7220      global $CFG;
7221  
7222      // We don't include here as all plugin types files would be included.
7223      $plugins = get_plugins_with_function($function, $file, false);
7224  
7225      if (empty($plugins[$plugintype])) {
7226          return array();
7227      }
7228  
7229      $allplugins = core_component::get_plugin_list($plugintype);
7230  
7231      // Reformat the array and include the files.
7232      $pluginfunctions = array();
7233      foreach ($plugins[$plugintype] as $pluginname => $functionname) {
7234  
7235          // Check that it has not been removed and the file is still available.
7236          if (!empty($allplugins[$pluginname])) {
7237  
7238              $filepath = $allplugins[$pluginname] . DIRECTORY_SEPARATOR . $file;
7239              if (file_exists($filepath)) {
7240                  include_once($filepath);
7241                  $pluginfunctions[$plugintype . '_' . $pluginname] = $functionname;
7242              }
7243          }
7244      }
7245  
7246      return $pluginfunctions;
7247  }
7248  
7249  /**
7250   * Get a list of all the plugins that define a certain API function in a certain file.
7251   *
7252   * @param string $function the part of the name of the function after the
7253   *      frankenstyle prefix. e.g 'hook' if you are looking for functions with
7254   *      names like report_courselist_hook.
7255   * @param string $file the name of file within the plugin that defines the
7256   *      function. Defaults to lib.php.
7257   * @param bool $include Whether to include the files that contain the functions or not.
7258   * @return array with [plugintype][plugin] = functionname
7259   */
7260  function get_plugins_with_function($function, $file = 'lib.php', $include = true) {
7261      global $CFG;
7262  
7263      $cache = \cache::make('core', 'plugin_functions');
7264  
7265      // Including both although I doubt that we will find two functions definitions with the same name.
7266      // Clearning the filename as cache_helper::hash_key only allows a-zA-Z0-9_.
7267      $key = $function . '_' . clean_param($file, PARAM_ALPHA);
7268  
7269      if ($pluginfunctions = $cache->get($key)) {
7270  
7271          // Checking that the files are still available.
7272          foreach ($pluginfunctions as $plugintype => $plugins) {
7273  
7274              $allplugins = \core_component::get_plugin_list($plugintype);
7275              foreach ($plugins as $plugin => $fullpath) {
7276  
7277                  // Cache might be out of sync with the codebase, skip the plugin if it is not available.
7278                  if (empty($allplugins[$plugin])) {
7279                      unset($pluginfunctions[$plugintype][$plugin]);
7280                      continue;
7281                  }
7282  
7283                  $fileexists = file_exists($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7284                  if ($include && $fileexists) {
7285                      // Include the files if it was requested.
7286                      include_once($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7287                  } else if (!$fileexists) {
7288                      // If the file is not available any more it should not be returned.
7289                      unset($pluginfunctions[$plugintype][$plugin]);
7290                  }
7291              }
7292          }
7293          return $pluginfunctions;
7294      }
7295  
7296      $pluginfunctions = array();
7297  
7298      // To fill the cached. Also, everything should continue working with cache disabled.
7299      $plugintypes = \core_component::get_plugin_types();
7300      foreach ($plugintypes as $plugintype => $unused) {
7301  
7302          // We need to include files here.
7303          $pluginswithfile = \core_component::get_plugin_list_with_file($plugintype, $file, true);
7304          foreach ($pluginswithfile as $plugin => $notused) {
7305  
7306              $fullfunction = $plugintype . '_' . $plugin . '_' . $function;
7307  
7308              $pluginfunction = false;
7309              if (function_exists($fullfunction)) {
7310                  // Function exists with standard name. Store, indexed by frankenstyle name of plugin.
7311                  $pluginfunction = $fullfunction;
7312  
7313              } else if ($plugintype === 'mod') {
7314                  // For modules, we also allow plugin without full frankenstyle but just starting with the module name.
7315                  $shortfunction = $plugin . '_' . $function;
7316                  if (function_exists($shortfunction)) {
7317                      $pluginfunction = $shortfunction;
7318                  }
7319              }
7320  
7321              if ($pluginfunction) {
7322                  if (empty($pluginfunctions[$plugintype])) {
7323                      $pluginfunctions[$plugintype] = array();
7324                  }
7325                  $pluginfunctions[$plugintype][$plugin] = $pluginfunction;
7326              }
7327  
7328          }
7329      }
7330      $cache->set($key, $pluginfunctions);
7331  
7332      return $pluginfunctions;
7333  
7334  }
7335  
7336  /**
7337   * Lists plugin-like directories within specified directory
7338   *
7339   * This function was originally used for standard Moodle plugins, please use
7340   * new core_component::get_plugin_list() now.
7341   *
7342   * This function is used for general directory listing and backwards compatility.
7343   *
7344   * @param string $directory relative directory from root
7345   * @param string $exclude dir name to exclude from the list (defaults to none)
7346   * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot)
7347   * @return array Sorted array of directory names found under the requested parameters
7348   */
7349  function get_list_of_plugins($directory='mod', $exclude='', $basedir='') {
7350      global $CFG;
7351  
7352      $plugins = array();
7353  
7354      if (empty($basedir)) {
7355          $basedir = $CFG->dirroot .'/'. $directory;
7356  
7357      } else {
7358          $basedir = $basedir .'/'. $directory;
7359      }
7360  
7361      if ($CFG->debugdeveloper and empty($exclude)) {
7362          // Make sure devs do not use this to list normal plugins,
7363          // this is intended for general directories that are not plugins!
7364  
7365          $subtypes = core_component::get_plugin_types();
7366          if (in_array($basedir, $subtypes)) {
7367              debugging('get_list_of_plugins() should not be used to list real plugins, use core_component::get_plugin_list() instead!', DEBUG_DEVELOPER);
7368          }
7369          unset($subtypes);
7370      }
7371  
7372      if (file_exists($basedir) && filetype($basedir) == 'dir') {
7373          if (!$dirhandle = opendir($basedir)) {
7374              debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER);
7375              return array();
7376          }
7377          while (false !== ($dir = readdir($dirhandle))) {
7378              // Func: strpos is marginally but reliably faster than substr($dir, 0, 1).
7379              if (strpos($dir, '.') === 0 or $dir === 'CVS' or $dir === '_vti_cnf' or $dir === 'simpletest' or $dir === 'yui' or
7380                  $dir === 'tests' or $dir === 'classes' or $dir === $exclude) {
7381                  continue;
7382              }
7383              if (filetype($basedir .'/'. $dir) != 'dir') {
7384                  continue;
7385              }
7386              $plugins[] = $dir;
7387          }
7388          closedir($dirhandle);
7389      }
7390      if ($plugins) {
7391          asort($plugins);
7392      }
7393      return $plugins;
7394  }
7395  
7396  /**
7397   * Invoke plugin's callback functions
7398   *
7399   * @param string $type plugin type e.g. 'mod'
7400   * @param string $name plugin name
7401   * @param string $feature feature name
7402   * @param string $action feature's action
7403   * @param array $params parameters of callback function, should be an array
7404   * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
7405   * @return mixed
7406   *
7407   * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743
7408   */
7409  function plugin_callback($type, $name, $feature, $action, $params = null, $default = null) {
7410      return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default);
7411  }
7412  
7413  /**
7414   * Invoke component's callback functions
7415   *
7416   * @param string $component frankenstyle component name, e.g. 'mod_quiz'
7417   * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
7418   * @param array $params parameters of callback function
7419   * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
7420   * @return mixed
7421   */
7422  function component_callback($component, $function, array $params = array(), $default = null) {
7423  
7424      $functionname = component_callback_exists($component, $function);
7425  
7426      if ($functionname) {
7427          // Function exists, so just return function result.
7428          $ret = call_user_func_array($functionname, $params);
7429          if (is_null($ret)) {
7430              return $default;
7431          } else {
7432              return $ret;
7433          }
7434      }
7435      return $default;
7436  }
7437  
7438  /**
7439   * Determine if a component callback exists and return the function name to call. Note that this
7440   * function will include the required library files so that the functioname returned can be
7441   * called directly.
7442   *
7443   * @param string $component frankenstyle component name, e.g. 'mod_quiz'
7444   * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
7445   * @return mixed Complete function name to call if the callback exists or false if it doesn't.
7446   * @throws coding_exception if invalid component specfied
7447   */
7448  function component_callback_exists($component, $function) {
7449      global $CFG; // This is needed for the inclusions.
7450  
7451      $cleancomponent = clean_param($component, PARAM_COMPONENT);
7452      if (empty($cleancomponent)) {
7453          throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
7454      }
7455      $component = $cleancomponent;
7456  
7457      list($type, $name) = core_component::normalize_component($component);
7458      $component = $type . '_' . $name;
7459  
7460      $oldfunction = $name.'_'.$function;
7461      $function = $component.'_'.$function;
7462  
7463      $dir = core_component::get_component_directory($component);
7464      if (empty($dir)) {
7465          throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
7466      }
7467  
7468      // Load library and look for function.
7469      if (file_exists($dir.'/lib.php')) {
7470          require_once($dir.'/lib.php');
7471      }
7472  
7473      if (!function_exists($function) and function_exists($oldfunction)) {
7474          if ($type !== 'mod' and $type !== 'core') {
7475              debugging("Please use new function name $function instead of legacy $oldfunction", DEBUG_DEVELOPER);
7476          }
7477          $function = $oldfunction;
7478      }
7479  
7480      if (function_exists($function)) {
7481          return $function;
7482      }
7483      return false;
7484  }
7485  
7486  /**
7487   * Checks whether a plugin supports a specified feature.
7488   *
7489   * @param string $type Plugin type e.g. 'mod'
7490   * @param string $name Plugin name e.g. 'forum'
7491   * @param string $feature Feature code (FEATURE_xx constant)
7492   * @param mixed $default default value if feature support unknown
7493   * @return mixed Feature result (false if not supported, null if feature is unknown,
7494   *         otherwise usually true but may have other feature-specific value such as array)
7495   * @throws coding_exception
7496   */
7497  function plugin_supports($type, $name, $feature, $default = null) {
7498      global $CFG;
7499  
7500      if ($type === 'mod' and $name === 'NEWMODULE') {
7501          // Somebody forgot to rename the module template.
7502          return false;
7503      }
7504  
7505      $component = clean_param($type . '_' . $name, PARAM_COMPONENT);
7506      if (empty($component)) {
7507          throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name);
7508      }
7509  
7510      $function = null;
7511  
7512      if ($type === 'mod') {
7513          // We need this special case because we support subplugins in modules,
7514          // otherwise it would end up in infinite loop.
7515          if (file_exists("$CFG->dirroot/mod/$name/lib.php")) {
7516              include_once("$CFG->dirroot/mod/$name/lib.php");
7517              $function = $component.'_supports';
7518              if (!function_exists($function)) {
7519                  // Legacy non-frankenstyle function name.
7520                  $function = $name.'_supports';
7521              }
7522          }
7523  
7524      } else {
7525          if (!$path = core_component::get_plugin_directory($type, $name)) {
7526              // Non existent plugin type.
7527              return false;
7528          }
7529          if (file_exists("$path/lib.php")) {
7530              include_once("$path/lib.php");
7531              $function = $component.'_supports';
7532          }
7533      }
7534  
7535      if ($function and function_exists($function)) {
7536          $supports = $function($feature);
7537          if (is_null($supports)) {
7538              // Plugin does not know - use default.
7539              return $default;
7540          } else {
7541              return $supports;
7542          }
7543      }
7544  
7545      // Plugin does not care, so use default.
7546      return $default;
7547  }
7548  
7549  /**
7550   * Returns true if the current version of PHP is greater that the specified one.
7551   *
7552   * @todo Check PHP version being required here is it too low?
7553   *
7554   * @param string $version The version of php being tested.
7555   * @return bool
7556   */
7557  function check_php_version($version='5.2.4') {
7558      return (version_compare(phpversion(), $version) >= 0);
7559  }
7560  
7561  /**
7562   * Determine if moodle installation requires update.
7563   *
7564   * Checks version numbers of main code and all plugins to see
7565   * if there are any mismatches.
7566   *
7567   * @return bool
7568   */
7569  function moodle_needs_upgrading() {
7570      global $CFG;
7571  
7572      if (empty($CFG->version)) {
7573          return true;
7574      }
7575  
7576      // There is no need to purge plugininfo caches here because
7577      // these caches are not used during upgrade and they are purged after
7578      // every upgrade.
7579  
7580      if (empty($CFG->allversionshash)) {
7581          return true;
7582      }
7583  
7584      $hash = core_component::get_all_versions_hash();
7585  
7586      return ($hash !== $CFG->allversionshash);
7587  }
7588  
7589  /**
7590   * Returns the major version of this site
7591   *
7592   * Moodle version numbers consist of three numbers separated by a dot, for
7593   * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so
7594   * called major version. This function extracts the major version from either
7595   * $CFG->release (default) or eventually from the $release variable defined in
7596   * the main version.php.
7597   *
7598   * @param bool $fromdisk should the version if source code files be used
7599   * @return string|false the major version like '2.3', false if could not be determined
7600   */
7601  function moodle_major_version($fromdisk = false) {
7602      global $CFG;
7603  
7604      if ($fromdisk) {
7605          $release = null;
7606          require($CFG->dirroot.'/version.php');
7607          if (empty($release)) {
7608              return false;
7609          }
7610  
7611      } else {
7612          if (empty($CFG->release)) {
7613              return false;
7614          }
7615          $release = $CFG->release;
7616      }
7617  
7618      if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) {
7619          return $matches[0];
7620      } else {
7621          return false;
7622      }
7623  }
7624  
7625  // MISCELLANEOUS.
7626  
7627  /**
7628   * Sets the system locale
7629   *
7630   * @category string
7631   * @param string $locale Can be used to force a locale
7632   */
7633  function moodle_setlocale($locale='') {
7634      global $CFG;
7635  
7636      static $currentlocale = ''; // Last locale caching.
7637  
7638      $oldlocale = $currentlocale;
7639  
7640      // Fetch the correct locale based on ostype.
7641      if ($CFG->ostype == 'WINDOWS') {
7642          $stringtofetch = 'localewin';
7643      } else {
7644          $stringtofetch = 'locale';
7645      }
7646  
7647      // The priority is the same as in get_string() - parameter, config, course, session, user, global language.
7648      if (!empty($locale)) {
7649          $currentlocale = $locale;
7650      } else if (!empty($CFG->locale)) { // Override locale for all language packs.
7651          $currentlocale = $CFG->locale;
7652      } else {
7653          $currentlocale = get_string($stringtofetch, 'langconfig');
7654      }
7655  
7656      // Do nothing if locale already set up.
7657      if ($oldlocale == $currentlocale) {
7658          return;
7659      }
7660  
7661      // Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values,
7662      // set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7
7663      // Some day, numeric, monetary and other categories should be set too, I think. :-/.
7664  
7665      // Get current values.
7666      $monetary= setlocale (LC_MONETARY, 0);
7667      $numeric = setlocale (LC_NUMERIC, 0);
7668      $ctype   = setlocale (LC_CTYPE, 0);
7669      if ($CFG->ostype != 'WINDOWS') {
7670          $messages= setlocale (LC_MESSAGES, 0);
7671      }
7672      // Set locale to all.
7673      $result = setlocale (LC_ALL, $currentlocale);
7674      // If setting of locale fails try the other utf8 or utf-8 variant,
7675      // some operating systems support both (Debian), others just one (OSX).
7676      if ($result === false) {
7677          if (stripos($currentlocale, '.UTF-8') !== false) {
7678              $newlocale = str_ireplace('.UTF-8', '.UTF8', $currentlocale);
7679              setlocale (LC_ALL, $newlocale);
7680          } else if (stripos($currentlocale, '.UTF8') !== false) {
7681              $newlocale = str_ireplace('.UTF8', '.UTF-8', $currentlocale);
7682              setlocale (LC_ALL, $newlocale);
7683          }
7684      }
7685      // Set old values.
7686      setlocale (LC_MONETARY, $monetary);
7687      setlocale (LC_NUMERIC, $numeric);
7688      if ($CFG->ostype != 'WINDOWS') {
7689          setlocale (LC_MESSAGES, $messages);
7690      }
7691      if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') {
7692          // To workaround a well-known PHP problem with Turkish letter Ii.
7693          setlocale (LC_CTYPE, $ctype);
7694      }
7695  }
7696  
7697  /**
7698   * Count words in a string.
7699   *
7700   * Words are defined as things between whitespace.
7701   *
7702   * @category string
7703   * @param string $string The text to be searched for words.
7704   * @return int The count of words in the specified string
7705   */
7706  function count_words($string) {
7707      $string = strip_tags($string);
7708      // Decode HTML entities.
7709      $string = html_entity_decode($string);
7710      // Replace underscores (which are classed as word characters) with spaces.
7711      $string = preg_replace('/_/u', ' ', $string);
7712      // Remove any characters that shouldn't be treated as word boundaries.
7713      $string = preg_replace('/[\'"’-]/u', '', $string);
7714      // Remove dots and commas from within numbers only.
7715      $string = preg_replace('/([0-9])[.,]([0-9])/u', '$1$2', $string);
7716  
7717      return count(preg_split('/\w\b/u', $string)) - 1;
7718  }
7719  
7720  /**
7721   * Count letters in a string.
7722   *
7723   * Letters are defined as chars not in tags and different from whitespace.
7724   *
7725   * @category string
7726   * @param string $string The text to be searched for letters.
7727   * @return int The count of letters in the specified text.
7728   */
7729  function count_letters($string) {
7730      $string = strip_tags($string); // Tags are out now.
7731      $string = preg_replace('/[[:space:]]*/', '', $string); // Whitespace are out now.
7732  
7733      return core_text::strlen($string);
7734  }
7735  
7736  /**
7737   * Generate and return a random string of the specified length.
7738   *
7739   * @param int $length The length of the string to be created.
7740   * @return string
7741   */
7742  function random_string($length=15) {
7743      $randombytes = random_bytes_emulate($length);
7744      $pool  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
7745      $pool .= 'abcdefghijklmnopqrstuvwxyz';
7746      $pool .= '0123456789';
7747      $poollen = strlen($pool);
7748      $string = '';
7749      for ($i = 0; $i < $length; $i++) {
7750          $rand = ord($randombytes[$i]);
7751          $string .= substr($pool, ($rand%($poollen)), 1);
7752      }
7753      return $string;
7754  }
7755  
7756  /**
7757   * Generate a complex random string (useful for md5 salts)
7758   *
7759   * This function is based on the above {@link random_string()} however it uses a
7760   * larger pool of characters and generates a string between 24 and 32 characters
7761   *
7762   * @param int $length Optional if set generates a string to exactly this length
7763   * @return string
7764   */
7765  function complex_random_string($length=null) {
7766      $pool  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
7767      $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
7768      $poollen = strlen($pool);
7769      if ($length===null) {
7770          $length = floor(rand(24, 32));
7771      }
7772      $randombytes = random_bytes_emulate($length);
7773      $string = '';
7774      for ($i = 0; $i < $length; $i++) {
7775          $rand = ord($randombytes[$i]);
7776          $string .= $pool[($rand%$poollen)];
7777      }
7778      return $string;
7779  }
7780  
7781  /**
7782   * Try to generates cryptographically secure pseudo-random bytes.
7783   *
7784   * Note this is achieved by fallbacking between:
7785   *  - PHP 7 random_bytes().
7786   *  - OpenSSL openssl_random_pseudo_bytes().
7787   *  - In house random generator getting its entropy from various, hard to guess, pseudo-random sources.
7788   *
7789   * @param int $length requested length in bytes
7790   * @return string binary data
7791   */
7792  function random_bytes_emulate($length) {
7793      global $CFG;
7794      if ($length <= 0) {
7795          debugging('Invalid random bytes length', DEBUG_DEVELOPER);
7796          return '';
7797      }
7798      if (function_exists('random_bytes')) {
7799          // Use PHP 7 goodness.
7800          $hash = @random_bytes($length);
7801          if ($hash !== false) {
7802              return $hash;
7803          }
7804      }
7805      if (function_exists('openssl_random_pseudo_bytes')) {
7806          // For PHP 5.3 and later with openssl extension.
7807          $hash = openssl_random_pseudo_bytes($length);
7808          if ($hash !== false) {
7809              return $hash;
7810          }
7811      }
7812  
7813      // Bad luck, there is no reliable random generator, let's just hash some unique stuff that is hard to guess.
7814      $hash = sha1(serialize($CFG) . serialize($_SERVER) . microtime(true) . uniqid('', true), true);
7815      // NOTE: the last param in sha1() is true, this means we are getting 20 bytes, not 40 chars as usual.
7816      if ($length <= 20) {
7817          return substr($hash, 0, $length);
7818      }
7819      return $hash . random_bytes_emulate($length - 20);
7820  }
7821  
7822  /**
7823   * Given some text (which may contain HTML) and an ideal length,
7824   * this function truncates the text neatly on a word boundary if possible
7825   *
7826   * @category string
7827   * @param string $text text to be shortened
7828   * @param int $ideal ideal string length
7829   * @param boolean $exact if false, $text will not be cut mid-word
7830   * @param string $ending The string to append if the passed string is truncated
7831   * @return string $truncate shortened string
7832   */
7833  function shorten_text($text, $ideal=30, $exact = false, $ending='...') {
7834      // If the plain text is shorter than the maximum length, return the whole text.
7835      if (core_text::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) {
7836          return $text;
7837      }
7838  
7839      // Splits on HTML tags. Each open/close/empty tag will be the first thing
7840      // and only tag in its 'line'.
7841      preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
7842  
7843      $totallength = core_text::strlen($ending);
7844      $truncate = '';
7845  
7846      // This array stores information about open and close tags and their position
7847      // in the truncated string. Each item in the array is an object with fields
7848      // ->open (true if open), ->tag (tag name in lower case), and ->pos
7849      // (byte position in truncated text).
7850      $tagdetails = array();
7851  
7852      foreach ($lines as $linematchings) {
7853          // If there is any html-tag in this line, handle it and add it (uncounted) to the output.
7854          if (!empty($linematchings[1])) {
7855              // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>).
7856              if (!preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $linematchings[1])) {
7857                  if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $linematchings[1], $tagmatchings)) {
7858                      // Record closing tag.
7859                      $tagdetails[] = (object) array(
7860                              'open' => false,
7861                              'tag'  => core_text::strtolower($tagmatchings[1]),
7862                              'pos'  => core_text::strlen($truncate),
7863                          );
7864  
7865                  } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $linematchings[1], $tagmatchings)) {
7866                      // Record opening tag.
7867                      $tagdetails[] = (object) array(
7868                              'open' => true,
7869                              'tag'  => core_text::strtolower($tagmatchings[1]),
7870                              'pos'  => core_text::strlen($truncate),
7871                          );
7872                  } else if (preg_match('/^<!--\[if\s.*?\]>$/s', $linematchings[1], $tagmatchings)) {
7873                      $tagdetails[] = (object) array(
7874                              'open' => true,
7875                              'tag'  => core_text::strtolower('if'),
7876                              'pos'  => core_text::strlen($truncate),
7877                      );
7878                  } else if (preg_match('/^<!--<!\[endif\]-->$/s', $linematchings[1], $tagmatchings)) {
7879                      $tagdetails[] = (object) array(
7880                              'open' => false,
7881                              'tag'  => core_text::strtolower('if'),
7882                              'pos'  => core_text::strlen($truncate),
7883                      );
7884                  }
7885              }
7886              // Add html-tag to $truncate'd text.
7887              $truncate .= $linematchings[1];
7888          }
7889  
7890          // Calculate the length of the plain text part of the line; handle entities as one character.
7891          $contentlength = core_text::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $linematchings[2]));
7892          if ($totallength + $contentlength > $ideal) {
7893              // The number of characters which are left.
7894              $left = $ideal - $totallength;
7895              $entitieslength = 0;
7896              // Search for html entities.
7897              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)) {
7898                  // Calculate the real length of all entities in the legal range.
7899                  foreach ($entities[0] as $entity) {
7900                      if ($entity[1]+1-$entitieslength <= $left) {
7901                          $left--;
7902                          $entitieslength += core_text::strlen($entity[0]);
7903                      } else {
7904                          // No more characters left.
7905                          break;
7906                      }
7907                  }
7908              }
7909              $breakpos = $left + $entitieslength;
7910  
7911              // If the words shouldn't be cut in the middle...
7912              if (!$exact) {
7913                  // Search the last occurence of a space.
7914                  for (; $breakpos > 0; $breakpos--) {
7915                      if ($char = core_text::substr($linematchings[2], $breakpos, 1)) {
7916                          if ($char === '.' or $char === ' ') {
7917                              $breakpos += 1;
7918                              break;
7919                          } else if (strlen($char) > 2) {
7920                              // Chinese/Japanese/Korean text can be truncated at any UTF-8 character boundary.
7921                              $breakpos += 1;
7922                              break;
7923                          }
7924                      }
7925                  }
7926              }
7927              if ($breakpos == 0) {
7928                  // This deals with the test_shorten_text_no_spaces case.
7929                  $breakpos = $left + $entitieslength;
7930              } else if ($breakpos > $left + $entitieslength) {
7931                  // This deals with the previous for loop breaking on the first char.
7932                  $breakpos = $left + $entitieslength;
7933              }
7934  
7935              $truncate .= core_text::substr($linematchings[2], 0, $breakpos);
7936              // Maximum length is reached, so get off the loop.
7937              break;
7938          } else {
7939              $truncate .= $linematchings[2];
7940              $totallength += $contentlength;
7941          }
7942  
7943          // If the maximum length is reached, get off the loop.
7944          if ($totallength >= $ideal) {
7945              break;
7946          }
7947      }
7948  
7949      // Add the defined ending to the text.
7950      $truncate .= $ending;
7951  
7952      // Now calculate the list of open html tags based on the truncate position.
7953      $opentags = array();
7954      foreach ($tagdetails as $taginfo) {
7955          if ($taginfo->open) {
7956              // Add tag to the beginning of $opentags list.
7957              array_unshift($opentags, $taginfo->tag);
7958          } else {
7959              // Can have multiple exact same open tags, close the last one.
7960              $pos = array_search($taginfo->tag, array_reverse($opentags, true));
7961              if ($pos !== false) {
7962                  unset($opentags[$pos]);
7963              }
7964          }
7965      }
7966  
7967      // Close all unclosed html-tags.
7968      foreach ($opentags as $tag) {
7969          if ($tag === 'if') {
7970              $truncate .= '<!--<![endif]-->';
7971          } else {
7972              $truncate .= '</' . $tag . '>';
7973          }
7974      }
7975  
7976      return $truncate;
7977  }
7978  
7979  
7980  /**
7981   * Given dates in seconds, how many weeks is the date from startdate
7982   * The first week is 1, the second 2 etc ...
7983   *
7984   * @param int $startdate Timestamp for the start date
7985   * @param int $thedate Timestamp for the end date
7986   * @return string
7987   */
7988  function getweek ($startdate, $thedate) {
7989      if ($thedate < $startdate) {
7990          return 0;
7991      }
7992  
7993      return floor(($thedate - $startdate) / WEEKSECS) + 1;
7994  }
7995  
7996  /**
7997   * Returns a randomly generated password of length $maxlen.  inspired by
7998   *
7999   * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and
8000   * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254}
8001   *
8002   * @param int $maxlen  The maximum size of the password being generated.
8003   * @return string
8004   */
8005  function generate_password($maxlen=10) {
8006      global $CFG;
8007  
8008      if (empty($CFG->passwordpolicy)) {
8009          $fillers = PASSWORD_DIGITS;
8010          $wordlist = file($CFG->wordlist);
8011          $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8012          $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8013          $filler1 = $fillers[rand(0, strlen($fillers) - 1)];
8014          $password = $word1 . $filler1 . $word2;
8015      } else {
8016          $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0;
8017          $digits = $CFG->minpassworddigits;
8018          $lower = $CFG->minpasswordlower;
8019          $upper = $CFG->minpasswordupper;
8020          $nonalphanum = $CFG->minpasswordnonalphanum;
8021          $total = $lower + $upper + $digits + $nonalphanum;
8022          // Var minlength should be the greater one of the two ( $minlen and $total ).
8023          $minlen = $minlen < $total ? $total : $minlen;
8024          // Var maxlen can never be smaller than minlen.
8025          $maxlen = $minlen > $maxlen ? $minlen : $maxlen;
8026          $additional = $maxlen - $total;
8027  
8028          // Make sure we have enough characters to fulfill
8029          // complexity requirements.
8030          $passworddigits = PASSWORD_DIGITS;
8031          while ($digits > strlen($passworddigits)) {
8032              $passworddigits .= PASSWORD_DIGITS;
8033          }
8034          $passwordlower = PASSWORD_LOWER;
8035          while ($lower > strlen($passwordlower)) {
8036              $passwordlower .= PASSWORD_LOWER;
8037          }
8038          $passwordupper = PASSWORD_UPPER;
8039          while ($upper > strlen($passwordupper)) {
8040              $passwordupper .= PASSWORD_UPPER;
8041          }
8042          $passwordnonalphanum = PASSWORD_NONALPHANUM;
8043          while ($nonalphanum > strlen($passwordnonalphanum)) {
8044              $passwordnonalphanum .= PASSWORD_NONALPHANUM;
8045          }
8046  
8047          // Now mix and shuffle it all.
8048          $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) .
8049                                   substr(str_shuffle ($passwordupper), 0, $upper) .
8050                                   substr(str_shuffle ($passworddigits), 0, $digits) .
8051                                   substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) .
8052                                   substr(str_shuffle ($passwordlower .
8053                                                       $passwordupper .
8054                                                       $passworddigits .
8055                                                       $passwordnonalphanum), 0 , $additional));
8056      }
8057  
8058      return substr ($password, 0, $maxlen);
8059  }
8060  
8061  /**
8062   * Given a float, prints it nicely.
8063   * Localized floats must not be used in calculations!
8064   *
8065   * The stripzeros feature is intended for making numbers look nicer in small
8066   * areas where it is not necessary to indicate the degree of accuracy by showing
8067   * ending zeros. If you turn it on with $decimalpoints set to 3, for example,
8068   * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'.
8069   *
8070   * @param float $float The float to print
8071   * @param int $decimalpoints The number of decimal places to print.
8072   * @param bool $localized use localized decimal separator
8073   * @param bool $stripzeros If true, removes final zeros after decimal point
8074   * @return string locale float
8075   */
8076  function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=false) {
8077      if (is_null($float)) {
8078          return '';
8079      }
8080      if ($localized) {
8081          $separator = get_string('decsep', 'langconfig');
8082      } else {
8083          $separator = '.';
8084      }
8085      $result = number_format($float, $decimalpoints, $separator, '');
8086      if ($stripzeros) {
8087          // Remove zeros and final dot if not needed.
8088          $result = preg_replace('~(' . preg_quote($separator) . ')?0+$~', '', $result);
8089      }
8090      return $result;
8091  }
8092  
8093  /**
8094   * Converts locale specific floating point/comma number back to standard PHP float value
8095   * Do NOT try to do any math operations before this conversion on any user submitted floats!
8096   *
8097   * @param string $localefloat locale aware float representation
8098   * @param bool $strict If true, then check the input and return false if it is not a valid number.
8099   * @return mixed float|bool - false or the parsed float.
8100   */
8101  function unformat_float($localefloat, $strict = false) {
8102      $localefloat = trim($localefloat);
8103  
8104      if ($localefloat == '') {
8105          return null;
8106      }
8107  
8108      $localefloat = str_replace(' ', '', $localefloat); // No spaces - those might be used as thousand separators.
8109      $localefloat = str_replace(get_string('decsep', 'langconfig'), '.', $localefloat);
8110  
8111      if ($strict && !is_numeric($localefloat)) {
8112          return false;
8113      }
8114  
8115      return (float)$localefloat;
8116  }
8117  
8118  /**
8119   * Given a simple array, this shuffles it up just like shuffle()
8120   * Unlike PHP's shuffle() this function works on any machine.
8121   *
8122   * @param array $array The array to be rearranged
8123   * @return array
8124   */
8125  function swapshuffle($array) {
8126  
8127      $last = count($array) - 1;
8128      for ($i = 0; $i <= $last; $i++) {
8129          $from = rand(0, $last);
8130          $curr = $array[$i];
8131          $array[$i] = $array[$from];
8132          $array[$from] = $curr;
8133      }
8134      return $array;
8135  }
8136  
8137  /**
8138   * Like {@link swapshuffle()}, but works on associative arrays
8139   *
8140   * @param array $array The associative array to be rearranged
8141   * @return array
8142   */
8143  function swapshuffle_assoc($array) {
8144  
8145      $newarray = array();
8146      $newkeys = swapshuffle(array_keys($array));
8147  
8148      foreach ($newkeys as $newkey) {
8149          $newarray[$newkey] = $array[$newkey];
8150      }
8151      return $newarray;
8152  }
8153  
8154  /**
8155   * Given an arbitrary array, and a number of draws,
8156   * this function returns an array with that amount
8157   * of items.  The indexes are retained.
8158   *
8159   * @todo Finish documenting this function
8160   *
8161   * @param array $array
8162   * @param int $draws
8163   * @return array
8164   */
8165  function draw_rand_array($array, $draws) {
8166  
8167      $return = array();
8168  
8169      $last = count($array);
8170  
8171      if ($draws > $last) {
8172          $draws = $last;
8173      }
8174  
8175      while ($draws > 0) {
8176          $last--;
8177  
8178          $keys = array_keys($array);
8179          $rand = rand(0, $last);
8180  
8181          $return[$keys[$rand]] = $array[$keys[$rand]];
8182          unset($array[$keys[$rand]]);
8183  
8184          $draws--;
8185      }
8186  
8187      return $return;
8188  }
8189  
8190  /**
8191   * Calculate the difference between two microtimes
8192   *
8193   * @param string $a The first Microtime
8194   * @param string $b The second Microtime
8195   * @return string
8196   */
8197  function microtime_diff($a, $b) {
8198      list($adec, $asec) = explode(' ', $a);
8199      list($bdec, $bsec) = explode(' ', $b);
8200      return $bsec - $asec + $bdec - $adec;
8201  }
8202  
8203  /**
8204   * Given a list (eg a,b,c,d,e) this function returns
8205   * an array of 1->a, 2->b, 3->c etc
8206   *
8207   * @param string $list The string to explode into array bits
8208   * @param string $separator The separator used within the list string
8209   * @return array The now assembled array
8210   */
8211  function make_menu_from_list($list, $separator=',') {
8212  
8213      $array = array_reverse(explode($separator, $list), true);
8214      foreach ($array as $key => $item) {
8215          $outarray[$key+1] = trim($item);
8216      }
8217      return $outarray;
8218  }
8219  
8220  /**
8221   * Creates an array that represents all the current grades that
8222   * can be chosen using the given grading type.
8223   *
8224   * Negative numbers
8225   * are scales, zero is no grade, and positive numbers are maximum
8226   * grades.
8227   *
8228   * @todo Finish documenting this function or better deprecated this completely!
8229   *
8230   * @param int $gradingtype
8231   * @return array
8232   */
8233  function make_grades_menu($gradingtype) {
8234      global $DB;
8235  
8236      $grades = array();
8237      if ($gradingtype < 0) {
8238          if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) {
8239              return make_menu_from_list($scale->scale);
8240          }
8241      } else if ($gradingtype > 0) {
8242          for ($i=$gradingtype; $i>=0; $i--) {
8243              $grades[$i] = $i .' / '. $gradingtype;
8244          }
8245          return $grades;
8246      }
8247      return $grades;
8248  }
8249  
8250  /**
8251   * make_unique_id_code
8252   *
8253   * @todo Finish documenting this function
8254   *
8255   * @uses $_SERVER
8256   * @param string $extra Extra string to append to the end of the code
8257   * @return string
8258   */
8259  function make_unique_id_code($extra = '') {
8260  
8261      $hostname = 'unknownhost';
8262      if (!empty($_SERVER['HTTP_HOST'])) {
8263          $hostname = $_SERVER['HTTP_HOST'];
8264      } else if (!empty($_ENV['HTTP_HOST'])) {
8265          $hostname = $_ENV['HTTP_HOST'];
8266      } else if (!empty($_SERVER['SERVER_NAME'])) {
8267          $hostname = $_SERVER['SERVER_NAME'];
8268      } else if (!empty($_ENV['SERVER_NAME'])) {
8269          $hostname = $_ENV['SERVER_NAME'];
8270      }
8271  
8272      $date = gmdate("ymdHis");
8273  
8274      $random =  random_string(6);
8275  
8276      if ($extra) {
8277          return $hostname .'+'. $date .'+'. $random .'+'. $extra;
8278      } else {
8279          return $hostname .'+'. $date .'+'. $random;
8280      }
8281  }
8282  
8283  
8284  /**
8285   * Function to check the passed address is within the passed subnet
8286   *
8287   * The parameter is a comma separated string of subnet definitions.
8288   * Subnet strings can be in one of three formats:
8289   *   1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn          (number of bits in net mask)
8290   *   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)
8291   *   3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.                  (incomplete address, a bit non-technical ;-)
8292   * Code for type 1 modified from user posted comments by mediator at
8293   * {@link http://au.php.net/manual/en/function.ip2long.php}
8294   *
8295   * @param string $addr    The address you are checking
8296   * @param string $subnetstr    The string of subnet addresses
8297   * @return bool
8298   */
8299  function address_in_subnet($addr, $subnetstr) {
8300  
8301      if ($addr == '0.0.0.0') {
8302          return false;
8303      }
8304      $subnets = explode(',', $subnetstr);
8305      $found = false;
8306      $addr = trim($addr);
8307      $addr = cleanremoteaddr($addr, false); // Normalise.
8308      if ($addr === null) {
8309          return false;
8310      }
8311      $addrparts = explode(':', $addr);
8312  
8313      $ipv6 = strpos($addr, ':');
8314  
8315      foreach ($subnets as $subnet) {
8316          $subnet = trim($subnet);
8317          if ($subnet === '') {
8318              continue;
8319          }
8320  
8321          if (strpos($subnet, '/') !== false) {
8322              // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn.
8323              list($ip, $mask) = explode('/', $subnet);
8324              $mask = trim($mask);
8325              if (!is_number($mask)) {
8326                  continue; // Incorect mask number, eh?
8327              }
8328              $ip = cleanremoteaddr($ip, false); // Normalise.
8329              if ($ip === null) {
8330                  continue;
8331              }
8332              if (strpos($ip, ':') !== false) {
8333                  // IPv6.
8334                  if (!$ipv6) {
8335                      continue;
8336                  }
8337                  if ($mask > 128 or $mask < 0) {
8338                      continue; // Nonsense.
8339                  }
8340                  if ($mask == 0) {
8341                      return true; // Any address.
8342                  }
8343                  if ($mask == 128) {
8344                      if ($ip === $addr) {
8345                          return true;
8346                      }
8347                      continue;
8348                  }
8349                  $ipparts = explode(':', $ip);
8350                  $modulo  = $mask % 16;
8351                  $ipnet   = array_slice($ipparts, 0, ($mask-$modulo)/16);
8352                  $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16);
8353                  if (implode(':', $ipnet) === implode(':', $addrnet)) {
8354                      if ($modulo == 0) {
8355                          return true;
8356                      }
8357                      $pos     = ($mask-$modulo)/16;
8358                      $ipnet   = hexdec($ipparts[$pos]);
8359                      $addrnet = hexdec($addrparts[$pos]);
8360                      $mask    = 0xffff << (16 - $modulo);
8361                      if (($addrnet & $mask) == ($ipnet & $mask)) {
8362                          return true;
8363                      }
8364                  }
8365  
8366              } else {
8367                  // IPv4.
8368                  if ($ipv6) {
8369                      continue;
8370                  }
8371                  if ($mask > 32 or $mask < 0) {
8372                      continue; // Nonsense.
8373                  }
8374                  if ($mask == 0) {
8375                      return true;
8376                  }
8377                  if ($mask == 32) {
8378                      if ($ip === $addr) {
8379                          return true;
8380                      }
8381                      continue;
8382                  }
8383                  $mask = 0xffffffff << (32 - $mask);
8384                  if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
8385                      return true;
8386                  }
8387              }
8388  
8389          } else if (strpos($subnet, '-') !== false) {
8390              // 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.
8391              $parts = explode('-', $subnet);
8392              if (count($parts) != 2) {
8393                  continue;
8394              }
8395  
8396              if (strpos($subnet, ':') !== false) {
8397                  // IPv6.
8398                  if (!$ipv6) {
8399                      continue;
8400                  }
8401                  $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
8402                  if ($ipstart === null) {
8403                      continue;
8404                  }
8405                  $ipparts = explode(':', $ipstart);
8406                  $start = hexdec(array_pop($ipparts));
8407                  $ipparts[] = trim($parts[1]);
8408                  $ipend = cleanremoteaddr(implode(':', $ipparts), false); // Normalise.
8409                  if ($ipend === null) {
8410                      continue;
8411                  }
8412                  $ipparts[7] = '';
8413                  $ipnet = implode(':', $ipparts);
8414                  if (strpos($addr, $ipnet) !== 0) {
8415                      continue;
8416                  }
8417                  $ipparts = explode(':', $ipend);
8418                  $end = hexdec($ipparts[7]);
8419  
8420                  $addrend = hexdec($addrparts[7]);
8421  
8422                  if (($addrend >= $start) and ($addrend <= $end)) {
8423                      return true;
8424                  }
8425  
8426              } else {
8427                  // IPv4.
8428                  if ($ipv6) {
8429                      continue;
8430                  }
8431                  $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
8432                  if ($ipstart === null) {
8433                      continue;
8434                  }
8435                  $ipparts = explode('.', $ipstart);
8436                  $ipparts[3] = trim($parts[1]);
8437                  $ipend = cleanremoteaddr(implode('.', $ipparts), false); // Normalise.
8438                  if ($ipend === null) {
8439                      continue;
8440                  }
8441  
8442                  if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
8443                      return true;
8444                  }
8445              }
8446  
8447          } else {
8448              // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
8449              if (strpos($subnet, ':') !== false) {
8450                  // IPv6.
8451                  if (!$ipv6) {
8452                      continue;
8453                  }
8454                  $parts = explode(':', $subnet);
8455                  $count = count($parts);
8456                  if ($parts[$count-1] === '') {
8457                      unset($parts[$count-1]); // Trim trailing :'s.
8458                      $count--;
8459                      $subnet = implode('.', $parts);
8460                  }
8461                  $isip = cleanremoteaddr($subnet, false); // Normalise.
8462                  if ($isip !== null) {
8463                      if ($isip === $addr) {
8464                          return true;
8465                      }
8466                      continue;
8467                  } else if ($count > 8) {
8468                      continue;
8469                  }
8470                  $zeros = array_fill(0, 8-$count, '0');
8471                  $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16);
8472                  if (address_in_subnet($addr, $subnet)) {
8473                      return true;
8474                  }
8475  
8476              } else {
8477                  // IPv4.
8478                  if ($ipv6) {
8479                      continue;
8480                  }
8481                  $parts = explode('.', $subnet);
8482                  $count = count($parts);
8483                  if ($parts[$count-1] === '') {
8484                      unset($parts[$count-1]); // Trim trailing .
8485                      $count--;
8486                      $subnet = implode('.', $parts);
8487                  }
8488                  if ($count == 4) {
8489                      $subnet = cleanremoteaddr($subnet, false); // Normalise.
8490                      if ($subnet === $addr) {
8491                          return true;
8492                      }
8493                      continue;
8494                  } else if ($count > 4) {
8495                      continue;
8496                  }
8497                  $zeros = array_fill(0, 4-$count, '0');
8498                  $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8);
8499                  if (address_in_subnet($addr, $subnet)) {
8500                      return true;
8501                  }
8502              }
8503          }
8504      }
8505  
8506      return false;
8507  }
8508  
8509  /**
8510   * For outputting debugging info
8511   *
8512   * @param string $string The string to write
8513   * @param string $eol The end of line char(s) to use
8514   * @param string $sleep Period to make the application sleep
8515   *                      This ensures any messages have time to display before redirect
8516   */
8517  function mtrace($string, $eol="\n", $sleep=0) {
8518  
8519      if (defined('STDOUT') && !PHPUNIT_TEST && !defined('BEHAT_TEST')) {
8520          fwrite(STDOUT, $string.$eol);
8521      } else {
8522          echo $string . $eol;
8523      }
8524  
8525      flush();
8526  
8527      // Delay to keep message on user's screen in case of subsequent redirect.
8528      if ($sleep) {
8529          sleep($sleep);
8530      }
8531  }
8532  
8533  /**
8534   * Replace 1 or more slashes or backslashes to 1 slash
8535   *
8536   * @param string $path The path to strip
8537   * @return string the path with double slashes removed
8538   */
8539  function cleardoubleslashes ($path) {
8540      return preg_replace('/(\/|\\\){1,}/', '/', $path);
8541  }
8542  
8543  /**
8544   * Is current ip in give list?
8545   *
8546   * @param string $list
8547   * @return bool
8548   */
8549  function remoteip_in_list($list) {
8550      $inlist = false;
8551      $clientip = getremoteaddr(null);
8552  
8553      if (!$clientip) {
8554          // Ensure access on cli.
8555          return true;
8556      }
8557  
8558      $list = explode("\n", $list);
8559      foreach ($list as $subnet) {
8560          $subnet = trim($subnet);
8561          if (address_in_subnet($clientip, $subnet)) {
8562              $inlist = true;
8563              break;
8564          }
8565      }
8566      return $inlist;
8567  }
8568  
8569  /**
8570   * Returns most reliable client address
8571   *
8572   * @param string $default If an address can't be determined, then return this
8573   * @return string The remote IP address
8574   */
8575  function getremoteaddr($default='0.0.0.0') {
8576      global $CFG;
8577  
8578      if (empty($CFG->getremoteaddrconf)) {
8579          // This will happen, for example, before just after the upgrade, as the
8580          // user is redirected to the admin screen.
8581          $variablestoskip = 0;
8582      } else {
8583          $variablestoskip = $CFG->getremoteaddrconf;
8584      }
8585      if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) {
8586          if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
8587              $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']);
8588              return $address ? $address : $default;
8589          }
8590      }
8591      if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
8592          if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
8593              $forwardedaddresses = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']);
8594              $address = $forwardedaddresses[0];
8595  
8596              if (substr_count($address, ":") > 1) {
8597                  // Remove port and brackets from IPv6.
8598                  if (preg_match("/\[(.*)\]:/", $address, $matches)) {
8599                      $address = $matches[1];
8600                  }
8601              } else {
8602                  // Remove port from IPv4.
8603                  if (substr_count($address, ":") == 1) {
8604                      $parts = explode(":", $address);
8605                      $address = $parts[0];
8606                  }
8607              }
8608  
8609              $address = cleanremoteaddr($address);
8610              return $address ? $address : $default;
8611          }
8612      }
8613      if (!empty($_SERVER['REMOTE_ADDR'])) {
8614          $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']);
8615          return $address ? $address : $default;
8616      } else {
8617          return $default;
8618      }
8619  }
8620  
8621  /**
8622   * Cleans an ip address. Internal addresses are now allowed.
8623   * (Originally local addresses were not allowed.)
8624   *
8625   * @param string $addr IPv4 or IPv6 address
8626   * @param bool $compress use IPv6 address compression
8627   * @return string normalised ip address string, null if error
8628   */
8629  function cleanremoteaddr($addr, $compress=false) {
8630      $addr = trim($addr);
8631  
8632      // TODO: maybe add a separate function is_addr_public() or something like this.
8633  
8634      if (strpos($addr, ':') !== false) {
8635          // Can be only IPv6.
8636          $parts = explode(':', $addr);
8637          $count = count($parts);
8638  
8639          if (strpos($parts[$count-1], '.') !== false) {
8640              // Legacy ipv4 notation.
8641              $last = array_pop($parts);
8642              $ipv4 = cleanremoteaddr($last, true);
8643              if ($ipv4 === null) {
8644                  return null;
8645              }
8646              $bits = explode('.', $ipv4);
8647              $parts[] = dechex($bits[0]).dechex($bits[1]);
8648              $parts[] = dechex($bits[2]).dechex($bits[3]);
8649              $count = count($parts);
8650              $addr = implode(':', $parts);
8651          }
8652  
8653          if ($count < 3 or $count > 8) {
8654              return null; // Severly malformed.
8655          }
8656  
8657          if ($count != 8) {
8658              if (strpos($addr, '::') === false) {
8659                  return null; // Malformed.
8660              }
8661              // Uncompress.
8662              $insertat = array_search('', $parts, true);
8663              $missing = array_fill(0, 1 + 8 - $count, '0');
8664              array_splice($parts, $insertat, 1, $missing);
8665              foreach ($parts as $key => $part) {
8666                  if ($part === '') {
8667                      $parts[$key] = '0';
8668                  }
8669              }
8670          }
8671  
8672          $adr = implode(':', $parts);
8673          if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) {
8674              return null; // Incorrect format - sorry.
8675          }
8676  
8677          // Normalise 0s and case.
8678          $parts = array_map('hexdec', $parts);
8679          $parts = array_map('dechex', $parts);
8680  
8681          $result = implode(':', $parts);
8682  
8683          if (!$compress) {
8684              return $result;
8685          }
8686  
8687          if ($result === '0:0:0:0:0:0:0:0') {
8688              return '::'; // All addresses.
8689          }
8690  
8691          $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1);
8692          if ($compressed !== $result) {
8693              return $compressed;
8694          }
8695  
8696          $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
8697          if ($compressed !== $result) {
8698              return $compressed;
8699          }
8700  
8701          $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
8702          if ($compressed !== $result) {
8703              return $compressed;
8704          }
8705  
8706          return $result;
8707      }
8708  
8709      // First get all things that look like IPv4 addresses.
8710      $parts = array();
8711      if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
8712          return null;
8713      }
8714      unset($parts[0]);
8715  
8716      foreach ($parts as $key => $match) {
8717          if ($match > 255) {
8718              return null;
8719          }
8720          $parts[$key] = (int)$match; // Normalise 0s.
8721      }
8722  
8723      return implode('.', $parts);
8724  }
8725  
8726  /**
8727   * This function will make a complete copy of anything it's given,
8728   * regardless of whether it's an object or not.
8729   *
8730   * @param mixed $thing Something you want cloned
8731   * @return mixed What ever it is you passed it
8732   */
8733  function fullclone($thing) {
8734      return unserialize(serialize($thing));
8735  }
8736  
8737   /**
8738    * If new messages are waiting for the current user, then insert
8739    * JavaScript to pop up the messaging window into the page
8740    *
8741    * @return void
8742    */
8743  function message_popup_window() {
8744      global $USER, $DB, $PAGE, $CFG;
8745  
8746      if (!$PAGE->get_popup_notification_allowed() || empty($CFG->messaging)) {
8747          return;
8748      }
8749  
8750      if (!isloggedin() || isguestuser()) {
8751          return;
8752      }
8753  
8754      if (!isset($USER->message_lastpopup)) {
8755          $USER->message_lastpopup = 0;
8756      } else if ($USER->message_lastpopup > (time()-120)) {
8757          // Don't run the query to check whether to display a popup if its been run in the last 2 minutes.
8758          return;
8759      }
8760  
8761      // A quick query to check whether the user has new messages.
8762      $messagecount = $DB->count_records('message', array('useridto' => $USER->id));
8763      if ($messagecount < 1) {
8764          return;
8765      }
8766  
8767      // There are unread messages so now do a more complex but slower query.
8768      $messagesql = "SELECT m.id, c.blocked
8769                       FROM {message} m
8770                       JOIN {message_working} mw ON m.id=mw.unreadmessageid
8771                       JOIN {message_processors} p ON mw.processorid=p.id
8772                       LEFT JOIN {message_contacts} c ON c.contactid = m.useridfrom
8773                                                     AND c.userid = m.useridto
8774                      WHERE m.useridto = :userid
8775                        AND p.name='popup'";
8776  
8777      // If the user was last notified over an hour ago we can re-notify them of old messages
8778      // so don't worry about when the new message was sent.
8779      $lastnotifiedlongago = $USER->message_lastpopup < (time()-3600);
8780      if (!$lastnotifiedlongago) {
8781          $messagesql .= 'AND m.timecreated > :lastpopuptime';
8782      }
8783  
8784      $waitingmessages = $DB->get_records_sql($messagesql, array('userid' => $USER->id, 'lastpopuptime' => $USER->message_lastpopup));
8785  
8786      $validmessages = 0;
8787      foreach ($waitingmessages as $messageinfo) {
8788          if ($messageinfo->blocked) {
8789              // Message is from a user who has since been blocked so just mark it read.
8790              // Get the full message to mark as read.
8791              $messageobject = $DB->get_record('message', array('id' => $messageinfo->id));
8792              message_mark_message_read($messageobject, time());
8793          } else {
8794              $validmessages++;
8795          }
8796      }
8797  
8798      if ($validmessages > 0) {
8799          $strmessages = get_string('unreadnewmessages', 'message', $validmessages);
8800          $strgomessage = get_string('gotomessages', 'message');
8801          $strstaymessage = get_string('ignore', 'admin');
8802  
8803          $notificationsound = null;
8804          $beep = get_user_preferences('message_beepnewmessage', '');
8805          if (!empty($beep)) {
8806              // Browsers will work down this list until they find something they support.
8807              $sourcetags =  html_writer::empty_tag('source', array('src' => $CFG->wwwroot.'/message/bell.wav', 'type' => 'audio/wav'));
8808              $sourcetags .= html_writer::empty_tag('source', array('src' => $CFG->wwwroot.'/message/bell.ogg', 'type' => 'audio/ogg'));
8809              $sourcetags .= html_writer::empty_tag('source', array('src' => $CFG->wwwroot.'/message/bell.mp3', 'type' => 'audio/mpeg'));
8810              $sourcetags .= html_writer::empty_tag('embed',  array('src' => $CFG->wwwroot.'/message/bell.wav', 'autostart' => 'true', 'hidden' => 'true'));
8811  
8812              $notificationsound = html_writer::tag('audio', $sourcetags, array('preload' => 'auto', 'autoplay' => 'autoplay'));
8813          }
8814  
8815          $url = $CFG->wwwroot.'/message/index.php';
8816          $content =  html_writer::start_tag('div', array('id' => 'newmessageoverlay', 'class' => 'mdl-align')).
8817                          html_writer::start_tag('div', array('id' => 'newmessagetext')).
8818                              $strmessages.
8819                          html_writer::end_tag('div').
8820  
8821                          $notificationsound.
8822                          html_writer::start_tag('div', array('id' => 'newmessagelinks')).
8823                          html_writer::link($url, $strgomessage, array('id' => 'notificationyes')).'&nbsp;&nbsp;&nbsp;'.
8824                          html_writer::link('', $strstaymessage, array('id' => 'notificationno')).
8825                          html_writer::end_tag('div');
8826                      html_writer::end_tag('div');
8827  
8828          $PAGE->requires->js_init_call('M.core_message.init_notification', array('', $content, $url));
8829  
8830          $USER->message_lastpopup = time();
8831      }
8832  }
8833  
8834  /**
8835   * Used to make sure that $min <= $value <= $max
8836   *
8837   * Make sure that value is between min, and max
8838   *
8839   * @param int $min The minimum value
8840   * @param int $value The value to check
8841   * @param int $max The maximum value
8842   * @return int
8843   */
8844  function bounded_number($min, $value, $max) {
8845      if ($value < $min) {
8846          return $min;
8847      }
8848      if ($value > $max) {
8849          return $max;
8850      }
8851      return $value;
8852  }
8853  
8854  /**
8855   * Check if there is a nested array within the passed array
8856   *
8857   * @param array $array
8858   * @return bool true if there is a nested array false otherwise
8859   */
8860  function array_is_nested($array) {
8861      foreach ($array as $value) {
8862          if (is_array($value)) {
8863              return true;
8864          }
8865      }
8866      return false;
8867  }
8868  
8869  /**
8870   * get_performance_info() pairs up with init_performance_info()
8871   * loaded in setup.php. Returns an array with 'html' and 'txt'
8872   * values ready for use, and each of the individual stats provided
8873   * separately as well.
8874   *
8875   * @return array
8876   */
8877  function get_performance_info() {
8878      global $CFG, $PERF, $DB, $PAGE;
8879  
8880      $info = array();
8881      $info['html'] = '';         // Holds userfriendly HTML representation.
8882      $info['txt']  = me() . ' '; // Holds log-friendly representation.
8883  
8884      $info['realtime'] = microtime_diff($PERF->starttime, microtime());
8885  
8886      $info['html'] .= '<span class="timeused">'.$info['realtime'].' secs</span> ';
8887      $info['txt'] .= 'time: '.$info['realtime'].'s ';
8888  
8889      if (function_exists('memory_get_usage')) {
8890          $info['memory_total'] = memory_get_usage();
8891          $info['memory_growth'] = memory_get_usage() - $PERF->startmemory;
8892          $info['html'] .= '<span class="memoryused">RAM: '.display_size($info['memory_total']).'</span> ';
8893          $info['txt']  .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '.
8894              $info['memory_growth'].'B ('.display_size($info['memory_growth']).') ';
8895      }
8896  
8897      if (function_exists('memory_get_peak_usage')) {
8898          $info['memory_peak'] = memory_get_peak_usage();
8899          $info['html'] .= '<span class="memoryused">RAM peak: '.display_size($info['memory_peak']).'</span> ';
8900          $info['txt']  .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') ';
8901      }
8902  
8903      $inc = get_included_files();
8904      $info['includecount'] = count($inc);
8905      $info['html'] .= '<span class="included">Included '.$info['includecount'].' files</span> ';
8906      $info['txt']  .= 'includecount: '.$info['includecount'].' ';
8907  
8908      if (!empty($CFG->early_install_lang) or empty($PAGE)) {
8909          // We can not track more performance before installation or before PAGE init, sorry.
8910          return $info;
8911      }
8912  
8913      $filtermanager = filter_manager::instance();
8914      if (method_exists($filtermanager, 'get_performance_summary')) {
8915          list($filterinfo, $nicenames) = $filtermanager->get_performance_summary();
8916          $info = array_merge($filterinfo, $info);
8917          foreach ($filterinfo as $key => $value) {
8918              $info['html'] .= "<span class='$key'>$nicenames[$key]: $value </span> ";
8919              $info['txt'] .= "$key: $value ";
8920          }
8921      }
8922  
8923      $stringmanager = get_string_manager();
8924      if (method_exists($stringmanager, 'get_performance_summary')) {
8925          list($filterinfo, $nicenames) = $stringmanager->get_performance_summary();
8926          $info = array_merge($filterinfo, $info);
8927          foreach ($filterinfo as $key => $value) {
8928              $info['html'] .= "<span class='$key'>$nicenames[$key]: $value </span> ";
8929              $info['txt'] .= "$key: $value ";
8930          }
8931      }
8932  
8933      if (!empty($PERF->logwrites)) {
8934          $info['logwrites'] = $PERF->logwrites;
8935          $info['html'] .= '<span class="logwrites">Log DB writes '.$info['logwrites'].'</span> ';
8936          $info['txt'] .= 'logwrites: '.$info['logwrites'].' ';
8937      }
8938  
8939      $info['dbqueries'] = $DB->perf_get_reads().'/'.($DB->perf_get_writes() - $PERF->logwrites);
8940      $info['html'] .= '<span class="dbqueries">DB reads/writes: '.$info['dbqueries'].'</span> ';
8941      $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' ';
8942  
8943      $info['dbtime'] = round($DB->perf_get_queries_time(), 5);
8944      $info['html'] .= '<span class="dbtime">DB queries time: '.$info['dbtime'].' secs</span> ';
8945      $info['txt'] .= 'db queries time: ' . $info['dbtime'] . 's ';
8946  
8947      if (function_exists('posix_times')) {
8948          $ptimes = posix_times();
8949          if (is_array($ptimes)) {
8950              foreach ($ptimes as $key => $val) {
8951                  $info[$key] = $ptimes[$key] -  $PERF->startposixtimes[$key];
8952              }
8953              $info['html'] .= "<span class=\"posixtimes\">ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</span> ";
8954              $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] ";
8955          }
8956      }
8957  
8958      // Grab the load average for the last minute.
8959      // /proc will only work under some linux configurations
8960      // while uptime is there under MacOSX/Darwin and other unices.
8961      if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) {
8962          list($serverload) = explode(' ', $loadavg[0]);
8963          unset($loadavg);
8964      } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) {
8965          if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) {
8966              $serverload = $matches[1];
8967          } else {
8968              trigger_error('Could not parse uptime output!');
8969          }
8970      }
8971      if (!empty($serverload)) {
8972          $info['serverload'] = $serverload;
8973          $info['html'] .= '<span class="serverload">Load average: '.$info['serverload'].'</span> ';
8974          $info['txt'] .= "serverload: {$info['serverload']} ";
8975      }
8976  
8977      // Display size of session if session started.
8978      if ($si = \core\session\manager::get_performance_info()) {
8979          $info['sessionsize'] = $si['size'];
8980          $info['html'] .= $si['html'];
8981          $info['txt'] .= $si['txt'];
8982      }
8983  
8984      if ($stats = cache_helper::get_stats()) {
8985          $html = '<span class="cachesused">';
8986          $html .= '<span class="cache-stats-heading">Caches used (hits/misses/sets)</span>';
8987          $text = 'Caches used (hits/misses/sets): ';
8988          $hits = 0;
8989          $misses = 0;
8990          $sets = 0;
8991          foreach ($stats as $definition => $details) {
8992              switch ($details['mode']) {
8993                  case cache_store::MODE_APPLICATION:
8994                      $modeclass = 'application';
8995                      $mode = ' <span title="application cache">[a]</span>';
8996                      break;
8997                  case cache_store::MODE_SESSION:
8998                      $modeclass = 'session';
8999                      $mode = ' <span title="session cache">[s]</span>';
9000                      break;
9001                  case cache_store::MODE_REQUEST:
9002                      $modeclass = 'request';
9003                      $mode = ' <span title="request cache">[r]</span>';
9004                      break;
9005              }
9006              $html .= '<span class="cache-definition-stats cache-mode-'.$modeclass.'">';
9007              $html .= '<span class="cache-definition-stats-heading">'.$definition.$mode.'</span>';
9008              $text .= "$definition {";
9009              foreach ($details['stores'] as $store => $data) {
9010                  $hits += $data['hits'];
9011                  $misses += $data['misses'];
9012                  $sets += $data['sets'];
9013                  if ($data['hits'] == 0 and $data['misses'] > 0) {
9014                      $cachestoreclass = 'nohits';
9015                  } else if ($data['hits'] < $data['misses']) {
9016                      $cachestoreclass = 'lowhits';
9017                  } else {
9018                      $cachestoreclass = 'hihits';
9019                  }
9020                  $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
9021                  $html .= "<span class=\"cache-store-stats $cachestoreclass\">$store: $data[hits] / $data[misses] / $data[sets]</span>";
9022              }
9023              $html .= '</span>';
9024              $text .= '} ';
9025          }
9026          $html .= "<span class='cache-total-stats'>Total: $hits / $misses / $sets</span>";
9027          $html .= '</span> ';
9028          $info['cachesused'] = "$hits / $misses / $sets";
9029          $info['html'] .= $html;
9030          $info['txt'] .= $text.'. ';
9031      } else {
9032          $info['cachesused'] = '0 / 0 / 0';
9033          $info['html'] .= '<span class="cachesused">Caches used (hits/misses/sets): 0/0/0</span>';
9034          $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 ';
9035      }
9036  
9037      $info['html'] = '<div class="performanceinfo siteinfo">'.$info['html'].'</div>';
9038      return $info;
9039  }
9040  
9041  /**
9042   * Delete directory or only its content
9043   *
9044   * @param string $dir directory path
9045   * @param bool $contentonly
9046   * @return bool success, true also if dir does not exist
9047   */
9048  function remove_dir($dir, $contentonly=false) {
9049      if (!file_exists($dir)) {
9050          // Nothing to do.
9051          return true;
9052      }
9053      if (!$handle = opendir($dir)) {
9054          return false;
9055      }
9056      $result = true;
9057      while (false!==($item = readdir($handle))) {
9058          if ($item != '.' && $item != '..') {
9059              if (is_dir($dir.'/'.$item)) {
9060                  $result = remove_dir($dir.'/'.$item) && $result;
9061              } else {
9062                  $result = unlink($dir.'/'.$item) && $result;
9063              }
9064          }
9065      }
9066      closedir($handle);
9067      if ($contentonly) {
9068          clearstatcache(); // Make sure file stat cache is properly invalidated.
9069          return $result;
9070      }
9071      $result = rmdir($dir); // If anything left the result will be false, no need for && $result.
9072      clearstatcache(); // Make sure file stat cache is properly invalidated.
9073      return $result;
9074  }
9075  
9076  /**
9077   * Detect if an object or a class contains a given property
9078   * will take an actual object or the name of a class
9079   *
9080   * @param mix $obj Name of class or real object to test
9081   * @param string $property name of property to find
9082   * @return bool true if property exists
9083   */
9084  function object_property_exists( $obj, $property ) {
9085      if (is_string( $obj )) {
9086          $properties = get_class_vars( $obj );
9087      } else {
9088          $properties = get_object_vars( $obj );
9089      }
9090      return array_key_exists( $property, $properties );
9091  }
9092  
9093  /**
9094   * Converts an object into an associative array
9095   *
9096   * This function converts an object into an associative array by iterating
9097   * over its public properties. Because this function uses the foreach
9098   * construct, Iterators are respected. It works recursively on arrays of objects.
9099   * Arrays and simple values are returned as is.
9100   *
9101   * If class has magic properties, it can implement IteratorAggregate
9102   * and return all available properties in getIterator()
9103   *
9104   * @param mixed $var
9105   * @return array
9106   */
9107  function convert_to_array($var) {
9108      $result = array();
9109  
9110      // Loop over elements/properties.
9111      foreach ($var as $key => $value) {
9112          // Recursively convert objects.
9113          if (is_object($value) || is_array($value)) {
9114              $result[$key] = convert_to_array($value);
9115          } else {
9116              // Simple values are untouched.
9117              $result[$key] = $value;
9118          }
9119      }
9120      return $result;
9121  }
9122  
9123  /**
9124   * Detect a custom script replacement in the data directory that will
9125   * replace an existing moodle script
9126   *
9127   * @return string|bool full path name if a custom script exists, false if no custom script exists
9128   */
9129  function custom_script_path() {
9130      global $CFG, $SCRIPT;
9131  
9132      if ($SCRIPT === null) {
9133          // Probably some weird external script.
9134          return false;
9135      }
9136  
9137      $scriptpath = $CFG->customscripts . $SCRIPT;
9138  
9139      // Check the custom script exists.
9140      if (file_exists($scriptpath) and is_file($scriptpath)) {
9141          return $scriptpath;
9142      } else {
9143          return false;
9144      }
9145  }
9146  
9147  /**
9148   * Returns whether or not the user object is a remote MNET user. This function
9149   * is in moodlelib because it does not rely on loading any of the MNET code.
9150   *
9151   * @param object $user A valid user object
9152   * @return bool        True if the user is from a remote Moodle.
9153   */
9154  function is_mnet_remote_user($user) {
9155      global $CFG;
9156  
9157      if (!isset($CFG->mnet_localhost_id)) {
9158          include_once($CFG->dirroot . '/mnet/lib.php');
9159          $env = new mnet_environment();
9160          $env->init();
9161          unset($env);
9162      }
9163  
9164      return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id);
9165  }
9166  
9167  /**
9168   * This function will search for browser prefereed languages, setting Moodle
9169   * to use the best one available if $SESSION->lang is undefined
9170   */
9171  function setup_lang_from_browser() {
9172      global $CFG, $SESSION, $USER;
9173  
9174      if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) {
9175          // Lang is defined in session or user profile, nothing to do.
9176          return;
9177      }
9178  
9179      if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do.
9180          return;
9181      }
9182  
9183      // Extract and clean langs from headers.
9184      $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
9185      $rawlangs = str_replace('-', '_', $rawlangs);         // We are using underscores.
9186      $rawlangs = explode(',', $rawlangs);                  // Convert to array.
9187      $langs = array();
9188  
9189      $order = 1.0;
9190      foreach ($rawlangs as $lang) {
9191          if (strpos($lang, ';') === false) {
9192              $langs[(string)$order] = $lang;
9193              $order = $order-0.01;
9194          } else {
9195              $parts = explode(';', $lang);
9196              $pos = strpos($parts[1], '=');
9197              $langs[substr($parts[1], $pos+1)] = $parts[0];
9198          }
9199      }
9200      krsort($langs, SORT_NUMERIC);
9201  
9202      // Look for such langs under standard locations.
9203      foreach ($langs as $lang) {
9204          // Clean it properly for include.
9205          $lang = strtolower(clean_param($lang, PARAM_SAFEDIR));
9206          if (get_string_manager()->translation_exists($lang, false)) {
9207              // Lang exists, set it in session.
9208              $SESSION->lang = $lang;
9209              // We have finished. Go out.
9210              break;
9211          }
9212      }
9213      return;
9214  }
9215  
9216  /**
9217   * Check if $url matches anything in proxybypass list
9218   *
9219   * Any errors just result in the proxy being used (least bad)
9220   *
9221   * @param string $url url to check
9222   * @return boolean true if we should bypass the proxy
9223   */
9224  function is_proxybypass( $url ) {
9225      global $CFG;
9226  
9227      // Sanity check.
9228      if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) {
9229          return false;
9230      }
9231  
9232      // Get the host part out of the url.
9233      if (!$host = parse_url( $url, PHP_URL_HOST )) {
9234          return false;
9235      }
9236  
9237      // Get the possible bypass hosts into an array.
9238      $matches = explode( ',', $CFG->proxybypass );
9239  
9240      // Check for a match.
9241      // (IPs need to match the left hand side and hosts the right of the url,
9242      // but we can recklessly check both as there can't be a false +ve).
9243      foreach ($matches as $match) {
9244          $match = trim($match);
9245  
9246          // Try for IP match (Left side).
9247          $lhs = substr($host, 0, strlen($match));
9248          if (strcasecmp($match, $lhs)==0) {
9249              return true;
9250          }
9251  
9252          // Try for host match (Right side).
9253          $rhs = substr($host, -strlen($match));
9254          if (strcasecmp($match, $rhs)==0) {
9255              return true;
9256          }
9257      }
9258  
9259      // Nothing matched.
9260      return false;
9261  }
9262  
9263  /**
9264   * Check if the passed navigation is of the new style
9265   *
9266   * @param mixed $navigation
9267   * @return bool true for yes false for no
9268   */
9269  function is_newnav($navigation) {
9270      if (is_array($navigation) && !empty($navigation['newnav'])) {
9271          return true;
9272      } else {
9273          return false;
9274      }
9275  }
9276  
9277  /**
9278   * Checks whether the given variable name is defined as a variable within the given object.
9279   *
9280   * This will NOT work with stdClass objects, which have no class variables.
9281   *
9282   * @param string $var The variable name
9283   * @param object $object The object to check
9284   * @return boolean
9285   */
9286  function in_object_vars($var, $object) {
9287      $classvars = get_class_vars(get_class($object));
9288      $classvars = array_keys($classvars);
9289      return in_array($var, $classvars);
9290  }
9291  
9292  /**
9293   * Returns an array without repeated objects.
9294   * This function is similar to array_unique, but for arrays that have objects as values
9295   *
9296   * @param array $array
9297   * @param bool $keepkeyassoc
9298   * @return array
9299   */
9300  function object_array_unique($array, $keepkeyassoc = true) {
9301      $duplicatekeys = array();
9302      $tmp         = array();
9303  
9304      foreach ($array as $key => $val) {
9305          // Convert objects to arrays, in_array() does not support objects.
9306          if (is_object($val)) {
9307              $val = (array)$val;
9308          }
9309  
9310          if (!in_array($val, $tmp)) {
9311              $tmp[] = $val;
9312          } else {
9313              $duplicatekeys[] = $key;
9314          }
9315      }
9316  
9317      foreach ($duplicatekeys as $key) {
9318          unset($array[$key]);
9319      }
9320  
9321      return $keepkeyassoc ? $array : array_values($array);
9322  }
9323  
9324  /**
9325   * Is a userid the primary administrator?
9326   *
9327   * @param int $userid int id of user to check
9328   * @return boolean
9329   */
9330  function is_primary_admin($userid) {
9331      $primaryadmin =  get_admin();
9332  
9333      if ($userid == $primaryadmin->id) {
9334          return true;
9335      } else {
9336          return false;
9337      }
9338  }
9339  
9340  /**
9341   * Returns the site identifier
9342   *
9343   * @return string $CFG->siteidentifier, first making sure it is properly initialised.
9344   */
9345  function get_site_identifier() {
9346      global $CFG;
9347      // Check to see if it is missing. If so, initialise it.
9348      if (empty($CFG->siteidentifier)) {
9349          set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']);
9350      }
9351      // Return it.
9352      return $CFG->siteidentifier;
9353  }
9354  
9355  /**
9356   * Check whether the given password has no more than the specified
9357   * number of consecutive identical characters.
9358   *
9359   * @param string $password   password to be checked against the password policy
9360   * @param integer $maxchars  maximum number of consecutive identical characters
9361   * @return bool
9362   */
9363  function check_consecutive_identical_characters($password, $maxchars) {
9364  
9365      if ($maxchars < 1) {
9366          return true; // Zero 0 is to disable this check.
9367      }
9368      if (strlen($password) <= $maxchars) {
9369          return true; // Too short to fail this test.
9370      }
9371  
9372      $previouschar = '';
9373      $consecutivecount = 1;
9374      foreach (str_split($password) as $char) {
9375          if ($char != $previouschar) {
9376              $consecutivecount = 1;
9377          } else {
9378              $consecutivecount++;
9379              if ($consecutivecount > $maxchars) {
9380                  return false; // Check failed already.
9381              }
9382          }
9383  
9384          $previouschar = $char;
9385      }
9386  
9387      return true;
9388  }
9389  
9390  /**
9391   * Helper function to do partial function binding.
9392   * so we can use it for preg_replace_callback, for example
9393   * this works with php functions, user functions, static methods and class methods
9394   * it returns you a callback that you can pass on like so:
9395   *
9396   * $callback = partial('somefunction', $arg1, $arg2);
9397   *     or
9398   * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
9399   *     or even
9400   * $obj = new someclass();
9401   * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2);
9402   *
9403   * and then the arguments that are passed through at calltime are appended to the argument list.
9404   *
9405   * @param mixed $function a php callback
9406   * @param mixed $arg1,... $argv arguments to partially bind with
9407   * @return array Array callback
9408   */
9409  function partial() {
9410      if (!class_exists('partial')) {
9411          /**
9412           * Used to manage function binding.
9413           * @copyright  2009 Penny Leach
9414           * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
9415           */
9416          class partial{
9417              /** @var array */
9418              public $values = array();
9419              /** @var string The function to call as a callback. */
9420              public $func;
9421              /**
9422               * Constructor
9423               * @param string $func
9424               * @param array $args
9425               */
9426              public function __construct($func, $args) {
9427                  $this->values = $args;
9428                  $this->func = $func;
9429              }
9430              /**
9431               * Calls the callback function.
9432               * @return mixed
9433               */
9434              public function method() {
9435                  $args = func_get_args();
9436                  return call_user_func_array($this->func, array_merge($this->values, $args));
9437              }
9438          }
9439      }
9440      $args = func_get_args();
9441      $func = array_shift($args);
9442      $p = new partial($func, $args);
9443      return array($p, 'method');
9444  }
9445  
9446  /**
9447   * helper function to load up and initialise the mnet environment
9448   * this must be called before you use mnet functions.
9449   *
9450   * @return mnet_environment the equivalent of old $MNET global
9451   */
9452  function get_mnet_environment() {
9453      global $CFG;
9454      require_once($CFG->dirroot . '/mnet/lib.php');
9455      static $instance = null;
9456      if (empty($instance)) {
9457          $instance = new mnet_environment();
9458          $instance->init();
9459      }
9460      return $instance;
9461  }
9462  
9463  /**
9464   * during xmlrpc server code execution, any code wishing to access
9465   * information about the remote peer must use this to get it.
9466   *
9467   * @return mnet_remote_client the equivalent of old $MNETREMOTE_CLIENT global
9468   */
9469  function get_mnet_remote_client() {
9470      if (!defined('MNET_SERVER')) {
9471          debugging(get_string('notinxmlrpcserver', 'mnet'));
9472          return false;
9473      }
9474      global $MNET_REMOTE_CLIENT;
9475      if (isset($MNET_REMOTE_CLIENT)) {
9476          return $MNET_REMOTE_CLIENT;
9477      }
9478      return false;
9479  }
9480  
9481  /**
9482   * during the xmlrpc server code execution, this will be called
9483   * to setup the object returned by {@link get_mnet_remote_client}
9484   *
9485   * @param mnet_remote_client $client the client to set up
9486   * @throws moodle_exception
9487   */
9488  function set_mnet_remote_client($client) {
9489      if (!defined('MNET_SERVER')) {
9490          throw new moodle_exception('notinxmlrpcserver', 'mnet');
9491      }
9492      global $MNET_REMOTE_CLIENT;
9493      $MNET_REMOTE_CLIENT = $client;
9494  }
9495  
9496  /**
9497   * return the jump url for a given remote user
9498   * this is used for rewriting forum post links in emails, etc
9499   *
9500   * @param stdclass $user the user to get the idp url for
9501   */
9502  function mnet_get_idp_jump_url($user) {
9503      global $CFG;
9504  
9505      static $mnetjumps = array();
9506      if (!array_key_exists($user->mnethostid, $mnetjumps)) {
9507          $idp = mnet_get_peer_host($user->mnethostid);
9508          $idpjumppath = mnet_get_app_jumppath($idp->applicationid);
9509          $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl=';
9510      }
9511      return $mnetjumps[$user->mnethostid];
9512  }
9513  
9514  /**
9515   * Gets the homepage to use for the current user
9516   *
9517   * @return int One of HOMEPAGE_*
9518   */
9519  function get_home_page() {
9520      global $CFG;
9521  
9522      if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) {
9523          if ($CFG->defaulthomepage == HOMEPAGE_MY) {
9524              return HOMEPAGE_MY;
9525          } else {
9526              return (int)get_user_preferences('user_home_page_preference', HOMEPAGE_MY);
9527          }
9528      }
9529      return HOMEPAGE_SITE;
9530  }
9531  
9532  /**
9533   * Gets the name of a course to be displayed when showing a list of courses.
9534   * By default this is just $course->fullname but user can configure it. The
9535   * result of this function should be passed through print_string.
9536   * @param stdClass|course_in_list $course Moodle course object
9537   * @return string Display name of course (either fullname or short + fullname)
9538   */
9539  function get_course_display_name_for_list($course) {
9540      global $CFG;
9541      if (!empty($CFG->courselistshortnames)) {
9542          if (!($course instanceof stdClass)) {
9543              $course = (object)convert_to_array($course);
9544          }
9545          return get_string('courseextendednamedisplay', '', $course);
9546      } else {
9547          return $course->fullname;
9548      }
9549  }
9550  
9551  /**
9552   * The lang_string class
9553   *
9554   * This special class is used to create an object representation of a string request.
9555   * It is special because processing doesn't occur until the object is first used.
9556   * The class was created especially to aid performance in areas where strings were
9557   * required to be generated but were not necessarily used.
9558   * As an example the admin tree when generated uses over 1500 strings, of which
9559   * normally only 1/3 are ever actually printed at any time.
9560   * The performance advantage is achieved by not actually processing strings that
9561   * arn't being used, as such reducing the processing required for the page.
9562   *
9563   * How to use the lang_string class?
9564   *     There are two methods of using the lang_string class, first through the
9565   *     forth argument of the get_string function, and secondly directly.
9566   *     The following are examples of both.
9567   * 1. Through get_string calls e.g.
9568   *     $string = get_string($identifier, $component, $a, true);
9569   *     $string = get_string('yes', 'moodle', null, true);
9570   * 2. Direct instantiation
9571   *     $string = new lang_string($identifier, $component, $a, $lang);
9572   *     $string = new lang_string('yes');
9573   *
9574   * How do I use a lang_string object?
9575   *     The lang_string object makes use of a magic __toString method so that you
9576   *     are able to use the object exactly as you would use a string in most cases.
9577   *     This means you are able to collect it into a variable and then directly
9578   *     echo it, or concatenate it into another string, or similar.
9579   *     The other thing you can do is manually get the string by calling the
9580   *     lang_strings out method e.g.
9581   *         $string = new lang_string('yes');
9582   *         $string->out();
9583   *     Also worth noting is that the out method can take one argument, $lang which
9584   *     allows the developer to change the language on the fly.
9585   *
9586   * When should I use a lang_string object?
9587   *     The lang_string object is designed to be used in any situation where a
9588   *     string may not be needed, but needs to be generated.
9589   *     The admin tree is a good example of where lang_string objects should be
9590   *     used.
9591   *     A more practical example would be any class that requries strings that may
9592   *     not be printed (after all classes get renderer by renderers and who knows
9593   *     what they will do ;))
9594   *
9595   * When should I not use a lang_string object?
9596   *     Don't use lang_strings when you are going to use a string immediately.
9597   *     There is no need as it will be processed immediately and there will be no
9598   *     advantage, and in fact perhaps a negative hit as a class has to be
9599   *     instantiated for a lang_string object, however get_string won't require
9600   *     that.
9601   *
9602   * Limitations:
9603   * 1. You cannot use a lang_string object as an array offset. Doing so will
9604   *     result in PHP throwing an error. (You can use it as an object property!)
9605   *
9606   * @package    core
9607   * @category   string
9608   * @copyright  2011 Sam Hemelryk
9609   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
9610   */
9611  class lang_string {
9612  
9613      /** @var string The strings identifier */
9614      protected $identifier;
9615      /** @var string The strings component. Default '' */
9616      protected $component = '';
9617      /** @var array|stdClass Any arguments required for the string. Default null */
9618      protected $a = null;
9619      /** @var string The language to use when processing the string. Default null */
9620      protected $lang = null;
9621  
9622      /** @var string The processed string (once processed) */
9623      protected $string = null;
9624  
9625      /**
9626       * A special boolean. If set to true then the object has been woken up and
9627       * cannot be regenerated. If this is set then $this->string MUST be used.
9628       * @var bool
9629       */
9630      protected $forcedstring = false;
9631  
9632      /**
9633       * Constructs a lang_string object
9634       *
9635       * This function should do as little processing as possible to ensure the best
9636       * performance for strings that won't be used.
9637       *
9638       * @param string $identifier The strings identifier
9639       * @param string $component The strings component
9640       * @param stdClass|array $a Any arguments the string requires
9641       * @param string $lang The language to use when processing the string.
9642       * @throws coding_exception
9643       */
9644      public function __construct($identifier, $component = '', $a = null, $lang = null) {
9645          if (empty($component)) {
9646              $component = 'moodle';
9647          }
9648  
9649          $this->identifier = $identifier;
9650          $this->component = $component;
9651          $this->lang = $lang;
9652  
9653          // We MUST duplicate $a to ensure that it if it changes by reference those
9654          // changes are not carried across.
9655          // To do this we always ensure $a or its properties/values are strings
9656          // and that any properties/values that arn't convertable are forgotten.
9657          if (!empty($a)) {
9658              if (is_scalar($a)) {
9659                  $this->a = $a;
9660              } else if ($a instanceof lang_string) {
9661                  $this->a = $a->out();
9662              } else if (is_object($a) or is_array($a)) {
9663                  $a = (array)$a;
9664                  $this->a = array();
9665                  foreach ($a as $key => $value) {
9666                      // Make sure conversion errors don't get displayed (results in '').
9667                      if (is_array($value)) {
9668                          $this->a[$key] = '';
9669                      } else if (is_object($value)) {
9670                          if (method_exists($value, '__toString')) {
9671                              $this->a[$key] = $value->__toString();
9672                          } else {
9673                              $this->a[$key] = '';
9674                          }
9675                      } else {
9676                          $this->a[$key] = (string)$value;
9677                      }
9678                  }
9679              }
9680          }
9681  
9682          if (debugging(false, DEBUG_DEVELOPER)) {
9683              if (clean_param($this->identifier, PARAM_STRINGID) == '') {
9684                  throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition');
9685              }
9686              if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') {
9687                  throw new coding_exception('Invalid string compontent. Please check your string definition');
9688              }
9689              if (!get_string_manager()->string_exists($this->identifier, $this->component)) {
9690                  debugging('String does not exist. Please check your string definition for '.$this->identifier.'/'.$this->component, DEBUG_DEVELOPER);
9691              }
9692          }
9693      }
9694  
9695      /**
9696       * Processes the string.
9697       *
9698       * This function actually processes the string, stores it in the string property
9699       * and then returns it.
9700       * You will notice that this function is VERY similar to the get_string method.
9701       * That is because it is pretty much doing the same thing.
9702       * However as this function is an upgrade it isn't as tolerant to backwards
9703       * compatibility.
9704       *
9705       * @return string
9706       * @throws coding_exception
9707       */
9708      protected function get_string() {
9709          global $CFG;
9710  
9711          // Check if we need to process the string.
9712          if ($this->string === null) {
9713              // Check the quality of the identifier.
9714              if ($CFG->debugdeveloper && clean_param($this->identifier, PARAM_STRINGID) === '') {
9715                  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);
9716              }
9717  
9718              // Process the string.
9719              $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang);
9720              // Debugging feature lets you display string identifier and component.
9721              if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
9722                  $this->string .= ' {' . $this->identifier . '/' . $this->component . '}';
9723              }
9724          }
9725          // Return the string.
9726          return $this->string;
9727      }
9728  
9729      /**
9730       * Returns the string
9731       *
9732       * @param string $lang The langauge to use when processing the string
9733       * @return string
9734       */
9735      public function out($lang = null) {
9736          if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) {
9737              if ($this->forcedstring) {
9738                  debugging('lang_string objects that have been used cannot be printed in another language. ('.$this->lang.' used)', DEBUG_DEVELOPER);
9739                  return $this->get_string();
9740              }
9741              $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang);
9742              return $translatedstring->out();
9743          }
9744          return $this->get_string();
9745      }
9746  
9747      /**
9748       * Magic __toString method for printing a string
9749       *
9750       * @return string
9751       */
9752      public function __toString() {
9753          return $this->get_string();
9754      }
9755  
9756      /**
9757       * Magic __set_state method used for var_export
9758       *
9759       * @return string
9760       */
9761      public function __set_state() {
9762          return $this->get_string();
9763      }
9764  
9765      /**
9766       * Prepares the lang_string for sleep and stores only the forcedstring and
9767       * string properties... the string cannot be regenerated so we need to ensure
9768       * it is generated for this.
9769       *
9770       * @return string
9771       */
9772      public function __sleep() {
9773          $this->get_string();
9774          $this->forcedstring = true;
9775          return array('forcedstring', 'string', 'lang');
9776      }
9777  }


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1