Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
  • /lib/ -> datalib.php (source)

    Differences Between: [Versions 310 and 311] [Versions 37 and 311] [Versions 38 and 311] [Versions 39 and 311]

       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   * Library of functions for database manipulation.
      19   *
      20   * Other main libraries:
      21   * - weblib.php - functions that produce web output
      22   * - moodlelib.php - general-purpose Moodle functions
      23   *
      24   * @package    core
      25   * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
      26   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
      27   */
      28  
      29  defined('MOODLE_INTERNAL') || die();
      30  
      31  /**
      32   * The maximum courses in a category
      33   * MAX_COURSES_IN_CATEGORY * MAX_COURSE_CATEGORIES must not be more than max integer!
      34   */
      35  define('MAX_COURSES_IN_CATEGORY', 10000);
      36  
      37  /**
      38    * The maximum number of course categories
      39    * MAX_COURSES_IN_CATEGORY * MAX_COURSE_CATEGORIES must not be more than max integer!
      40    */
      41  define('MAX_COURSE_CATEGORIES', 10000);
      42  
      43  /**
      44   * Number of seconds to wait before updating lastaccess information in DB.
      45   *
      46   * We allow overwrites from config.php, useful to ensure coherence in performance
      47   * tests results.
      48   *
      49   * Note: For web service requests in the external_tokens field, we use a different constant
      50   * webservice::TOKEN_LASTACCESS_UPDATE_SECS.
      51   */
      52  if (!defined('LASTACCESS_UPDATE_SECS')) {
      53      define('LASTACCESS_UPDATE_SECS', 60);
      54  }
      55  
      56  /**
      57   * Returns $user object of the main admin user
      58   *
      59   * @static stdClass $mainadmin
      60   * @return stdClass {@link $USER} record from DB, false if not found
      61   */
      62  function get_admin() {
      63      global $CFG, $DB;
      64  
      65      static $mainadmin = null;
      66      static $prevadmins = null;
      67  
      68      if (empty($CFG->siteadmins)) {
      69          // Should not happen on an ordinary site.
      70          // It does however happen during unit tests.
      71          return false;
      72      }
      73  
      74      if (isset($mainadmin) and $prevadmins === $CFG->siteadmins) {
      75          return clone($mainadmin);
      76      }
      77  
      78      $mainadmin = null;
      79  
      80      foreach (explode(',', $CFG->siteadmins) as $id) {
      81          if ($user = $DB->get_record('user', array('id'=>$id, 'deleted'=>0))) {
      82              $mainadmin = $user;
      83              break;
      84          }
      85      }
      86  
      87      if ($mainadmin) {
      88          $prevadmins = $CFG->siteadmins;
      89          return clone($mainadmin);
      90      } else {
      91          // this should not happen
      92          return false;
      93      }
      94  }
      95  
      96  /**
      97   * Returns list of all admins, using 1 DB query
      98   *
      99   * @return array
     100   */
     101  function get_admins() {
     102      global $DB, $CFG;
     103  
     104      if (empty($CFG->siteadmins)) {  // Should not happen on an ordinary site
     105          return array();
     106      }
     107  
     108      $sql = "SELECT u.*
     109                FROM {user} u
     110               WHERE u.deleted = 0 AND u.id IN ($CFG->siteadmins)";
     111  
     112      // We want the same order as in $CFG->siteadmins.
     113      $records = $DB->get_records_sql($sql);
     114      $admins = array();
     115      foreach (explode(',', $CFG->siteadmins) as $id) {
     116          $id = (int)$id;
     117          if (!isset($records[$id])) {
     118              // User does not exist, this should not happen.
     119              continue;
     120          }
     121          $admins[$records[$id]->id] = $records[$id];
     122      }
     123  
     124      return $admins;
     125  }
     126  
     127  /**
     128   * Search through course users
     129   *
     130   * If $coursid specifies the site course then this function searches
     131   * through all undeleted and confirmed users
     132   *
     133   * @global object
     134   * @uses SITEID
     135   * @uses SQL_PARAMS_NAMED
     136   * @uses CONTEXT_COURSE
     137   * @param int $courseid The course in question.
     138   * @param int $groupid The group in question.
     139   * @param string $searchtext The string to search for
     140   * @param string $sort A field to sort by
     141   * @param array $exceptions A list of IDs to ignore, eg 2,4,5,8,9,10
     142   * @return array
     143   */
     144  function search_users($courseid, $groupid, $searchtext, $sort='', array $exceptions=null) {
     145      global $DB;
     146  
     147      $fullname  = $DB->sql_fullname('u.firstname', 'u.lastname');
     148  
     149      if (!empty($exceptions)) {
     150          list($exceptions, $params) = $DB->get_in_or_equal($exceptions, SQL_PARAMS_NAMED, 'ex', false);
     151          $except = "AND u.id $exceptions";
     152      } else {
     153          $except = "";
     154          $params = array();
     155      }
     156  
     157      if (!empty($sort)) {
     158          $order = "ORDER BY $sort";
     159      } else {
     160          $order = "";
     161      }
     162  
     163      $select = "u.deleted = 0 AND u.confirmed = 1 AND (".$DB->sql_like($fullname, ':search1', false)." OR ".$DB->sql_like('u.email', ':search2', false).")";
     164      $params['search1'] = "%$searchtext%";
     165      $params['search2'] = "%$searchtext%";
     166  
     167      if (!$courseid or $courseid == SITEID) {
     168          $sql = "SELECT u.id, u.firstname, u.lastname, u.email
     169                    FROM {user} u
     170                   WHERE $select
     171                         $except
     172                  $order";
     173          return $DB->get_records_sql($sql, $params);
     174  
     175      } else {
     176          if ($groupid) {
     177              $sql = "SELECT u.id, u.firstname, u.lastname, u.email
     178                        FROM {user} u
     179                        JOIN {groups_members} gm ON gm.userid = u.id
     180                       WHERE $select AND gm.groupid = :groupid
     181                             $except
     182                       $order";
     183              $params['groupid'] = $groupid;
     184              return $DB->get_records_sql($sql, $params);
     185  
     186          } else {
     187              $context = context_course::instance($courseid);
     188  
     189              // We want to query both the current context and parent contexts.
     190              list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
     191  
     192              $sql = "SELECT u.id, u.firstname, u.lastname, u.email
     193                        FROM {user} u
     194                        JOIN {role_assignments} ra ON ra.userid = u.id
     195                       WHERE $select AND ra.contextid $relatedctxsql
     196                             $except
     197                      $order";
     198              $params = array_merge($params, $relatedctxparams);
     199              return $DB->get_records_sql($sql, $params);
     200          }
     201      }
     202  }
     203  
     204  /**
     205   * Returns SQL used to search through user table to find users (in a query
     206   * which may also join and apply other conditions).
     207   *
     208   * You can combine this SQL with an existing query by adding 'AND $sql' to the
     209   * WHERE clause of your query (where $sql is the first element in the array
     210   * returned by this function), and merging in the $params array to the parameters
     211   * of your query (where $params is the second element). Your query should use
     212   * named parameters such as :param, rather than the question mark style.
     213   *
     214   * There are examples of basic usage in the unit test for this function.
     215   *
     216   * @param string $search the text to search for (empty string = find all)
     217   * @param string $u the table alias for the user table in the query being
     218   *     built. May be ''.
     219   * @param bool $searchanywhere If true (default), searches in the middle of
     220   *     names, otherwise only searches at start
     221   * @param array $extrafields Array of extra user fields to include in search, must be prefixed with table alias if they are not in
     222   *     the user table.
     223   * @param array $exclude Array of user ids to exclude (empty = don't exclude)
     224   * @param array $includeonly If specified, only returns users that have ids
     225   *     incldued in this array (empty = don't restrict)
     226   * @return array an array with two elements, a fragment of SQL to go in the
     227   *     where clause the query, and an associative array containing any required
     228   *     parameters (using named placeholders).
     229   */
     230  function users_search_sql(string $search, string $u = 'u', bool $searchanywhere = true, array $extrafields = [],
     231          array $exclude = null, array $includeonly = null): array {
     232      global $DB, $CFG;
     233      $params = array();
     234      $tests = array();
     235  
     236      if ($u) {
     237          $u .= '.';
     238      }
     239  
     240      // If we have a $search string, put a field LIKE '$search%' condition on each field.
     241      if ($search) {
     242          $conditions = array(
     243              $DB->sql_fullname($u . 'firstname', $u . 'lastname'),
     244              $conditions[] = $u . 'lastname'
     245          );
     246          foreach ($extrafields as $field) {
     247              // Add the table alias for the user table if the field doesn't already have an alias.
     248              $conditions[] = strpos($field, '.') !== false ? $field : $u . $field;
     249          }
     250          if ($searchanywhere) {
     251              $searchparam = '%' . $search . '%';
     252          } else {
     253              $searchparam = $search . '%';
     254          }
     255          $i = 0;
     256          foreach ($conditions as $key => $condition) {
     257              $conditions[$key] = $DB->sql_like($condition, ":con{$i}00", false, false);
     258              $params["con{$i}00"] = $searchparam;
     259              $i++;
     260          }
     261          $tests[] = '(' . implode(' OR ', $conditions) . ')';
     262      }
     263  
     264      // Add some additional sensible conditions.
     265      $tests[] = $u . "id <> :guestid";
     266      $params['guestid'] = $CFG->siteguest;
     267      $tests[] = $u . 'deleted = 0';
     268      $tests[] = $u . 'confirmed = 1';
     269  
     270      // If we are being asked to exclude any users, do that.
     271      if (!empty($exclude)) {
     272          list($usertest, $userparams) = $DB->get_in_or_equal($exclude, SQL_PARAMS_NAMED, 'ex', false);
     273          $tests[] = $u . 'id ' . $usertest;
     274          $params = array_merge($params, $userparams);
     275      }
     276  
     277      // If we are validating a set list of userids, add an id IN (...) test.
     278      if (!empty($includeonly)) {
     279          list($usertest, $userparams) = $DB->get_in_or_equal($includeonly, SQL_PARAMS_NAMED, 'val');
     280          $tests[] = $u . 'id ' . $usertest;
     281          $params = array_merge($params, $userparams);
     282      }
     283  
     284      // In case there are no tests, add one result (this makes it easier to combine
     285      // this with an existing query as you can always add AND $sql).
     286      if (empty($tests)) {
     287          $tests[] = '1 = 1';
     288      }
     289  
     290      // Combing the conditions and return.
     291      return array(implode(' AND ', $tests), $params);
     292  }
     293  
     294  
     295  /**
     296   * This function generates the standard ORDER BY clause for use when generating
     297   * lists of users. If you don't have a reason to use a different order, then
     298   * you should use this method to generate the order when displaying lists of users.
     299   *
     300   * If the optional $search parameter is passed, then exact matches to the search
     301   * will be sorted first. For example, suppose you have two users 'Al Zebra' and
     302   * 'Alan Aardvark'. The default sort is Alan, then Al. If, however, you search for
     303   * 'Al', then Al will be listed first. (With two users, this is not a big deal,
     304   * but with thousands of users, it is essential.)
     305   *
     306   * The list of fields scanned for exact matches are:
     307   *  - firstname
     308   *  - lastname
     309   *  - $DB->sql_fullname
     310   *  - those returned by \core_user\fields::get_identity_fields or those included in $customfieldmappings
     311   *
     312   * If named parameters are used (which is the default, and highly recommended),
     313   * then the parameter names are like :usersortexactN, where N is an int.
     314   *
     315   * The simplest possible example use is:
     316   * list($sort, $params) = users_order_by_sql();
     317   * $sql = 'SELECT * FROM {users} ORDER BY ' . $sort;
     318   *
     319   * A more complex example, showing that this sort can be combined with other sorts:
     320   * list($sort, $sortparams) = users_order_by_sql('u');
     321   * $sql = "SELECT g.id AS groupid, gg.groupingid, u.id AS userid, u.firstname, u.lastname, u.idnumber, u.username
     322   *           FROM {groups} g
     323   *      LEFT JOIN {groupings_groups} gg ON g.id = gg.groupid
     324   *      LEFT JOIN {groups_members} gm ON g.id = gm.groupid
     325   *      LEFT JOIN {user} u ON gm.userid = u.id
     326   *          WHERE g.courseid = :courseid $groupwhere $groupingwhere
     327   *       ORDER BY g.name, $sort";
     328   * $params += $sortparams;
     329   *
     330   * An example showing the use of $search:
     331   * list($sort, $sortparams) = users_order_by_sql('u', $search, $this->get_context());
     332   * $order = ' ORDER BY ' . $sort;
     333   * $params += $sortparams;
     334   * $availableusers = $DB->get_records_sql($fields . $sql . $order, $params, $page*$perpage, $perpage);
     335   *
     336   * @param string $usertablealias (optional) any table prefix for the {users} table. E.g. 'u'.
     337   * @param string $search (optional) a current search string. If given,
     338   *      any exact matches to this string will be sorted first.
     339   * @param context|null $context the context we are in. Used by \core_user\fields::get_identity_fields.
     340   *      Defaults to $PAGE->context.
     341   * @param array $customfieldmappings associative array of mappings for custom fields returned by \core_user\fields::get_sql.
     342   * @return array with two elements:
     343   *      string SQL fragment to use in the ORDER BY clause. For example, "firstname, lastname".
     344   *      array of parameters used in the SQL fragment. If $search is not given, this is guaranteed to be an empty array.
     345   */
     346  function users_order_by_sql(string $usertablealias = '', string $search = null, context $context = null,
     347          array $customfieldmappings = []) {
     348      global $DB, $PAGE;
     349  
     350      if ($usertablealias) {
     351          $tableprefix = $usertablealias . '.';
     352      } else {
     353          $tableprefix = '';
     354      }
     355  
     356      $sort = "{$tableprefix}lastname, {$tableprefix}firstname, {$tableprefix}id";
     357      $params = array();
     358  
     359      if (!$search) {
     360          return array($sort, $params);
     361      }
     362  
     363      if (!$context) {
     364          $context = $PAGE->context;
     365      }
     366  
     367      $exactconditions = array();
     368      $paramkey = 'usersortexact1';
     369  
     370      $exactconditions[] = $DB->sql_fullname($tableprefix . 'firstname', $tableprefix  . 'lastname') .
     371              ' = :' . $paramkey;
     372      $params[$paramkey] = $search;
     373      $paramkey++;
     374  
     375      if ($customfieldmappings) {
     376          $fieldstocheck = array_merge([$tableprefix . 'firstname', $tableprefix . 'lastname'], array_values($customfieldmappings));
     377      } else {
     378          $fieldstocheck = array_merge(['firstname', 'lastname'], \core_user\fields::get_identity_fields($context, false));
     379          $fieldstocheck = array_map(function($field) use ($tableprefix) {
     380              return $tableprefix . $field;
     381          }, $fieldstocheck);
     382      }
     383  
     384      foreach ($fieldstocheck as $key => $field) {
     385          $exactconditions[] = 'LOWER(' . $field . ') = LOWER(:' . $paramkey . ')';
     386          $params[$paramkey] = $search;
     387          $paramkey++;
     388      }
     389  
     390      $sort = 'CASE WHEN ' . implode(' OR ', $exactconditions) .
     391              ' THEN 0 ELSE 1 END, ' . $sort;
     392  
     393      return array($sort, $params);
     394  }
     395  
     396  /**
     397   * Returns a subset of users
     398   *
     399   * @global object
     400   * @uses DEBUG_DEVELOPER
     401   * @uses SQL_PARAMS_NAMED
     402   * @param bool $get If false then only a count of the records is returned
     403   * @param string $search A simple string to search for
     404   * @param bool $confirmed A switch to allow/disallow unconfirmed users
     405   * @param array $exceptions A list of IDs to ignore, eg 2,4,5,8,9,10
     406   * @param string $sort A SQL snippet for the sorting criteria to use
     407   * @param string $firstinitial Users whose first name starts with $firstinitial
     408   * @param string $lastinitial Users whose last name starts with $lastinitial
     409   * @param string $page The page or records to return
     410   * @param string $recordsperpage The number of records to return per page
     411   * @param string $fields A comma separated list of fields to be returned from the chosen table.
     412   * @return array|int|bool  {@link $USER} records unless get is false in which case the integer count of the records found is returned.
     413   *                        False is returned if an error is encountered.
     414   */
     415  function get_users($get=true, $search='', $confirmed=false, array $exceptions=null, $sort='firstname ASC',
     416                     $firstinitial='', $lastinitial='', $page='', $recordsperpage='', $fields='*', $extraselect='', array $extraparams=null) {
     417      global $DB, $CFG;
     418  
     419      if ($get && !$recordsperpage) {
     420          debugging('Call to get_users with $get = true no $recordsperpage limit. ' .
     421                  'On large installations, this will probably cause an out of memory error. ' .
     422                  'Please think again and change your code so that it does not try to ' .
     423                  'load so much data into memory.', DEBUG_DEVELOPER);
     424      }
     425  
     426      $fullname  = $DB->sql_fullname();
     427  
     428      $select = " id <> :guestid AND deleted = 0";
     429      $params = array('guestid'=>$CFG->siteguest);
     430  
     431      if (!empty($search)){
     432          $search = trim($search);
     433          $select .= " AND (".$DB->sql_like($fullname, ':search1', false)." OR ".$DB->sql_like('email', ':search2', false)." OR username = :search3)";
     434          $params['search1'] = "%$search%";
     435          $params['search2'] = "%$search%";
     436          $params['search3'] = "$search";
     437      }
     438  
     439      if ($confirmed) {
     440          $select .= " AND confirmed = 1";
     441      }
     442  
     443      if ($exceptions) {
     444          list($exceptions, $eparams) = $DB->get_in_or_equal($exceptions, SQL_PARAMS_NAMED, 'ex', false);
     445          $params = $params + $eparams;
     446          $select .= " AND id $exceptions";
     447      }
     448  
     449      if ($firstinitial) {
     450          $select .= " AND ".$DB->sql_like('firstname', ':fni', false, false);
     451          $params['fni'] = "$firstinitial%";
     452      }
     453      if ($lastinitial) {
     454          $select .= " AND ".$DB->sql_like('lastname', ':lni', false, false);
     455          $params['lni'] = "$lastinitial%";
     456      }
     457  
     458      if ($extraselect) {
     459          $select .= " AND $extraselect";
     460          $params = $params + (array)$extraparams;
     461      }
     462  
     463      if ($get) {
     464          return $DB->get_records_select('user', $select, $params, $sort, $fields, $page, $recordsperpage);
     465      } else {
     466          return $DB->count_records_select('user', $select, $params);
     467      }
     468  }
     469  
     470  
     471  /**
     472   * Return filtered (if provided) list of users in site, except guest and deleted users.
     473   *
     474   * @param string $sort An SQL field to sort by
     475   * @param string $dir The sort direction ASC|DESC
     476   * @param int $page The page or records to return
     477   * @param int $recordsperpage The number of records to return per page
     478   * @param string $search A simple string to search for
     479   * @param string $firstinitial Users whose first name starts with $firstinitial
     480   * @param string $lastinitial Users whose last name starts with $lastinitial
     481   * @param string $extraselect An additional SQL select statement to append to the query
     482   * @param array $extraparams Additional parameters to use for the above $extraselect
     483   * @param stdClass $extracontext If specified, will include user 'extra fields'
     484   *   as appropriate for current user and given context
     485   * @return array Array of {@link $USER} records
     486   */
     487  function get_users_listing($sort='lastaccess', $dir='ASC', $page=0, $recordsperpage=0,
     488                             $search='', $firstinitial='', $lastinitial='', $extraselect='',
     489                             array $extraparams=null, $extracontext = null) {
     490      global $DB, $CFG;
     491  
     492      $fullname  = $DB->sql_fullname();
     493  
     494      $select = "deleted <> 1 AND u.id <> :guestid";
     495      $params = array('guestid' => $CFG->siteguest);
     496  
     497      if (!empty($search)) {
     498          $search = trim($search);
     499          $select .= " AND (". $DB->sql_like($fullname, ':search1', false, false).
     500                     " OR ". $DB->sql_like('email', ':search2', false, false).
     501                     " OR username = :search3)";
     502          $params['search1'] = "%$search%";
     503          $params['search2'] = "%$search%";
     504          $params['search3'] = "$search";
     505      }
     506  
     507      if ($firstinitial) {
     508          $select .= " AND ". $DB->sql_like('firstname', ':fni', false, false);
     509          $params['fni'] = "$firstinitial%";
     510      }
     511      if ($lastinitial) {
     512          $select .= " AND ". $DB->sql_like('lastname', ':lni', false, false);
     513          $params['lni'] = "$lastinitial%";
     514      }
     515  
     516      if ($extraselect) {
     517          // The extra WHERE clause may refer to the 'id' column which can now be ambiguous because we
     518          // changed the query to include joins, so replace any 'id' that is on its own (no alias)
     519          // with 'u.id'.
     520          $extraselect = preg_replace('~([ =]|^)id([ =]|$)~', '$1u.id$2', $extraselect);
     521          $select .= " AND $extraselect";
     522          $params = $params + (array)$extraparams;
     523      }
     524  
     525      if ($sort) {
     526          $sort = " ORDER BY $sort $dir";
     527      }
     528  
     529      // If a context is specified, get extra user fields that the current user
     530      // is supposed to see, otherwise just get the name fields.
     531      $userfields = \core_user\fields::for_name();
     532      if ($extracontext) {
     533          $userfields->with_identity($extracontext, true);
     534      }
     535      $userfields->excluding('id', 'username', 'email', 'city', 'country', 'lastaccess', 'confirmed', 'mnethostid');
     536      ['selects' => $selects, 'joins' => $joins, 'params' => $joinparams] =
     537              (array)$userfields->get_sql('u', true);
     538  
     539      // warning: will return UNCONFIRMED USERS
     540      return $DB->get_records_sql("SELECT u.id, username, email, city, country, lastaccess, confirmed, mnethostid, suspended $selects
     541                                     FROM {user} u
     542                                          $joins
     543                                    WHERE $select
     544                                    $sort", array_merge($params, $joinparams), $page, $recordsperpage);
     545  
     546  }
     547  
     548  
     549  /**
     550   * Full list of users that have confirmed their accounts.
     551   *
     552   * @global object
     553   * @return array of unconfirmed users
     554   */
     555  function get_users_confirmed() {
     556      global $DB, $CFG;
     557      return $DB->get_records_sql("SELECT *
     558                                     FROM {user}
     559                                    WHERE confirmed = 1 AND deleted = 0 AND id <> ?", array($CFG->siteguest));
     560  }
     561  
     562  
     563  /// OTHER SITE AND COURSE FUNCTIONS /////////////////////////////////////////////
     564  
     565  
     566  /**
     567   * Returns $course object of the top-level site.
     568   *
     569   * @return object A {@link $COURSE} object for the site, exception if not found
     570   */
     571  function get_site() {
     572      global $SITE, $DB;
     573  
     574      if (!empty($SITE->id)) {   // We already have a global to use, so return that
     575          return $SITE;
     576      }
     577  
     578      if ($course = $DB->get_record('course', array('category'=>0))) {
     579          return $course;
     580      } else {
     581          // course table exists, but the site is not there,
     582          // unfortunately there is no automatic way to recover
     583          throw new moodle_exception('nosite', 'error');
     584      }
     585  }
     586  
     587  /**
     588   * Gets a course object from database. If the course id corresponds to an
     589   * already-loaded $COURSE or $SITE object, then the loaded object will be used,
     590   * saving a database query.
     591   *
     592   * If it reuses an existing object, by default the object will be cloned. This
     593   * means you can modify the object safely without affecting other code.
     594   *
     595   * @param int $courseid Course id
     596   * @param bool $clone If true (default), makes a clone of the record
     597   * @return stdClass A course object
     598   * @throws dml_exception If not found in database
     599   */
     600  function get_course($courseid, $clone = true) {
     601      global $DB, $COURSE, $SITE;
     602      if (!empty($COURSE->id) && $COURSE->id == $courseid) {
     603          return $clone ? clone($COURSE) : $COURSE;
     604      } else if (!empty($SITE->id) && $SITE->id == $courseid) {
     605          return $clone ? clone($SITE) : $SITE;
     606      } else {
     607          return $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
     608      }
     609  }
     610  
     611  /**
     612   * Returns list of courses, for whole site, or category
     613   *
     614   * Returns list of courses, for whole site, or category
     615   * Important: Using c.* for fields is extremely expensive because
     616   *            we are using distinct. You almost _NEVER_ need all the fields
     617   *            in such a large SELECT
     618   *
     619   * Consider using core_course_category::get_courses()
     620   * or core_course_category::search_courses() instead since they use caching.
     621   *
     622   * @global object
     623   * @global object
     624   * @global object
     625   * @uses CONTEXT_COURSE
     626   * @param string|int $categoryid Either a category id or 'all' for everything
     627   * @param string $sort A field and direction to sort by
     628   * @param string $fields The additional fields to return (note that "id, category, visible" are always present)
     629   * @return array Array of courses
     630   */
     631  function get_courses($categoryid="all", $sort="c.sortorder ASC", $fields="c.*") {
     632  
     633      global $USER, $CFG, $DB;
     634  
     635      $params = array();
     636  
     637      if ($categoryid !== "all" && is_numeric($categoryid)) {
     638          $categoryselect = "WHERE c.category = :catid";
     639          $params['catid'] = $categoryid;
     640      } else {
     641          $categoryselect = "";
     642      }
     643  
     644      if (empty($sort)) {
     645          $sortstatement = "";
     646      } else {
     647          $sortstatement = "ORDER BY $sort";
     648      }
     649  
     650      $visiblecourses = array();
     651  
     652      $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
     653      $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
     654      $params['contextlevel'] = CONTEXT_COURSE;
     655  
     656      // The fields "id, category, visible" are required in the subsequent loop and must always be present.
     657      if ($fields !== 'c.*') {
     658          $fieldarray = array_merge(
     659              // Split fields on comma + zero or more whitespace, merge with required fields.
     660              preg_split('/,\s*/', $fields), [
     661                  'c.id',
     662                  'c.category',
     663                  'c.visible',
     664              ]
     665          );
     666          $fields = implode(',', array_unique($fieldarray));
     667      }
     668  
     669      $sql = "SELECT $fields $ccselect
     670                FROM {course} c
     671             $ccjoin
     672                $categoryselect
     673                $sortstatement";
     674  
     675      // pull out all course matching the cat
     676      if ($courses = $DB->get_records_sql($sql, $params)) {
     677  
     678          // loop throught them
     679          foreach ($courses as $course) {
     680              context_helper::preload_from_record($course);
     681              if (core_course_category::can_view_course_info($course)) {
     682                  $visiblecourses [$course->id] = $course;
     683              }
     684          }
     685      }
     686      return $visiblecourses;
     687  }
     688  
     689  /**
     690   * A list of courses that match a search
     691   *
     692   * @global object
     693   * @global object
     694   * @param array $searchterms An array of search criteria
     695   * @param string $sort A field and direction to sort by
     696   * @param int $page The page number to get
     697   * @param int $recordsperpage The number of records per page
     698   * @param int $totalcount Passed in by reference.
     699   * @param array $requiredcapabilities Extra list of capabilities used to filter courses
     700   * @param array $searchcond additional search conditions, for example ['c.enablecompletion = :p1']
     701   * @param array $params named parameters for additional search conditions, for example ['p1' => 1]
     702   * @return stdClass[] {@link $COURSE} records
     703   */
     704  function get_courses_search($searchterms, $sort, $page, $recordsperpage, &$totalcount,
     705                              $requiredcapabilities = array(), $searchcond = [], $params = []) {
     706      global $CFG, $DB;
     707  
     708      if ($DB->sql_regex_supported()) {
     709          $REGEXP    = $DB->sql_regex(true);
     710          $NOTREGEXP = $DB->sql_regex(false);
     711      }
     712  
     713      $i = 0;
     714  
     715      // Thanks Oracle for your non-ansi concat and type limits in coalesce. MDL-29912
     716      if ($DB->get_dbfamily() == 'oracle') {
     717          $concat = "(c.summary|| ' ' || c.fullname || ' ' || c.idnumber || ' ' || c.shortname)";
     718      } else {
     719          $concat = $DB->sql_concat("COALESCE(c.summary, '')", "' '", 'c.fullname', "' '", 'c.idnumber', "' '", 'c.shortname');
     720      }
     721  
     722      foreach ($searchterms as $searchterm) {
     723          $i++;
     724  
     725          $NOT = false; /// Initially we aren't going to perform NOT LIKE searches, only MSSQL and Oracle
     726                     /// will use it to simulate the "-" operator with LIKE clause
     727  
     728      /// Under Oracle and MSSQL, trim the + and - operators and perform
     729      /// simpler LIKE (or NOT LIKE) queries
     730          if (!$DB->sql_regex_supported()) {
     731              if (substr($searchterm, 0, 1) == '-') {
     732                  $NOT = true;
     733              }
     734              $searchterm = trim($searchterm, '+-');
     735          }
     736  
     737          // TODO: +- may not work for non latin languages
     738  
     739          if (substr($searchterm,0,1) == '+') {
     740              $searchterm = trim($searchterm, '+-');
     741              $searchterm = preg_quote($searchterm, '|');
     742              $searchcond[] = "$concat $REGEXP :ss$i";
     743              $params['ss'.$i] = "(^|[^a-zA-Z0-9])$searchterm([^a-zA-Z0-9]|$)";
     744  
     745          } else if ((substr($searchterm,0,1) == "-") && (core_text::strlen($searchterm) > 1)) {
     746              $searchterm = trim($searchterm, '+-');
     747              $searchterm = preg_quote($searchterm, '|');
     748              $searchcond[] = "$concat $NOTREGEXP :ss$i";
     749              $params['ss'.$i] = "(^|[^a-zA-Z0-9])$searchterm([^a-zA-Z0-9]|$)";
     750  
     751          } else {
     752              $searchcond[] = $DB->sql_like($concat,":ss$i", false, true, $NOT);
     753              $params['ss'.$i] = "%$searchterm%";
     754          }
     755      }
     756  
     757      if (empty($searchcond)) {
     758          $searchcond = array('1 = 1');
     759      }
     760  
     761      $searchcond = implode(" AND ", $searchcond);
     762  
     763      $courses = array();
     764      $c = 0; // counts how many visible courses we've seen
     765  
     766      // Tiki pagination
     767      $limitfrom = $page * $recordsperpage;
     768      $limitto   = $limitfrom + $recordsperpage;
     769  
     770      $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
     771      $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
     772      $params['contextlevel'] = CONTEXT_COURSE;
     773  
     774      $sql = "SELECT c.* $ccselect
     775                FROM {course} c
     776             $ccjoin
     777               WHERE $searchcond AND c.id <> ".SITEID."
     778            ORDER BY $sort";
     779  
     780      $mycourses = enrol_get_my_courses();
     781      $rs = $DB->get_recordset_sql($sql, $params);
     782      foreach($rs as $course) {
     783          // Preload contexts only for hidden courses or courses we need to return.
     784          context_helper::preload_from_record($course);
     785          $coursecontext = context_course::instance($course->id);
     786          if (!array_key_exists($course->id, $mycourses) && !core_course_category::can_view_course_info($course)) {
     787              continue;
     788          }
     789          if (!empty($requiredcapabilities)) {
     790              if (!has_all_capabilities($requiredcapabilities, $coursecontext)) {
     791                  continue;
     792              }
     793          }
     794          // Don't exit this loop till the end
     795          // we need to count all the visible courses
     796          // to update $totalcount
     797          if ($c >= $limitfrom && $c < $limitto) {
     798              $courses[$course->id] = $course;
     799          }
     800          $c++;
     801      }
     802      $rs->close();
     803  
     804      // our caller expects 2 bits of data - our return
     805      // array, and an updated $totalcount
     806      $totalcount = $c;
     807      return $courses;
     808  }
     809  
     810  /**
     811   * Fixes course category and course sortorder, also verifies category and course parents and paths.
     812   * (circular references are not fixed)
     813   *
     814   * @global object
     815   * @global object
     816   * @uses MAX_COURSE_CATEGORIES
     817   * @uses SITEID
     818   * @uses CONTEXT_COURSE
     819   * @return void
     820   */
     821  function fix_course_sortorder() {
     822      global $DB, $SITE;
     823  
     824      //WARNING: this is PHP5 only code!
     825  
     826      // if there are any changes made to courses or categories we will trigger
     827      // the cache events to purge all cached courses/categories data
     828      $cacheevents = array();
     829  
     830      if ($unsorted = $DB->get_records('course_categories', array('sortorder'=>0))) {
     831          //move all categories that are not sorted yet to the end
     832          $DB->set_field('course_categories', 'sortorder',
     833              get_max_courses_in_category() * MAX_COURSE_CATEGORIES, array('sortorder' => 0));
     834          $cacheevents['changesincoursecat'] = true;
     835      }
     836  
     837      $allcats = $DB->get_records('course_categories', null, 'sortorder, id', 'id, sortorder, parent, depth, path');
     838      $topcats    = array();
     839      $brokencats = array();
     840      foreach ($allcats as $cat) {
     841          $sortorder = (int)$cat->sortorder;
     842          if (!$cat->parent) {
     843              while(isset($topcats[$sortorder])) {
     844                  $sortorder++;
     845              }
     846              $topcats[$sortorder] = $cat;
     847              continue;
     848          }
     849          if (!isset($allcats[$cat->parent])) {
     850              $brokencats[] = $cat;
     851              continue;
     852          }
     853          if (!isset($allcats[$cat->parent]->children)) {
     854              $allcats[$cat->parent]->children = array();
     855          }
     856          while(isset($allcats[$cat->parent]->children[$sortorder])) {
     857              $sortorder++;
     858          }
     859          $allcats[$cat->parent]->children[$sortorder] = $cat;
     860      }
     861      unset($allcats);
     862  
     863      // add broken cats to category tree
     864      if ($brokencats) {
     865          $defaultcat = reset($topcats);
     866          foreach ($brokencats as $cat) {
     867              $topcats[] = $cat;
     868          }
     869      }
     870  
     871      // now walk recursively the tree and fix any problems found
     872      $sortorder = 0;
     873      $fixcontexts = array();
     874      if (_fix_course_cats($topcats, $sortorder, 0, 0, '', $fixcontexts)) {
     875          $cacheevents['changesincoursecat'] = true;
     876      }
     877  
     878      // detect if there are "multiple" frontpage courses and fix them if needed
     879      $frontcourses = $DB->get_records('course', array('category'=>0), 'id');
     880      if (count($frontcourses) > 1) {
     881          if (isset($frontcourses[SITEID])) {
     882              $frontcourse = $frontcourses[SITEID];
     883              unset($frontcourses[SITEID]);
     884          } else {
     885              $frontcourse = array_shift($frontcourses);
     886          }
     887          $defaultcat = reset($topcats);
     888          foreach ($frontcourses as $course) {
     889              $DB->set_field('course', 'category', $defaultcat->id, array('id'=>$course->id));
     890              $context = context_course::instance($course->id);
     891              $fixcontexts[$context->id] = $context;
     892              $cacheevents['changesincourse'] = true;
     893          }
     894          unset($frontcourses);
     895      } else {
     896          $frontcourse = reset($frontcourses);
     897      }
     898  
     899      // now fix the paths and depths in context table if needed
     900      if ($fixcontexts) {
     901          foreach ($fixcontexts as $fixcontext) {
     902              $fixcontext->reset_paths(false);
     903          }
     904          context_helper::build_all_paths(false);
     905          unset($fixcontexts);
     906          $cacheevents['changesincourse'] = true;
     907          $cacheevents['changesincoursecat'] = true;
     908      }
     909  
     910      // release memory
     911      unset($topcats);
     912      unset($brokencats);
     913      unset($fixcontexts);
     914  
     915      // fix frontpage course sortorder
     916      if ($frontcourse->sortorder != 1) {
     917          $DB->set_field('course', 'sortorder', 1, array('id'=>$frontcourse->id));
     918          $cacheevents['changesincourse'] = true;
     919      }
     920  
     921      // now fix the course counts in category records if needed
     922      $sql = "SELECT cc.id, cc.coursecount, COUNT(c.id) AS newcount
     923                FROM {course_categories} cc
     924                LEFT JOIN {course} c ON c.category = cc.id
     925            GROUP BY cc.id, cc.coursecount
     926              HAVING cc.coursecount <> COUNT(c.id)";
     927  
     928      if ($updatecounts = $DB->get_records_sql($sql)) {
     929          // categories with more courses than MAX_COURSES_IN_CATEGORY
     930          $categories = array();
     931          foreach ($updatecounts as $cat) {
     932              $cat->coursecount = $cat->newcount;
     933              if ($cat->coursecount >= get_max_courses_in_category()) {
     934                  $categories[] = $cat->id;
     935              }
     936              unset($cat->newcount);
     937              $DB->update_record_raw('course_categories', $cat, true);
     938          }
     939          if (!empty($categories)) {
     940              $str = implode(', ', $categories);
     941              debugging("The number of courses (category id: $str) has reached max number of courses " .
     942                  "in a category (" . get_max_courses_in_category() . "). It will cause a sorting performance issue. " .
     943                  "Please set higher value for \$CFG->maxcoursesincategory in config.php. " .
     944                  "Please also make sure \$CFG->maxcoursesincategory * MAX_COURSE_CATEGORIES less than max integer. " .
     945                  "See tracker issues: MDL-25669 and MDL-69573", DEBUG_DEVELOPER);
     946          }
     947          $cacheevents['changesincoursecat'] = true;
     948      }
     949  
     950      // now make sure that sortorders in course table are withing the category sortorder ranges
     951      $sql = "SELECT DISTINCT cc.id, cc.sortorder
     952                FROM {course_categories} cc
     953                JOIN {course} c ON c.category = cc.id
     954               WHERE c.sortorder < cc.sortorder OR c.sortorder > cc.sortorder + " . get_max_courses_in_category();
     955  
     956      if ($fixcategories = $DB->get_records_sql($sql)) {
     957          //fix the course sortorder ranges
     958          foreach ($fixcategories as $cat) {
     959              $sql = "UPDATE {course}
     960                         SET sortorder = ".$DB->sql_modulo('sortorder', get_max_courses_in_category())." + ?
     961                       WHERE category = ?";
     962              $DB->execute($sql, array($cat->sortorder, $cat->id));
     963          }
     964          $cacheevents['changesincoursecat'] = true;
     965      }
     966      unset($fixcategories);
     967  
     968      // categories having courses with sortorder duplicates or having gaps in sortorder
     969      $sql = "SELECT DISTINCT c1.category AS id , cc.sortorder
     970                FROM {course} c1
     971                JOIN {course} c2 ON c1.sortorder = c2.sortorder
     972                JOIN {course_categories} cc ON (c1.category = cc.id)
     973               WHERE c1.id <> c2.id";
     974      $fixcategories = $DB->get_records_sql($sql);
     975  
     976      $sql = "SELECT cc.id, cc.sortorder, cc.coursecount, MAX(c.sortorder) AS maxsort, MIN(c.sortorder) AS minsort
     977                FROM {course_categories} cc
     978                JOIN {course} c ON c.category = cc.id
     979            GROUP BY cc.id, cc.sortorder, cc.coursecount
     980              HAVING (MAX(c.sortorder) <>  cc.sortorder + cc.coursecount) OR (MIN(c.sortorder) <>  cc.sortorder + 1)";
     981      $gapcategories = $DB->get_records_sql($sql);
     982  
     983      foreach ($gapcategories as $cat) {
     984          if (isset($fixcategories[$cat->id])) {
     985              // duplicates detected already
     986  
     987          } else if ($cat->minsort == $cat->sortorder and $cat->maxsort == $cat->sortorder + $cat->coursecount - 1) {
     988              // easy - new course inserted with sortorder 0, the rest is ok
     989              $sql = "UPDATE {course}
     990                         SET sortorder = sortorder + 1
     991                       WHERE category = ?";
     992              $DB->execute($sql, array($cat->id));
     993  
     994          } else {
     995              // it needs full resorting
     996              $fixcategories[$cat->id] = $cat;
     997          }
     998          $cacheevents['changesincourse'] = true;
     999      }
    1000      unset($gapcategories);
    1001  
    1002      // fix course sortorders in problematic categories only
    1003      foreach ($fixcategories as $cat) {
    1004          $i = 1;
    1005          $courses = $DB->get_records('course', array('category'=>$cat->id), 'sortorder ASC, id DESC', 'id, sortorder');
    1006          foreach ($courses as $course) {
    1007              if ($course->sortorder != $cat->sortorder + $i) {
    1008                  $course->sortorder = $cat->sortorder + $i;
    1009                  $DB->update_record_raw('course', $course, true);
    1010                  $cacheevents['changesincourse'] = true;
    1011              }
    1012              $i++;
    1013          }
    1014      }
    1015  
    1016      // advise all caches that need to be rebuilt
    1017      foreach (array_keys($cacheevents) as $event) {
    1018          cache_helper::purge_by_event($event);
    1019      }
    1020  }
    1021  
    1022  /**
    1023   * Internal recursive category verification function, do not use directly!
    1024   *
    1025   * @todo Document the arguments of this function better
    1026   *
    1027   * @global object
    1028   * @uses CONTEXT_COURSECAT
    1029   * @param array $children
    1030   * @param int $sortorder
    1031   * @param string $parent
    1032   * @param int $depth
    1033   * @param string $path
    1034   * @param array $fixcontexts
    1035   * @return bool if changes were made
    1036   */
    1037  function _fix_course_cats($children, &$sortorder, $parent, $depth, $path, &$fixcontexts) {
    1038      global $DB;
    1039  
    1040      $depth++;
    1041      $changesmade = false;
    1042  
    1043      foreach ($children as $cat) {
    1044          $sortorder = $sortorder + get_max_courses_in_category();
    1045          $update = false;
    1046          if ($parent != $cat->parent or $depth != $cat->depth or $path.'/'.$cat->id != $cat->path) {
    1047              $cat->parent = $parent;
    1048              $cat->depth  = $depth;
    1049              $cat->path   = $path.'/'.$cat->id;
    1050              $update = true;
    1051  
    1052              // make sure context caches are rebuild and dirty contexts marked
    1053              $context = context_coursecat::instance($cat->id);
    1054              $fixcontexts[$context->id] = $context;
    1055          }
    1056          if ($cat->sortorder != $sortorder) {
    1057              $cat->sortorder = $sortorder;
    1058              $update = true;
    1059          }
    1060          if ($update) {
    1061              $DB->update_record('course_categories', $cat, true);
    1062              $changesmade = true;
    1063          }
    1064          if (isset($cat->children)) {
    1065              if (_fix_course_cats($cat->children, $sortorder, $cat->id, $cat->depth, $cat->path, $fixcontexts)) {
    1066                  $changesmade = true;
    1067              }
    1068          }
    1069      }
    1070      return $changesmade;
    1071  }
    1072  
    1073  /**
    1074   * List of remote courses that a user has access to via MNET.
    1075   * Works only on the IDP
    1076   *
    1077   * @global object
    1078   * @global object
    1079   * @param int @userid The user id to get remote courses for
    1080   * @return array Array of {@link $COURSE} of course objects
    1081   */
    1082  function get_my_remotecourses($userid=0) {
    1083      global $DB, $USER;
    1084  
    1085      if (empty($userid)) {
    1086          $userid = $USER->id;
    1087      }
    1088  
    1089      // we can not use SELECT DISTINCT + text field (summary) because of MS SQL and Oracle, subselect used therefore
    1090      $sql = "SELECT c.id, c.remoteid, c.shortname, c.fullname,
    1091                     c.hostid, c.summary, c.summaryformat, c.categoryname AS cat_name,
    1092                     h.name AS hostname
    1093                FROM {mnetservice_enrol_courses} c
    1094                JOIN (SELECT DISTINCT hostid, remotecourseid
    1095                        FROM {mnetservice_enrol_enrolments}
    1096                       WHERE userid = ?
    1097                     ) e ON (e.hostid = c.hostid AND e.remotecourseid = c.remoteid)
    1098                JOIN {mnet_host} h ON h.id = c.hostid";
    1099  
    1100      return $DB->get_records_sql($sql, array($userid));
    1101  }
    1102  
    1103  /**
    1104   * List of remote hosts that a user has access to via MNET.
    1105   * Works on the SP
    1106   *
    1107   * @global object
    1108   * @global object
    1109   * @return array|bool Array of host objects or false
    1110   */
    1111  function get_my_remotehosts() {
    1112      global $CFG, $USER;
    1113  
    1114      if ($USER->mnethostid == $CFG->mnet_localhost_id) {
    1115          return false; // Return nothing on the IDP
    1116      }
    1117      if (!empty($USER->mnet_foreign_host_array) && is_array($USER->mnet_foreign_host_array)) {
    1118          return $USER->mnet_foreign_host_array;
    1119      }
    1120      return false;
    1121  }
    1122  
    1123  
    1124  /**
    1125   * Returns a menu of all available scales from the site as well as the given course
    1126   *
    1127   * @global object
    1128   * @param int $courseid The id of the course as found in the 'course' table.
    1129   * @return array
    1130   */
    1131  function get_scales_menu($courseid=0) {
    1132      global $DB;
    1133  
    1134      $sql = "SELECT id, name, courseid
    1135                FROM {scale}
    1136               WHERE courseid = 0 or courseid = ?
    1137            ORDER BY courseid ASC, name ASC";
    1138      $params = array($courseid);
    1139      $scales = array();
    1140      $results = $DB->get_records_sql($sql, $params);
    1141      foreach ($results as $index => $record) {
    1142          $context = empty($record->courseid) ? context_system::instance() : context_course::instance($record->courseid);
    1143          $scales[$index] = format_string($record->name, false, ["context" => $context]);
    1144      }
    1145      // Format: [id => 'scale name'].
    1146      return $scales;
    1147  }
    1148  
    1149  /**
    1150   * Increment standard revision field.
    1151   *
    1152   * The revision are based on current time and are incrementing.
    1153   * There is a protection for runaway revisions, it may not go further than
    1154   * one hour into future.
    1155   *
    1156   * The field has to be XMLDB_TYPE_INTEGER with size 10.
    1157   *
    1158   * @param string $table
    1159   * @param string $field name of the field containing revision
    1160   * @param string $select use empty string when updating all records
    1161   * @param array $params optional select parameters
    1162   */
    1163  function increment_revision_number($table, $field, $select, array $params = null) {
    1164      global $DB;
    1165  
    1166      $now = time();
    1167      $sql = "UPDATE {{$table}}
    1168                     SET $field = (CASE
    1169                         WHEN $field IS NULL THEN $now
    1170                         WHEN $field < $now THEN $now
    1171                         WHEN $field > $now + 3600 THEN $now
    1172                         ELSE $field + 1 END)";
    1173      if ($select) {
    1174          $sql = $sql . " WHERE $select";
    1175      }
    1176      $DB->execute($sql, $params);
    1177  }
    1178  
    1179  
    1180  /// MODULE FUNCTIONS /////////////////////////////////////////////////
    1181  
    1182  /**
    1183   * Just gets a raw list of all modules in a course
    1184   *
    1185   * @global object
    1186   * @param int $courseid The id of the course as found in the 'course' table.
    1187   * @return array
    1188   */
    1189  function get_course_mods($courseid) {
    1190      global $DB;
    1191  
    1192      if (empty($courseid)) {
    1193          return false; // avoid warnings
    1194      }
    1195  
    1196      return $DB->get_records_sql("SELECT cm.*, m.name as modname
    1197                                     FROM {modules} m, {course_modules} cm
    1198                                    WHERE cm.course = ? AND cm.module = m.id AND m.visible = 1",
    1199                                  array($courseid)); // no disabled mods
    1200  }
    1201  
    1202  
    1203  /**
    1204   * Given an id of a course module, finds the coursemodule description
    1205   *
    1206   * Please note that this function performs 1-2 DB queries. When possible use cached
    1207   * course modinfo. For example get_fast_modinfo($courseorid)->get_cm($cmid)
    1208   * See also {@link cm_info::get_course_module_record()}
    1209   *
    1210   * @global object
    1211   * @param string $modulename name of module type, eg. resource, assignment,... (optional, slower and less safe if not specified)
    1212   * @param int $cmid course module id (id in course_modules table)
    1213   * @param int $courseid optional course id for extra validation
    1214   * @param bool $sectionnum include relative section number (0,1,2 ...)
    1215   * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
    1216   *                        IGNORE_MULTIPLE means return first, ignore multiple records found(not recommended);
    1217   *                        MUST_EXIST means throw exception if no record or multiple records found
    1218   * @return stdClass
    1219   */
    1220  function get_coursemodule_from_id($modulename, $cmid, $courseid=0, $sectionnum=false, $strictness=IGNORE_MISSING) {
    1221      global $DB;
    1222  
    1223      $params = array('cmid'=>$cmid);
    1224  
    1225      if (!$modulename) {
    1226          if (!$modulename = $DB->get_field_sql("SELECT md.name
    1227                                                   FROM {modules} md
    1228                                                   JOIN {course_modules} cm ON cm.module = md.id
    1229                                                  WHERE cm.id = :cmid", $params, $strictness)) {
    1230              return false;
    1231          }
    1232      } else {
    1233          if (!core_component::is_valid_plugin_name('mod', $modulename)) {
    1234              throw new coding_exception('Invalid modulename parameter');
    1235          }
    1236      }
    1237  
    1238      $params['modulename'] = $modulename;
    1239  
    1240      $courseselect = "";
    1241      $sectionfield = "";
    1242      $sectionjoin  = "";
    1243  
    1244      if ($courseid) {
    1245          $courseselect = "AND cm.course = :courseid";
    1246          $params['courseid'] = $courseid;
    1247      }
    1248  
    1249      if ($sectionnum) {
    1250          $sectionfield = ", cw.section AS sectionnum";
    1251          $sectionjoin  = "LEFT JOIN {course_sections} cw ON cw.id = cm.section";
    1252      }
    1253  
    1254      $sql = "SELECT cm.*, m.name, md.name AS modname $sectionfield
    1255                FROM {course_modules} cm
    1256                     JOIN {modules} md ON md.id = cm.module
    1257                     JOIN {".$modulename."} m ON m.id = cm.instance
    1258                     $sectionjoin
    1259               WHERE cm.id = :cmid AND md.name = :modulename
    1260                     $courseselect";
    1261  
    1262      return $DB->get_record_sql($sql, $params, $strictness);
    1263  }
    1264  
    1265  /**
    1266   * Given an instance number of a module, finds the coursemodule description
    1267   *
    1268   * Please note that this function performs DB query. When possible use cached course
    1269   * modinfo. For example get_fast_modinfo($courseorid)->instances[$modulename][$instance]
    1270   * See also {@link cm_info::get_course_module_record()}
    1271   *
    1272   * @global object
    1273   * @param string $modulename name of module type, eg. resource, assignment,...
    1274   * @param int $instance module instance number (id in resource, assignment etc. table)
    1275   * @param int $courseid optional course id for extra validation
    1276   * @param bool $sectionnum include relative section number (0,1,2 ...)
    1277   * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
    1278   *                        IGNORE_MULTIPLE means return first, ignore multiple records found(not recommended);
    1279   *                        MUST_EXIST means throw exception if no record or multiple records found
    1280   * @return stdClass
    1281   */
    1282  function get_coursemodule_from_instance($modulename, $instance, $courseid=0, $sectionnum=false, $strictness=IGNORE_MISSING) {
    1283      global $DB;
    1284  
    1285      if (!core_component::is_valid_plugin_name('mod', $modulename)) {
    1286          throw new coding_exception('Invalid modulename parameter');
    1287      }
    1288  
    1289      $params = array('instance'=>$instance, 'modulename'=>$modulename);
    1290  
    1291      $courseselect = "";
    1292      $sectionfield = "";
    1293      $sectionjoin  = "";
    1294  
    1295      if ($courseid) {
    1296          $courseselect = "AND cm.course = :courseid";
    1297          $params['courseid'] = $courseid;
    1298      }
    1299  
    1300      if ($sectionnum) {
    1301          $sectionfield = ", cw.section AS sectionnum";
    1302          $sectionjoin  = "LEFT JOIN {course_sections} cw ON cw.id = cm.section";
    1303      }
    1304  
    1305      $sql = "SELECT cm.*, m.name, md.name AS modname $sectionfield
    1306                FROM {course_modules} cm
    1307                     JOIN {modules} md ON md.id = cm.module
    1308                     JOIN {".$modulename."} m ON m.id = cm.instance
    1309                     $sectionjoin
    1310               WHERE m.id = :instance AND md.name = :modulename
    1311                     $courseselect";
    1312  
    1313      return $DB->get_record_sql($sql, $params, $strictness);
    1314  }
    1315  
    1316  /**
    1317   * Returns all course modules of given activity in course
    1318   *
    1319   * @param string $modulename The module name (forum, quiz, etc.)
    1320   * @param int $courseid The course id to get modules for
    1321   * @param string $extrafields extra fields starting with m.
    1322   * @return array Array of results
    1323   */
    1324  function get_coursemodules_in_course($modulename, $courseid, $extrafields='') {
    1325      global $DB;
    1326  
    1327      if (!core_component::is_valid_plugin_name('mod', $modulename)) {
    1328          throw new coding_exception('Invalid modulename parameter');
    1329      }
    1330  
    1331      if (!empty($extrafields)) {
    1332          $extrafields = ", $extrafields";
    1333      }
    1334      $params = array();
    1335      $params['courseid'] = $courseid;
    1336      $params['modulename'] = $modulename;
    1337  
    1338  
    1339      return $DB->get_records_sql("SELECT cm.*, m.name, md.name as modname $extrafields
    1340                                     FROM {course_modules} cm, {modules} md, {".$modulename."} m
    1341                                    WHERE cm.course = :courseid AND
    1342                                          cm.instance = m.id AND
    1343                                          md.name = :modulename AND
    1344                                          md.id = cm.module", $params);
    1345  }
    1346  
    1347  /**
    1348   * Returns an array of all the active instances of a particular module in given courses, sorted in the order they are defined
    1349   *
    1350   * Returns an array of all the active instances of a particular
    1351   * module in given courses, sorted in the order they are defined
    1352   * in the course. Returns an empty array on any errors.
    1353   *
    1354   * The returned objects includle the columns cw.section, cm.visible,
    1355   * cm.groupmode, and cm.groupingid, and are indexed by cm.id.
    1356   *
    1357   * @global object
    1358   * @global object
    1359   * @param string $modulename The name of the module to get instances for
    1360   * @param array $courses an array of course objects.
    1361   * @param int $userid
    1362   * @param int $includeinvisible
    1363   * @return array of module instance objects, including some extra fields from the course_modules
    1364   *          and course_sections tables, or an empty array if an error occurred.
    1365   */
    1366  function get_all_instances_in_courses($modulename, $courses, $userid=NULL, $includeinvisible=false) {
    1367      global $CFG, $DB;
    1368  
    1369      if (!core_component::is_valid_plugin_name('mod', $modulename)) {
    1370          throw new coding_exception('Invalid modulename parameter');
    1371      }
    1372  
    1373      $outputarray = array();
    1374  
    1375      if (empty($courses) || !is_array($courses) || count($courses) == 0) {
    1376          return $outputarray;
    1377      }
    1378  
    1379      list($coursessql, $params) = $DB->get_in_or_equal(array_keys($courses), SQL_PARAMS_NAMED, 'c0');
    1380      $params['modulename'] = $modulename;
    1381  
    1382      if (!$rawmods = $DB->get_records_sql("SELECT cm.id AS coursemodule, m.*, cw.section, cm.visible AS visible,
    1383                                                   cm.groupmode, cm.groupingid
    1384                                              FROM {course_modules} cm, {course_sections} cw, {modules} md,
    1385                                                   {".$modulename."} m
    1386                                             WHERE cm.course $coursessql AND
    1387                                                   cm.instance = m.id AND
    1388                                                   cm.section = cw.id AND
    1389                                                   md.name = :modulename AND
    1390                                                   md.id = cm.module", $params)) {
    1391          return $outputarray;
    1392      }
    1393  
    1394      foreach ($courses as $course) {
    1395          $modinfo = get_fast_modinfo($course, $userid);
    1396  
    1397          if (empty($modinfo->instances[$modulename])) {
    1398              continue;
    1399          }
    1400  
    1401          foreach ($modinfo->instances[$modulename] as $cm) {
    1402              if (!$includeinvisible and !$cm->uservisible) {
    1403                  continue;
    1404              }
    1405              if (!isset($rawmods[$cm->id])) {
    1406                  continue;
    1407              }
    1408              $instance = $rawmods[$cm->id];
    1409              if (!empty($cm->extra)) {
    1410                  $instance->extra = $cm->extra;
    1411              }
    1412              $outputarray[] = $instance;
    1413          }
    1414      }
    1415  
    1416      return $outputarray;
    1417  }
    1418  
    1419  /**
    1420   * Returns an array of all the active instances of a particular module in a given course,
    1421   * sorted in the order they are defined.
    1422   *
    1423   * Returns an array of all the active instances of a particular
    1424   * module in a given course, sorted in the order they are defined
    1425   * in the course. Returns an empty array on any errors.
    1426   *
    1427   * The returned objects includle the columns cw.section, cm.visible,
    1428   * cm.groupmode, and cm.groupingid, and are indexed by cm.id.
    1429   *
    1430   * Simply calls {@link all_instances_in_courses()} with a single provided course
    1431   *
    1432   * @param string $modulename The name of the module to get instances for
    1433   * @param object $course The course obect.
    1434   * @return array of module instance objects, including some extra fields from the course_modules
    1435   *          and course_sections tables, or an empty array if an error occurred.
    1436   * @param int $userid
    1437   * @param int $includeinvisible
    1438   */
    1439  function get_all_instances_in_course($modulename, $course, $userid=NULL, $includeinvisible=false) {
    1440      return get_all_instances_in_courses($modulename, array($course->id => $course), $userid, $includeinvisible);
    1441  }
    1442  
    1443  
    1444  /**
    1445   * Determine whether a module instance is visible within a course
    1446   *
    1447   * Given a valid module object with info about the id and course,
    1448   * and the module's type (eg "forum") returns whether the object
    1449   * is visible or not according to the 'eye' icon only.
    1450   *
    1451   * NOTE: This does NOT take into account visibility to a particular user.
    1452   * To get visibility access for a specific user, use get_fast_modinfo, get a
    1453   * cm_info object from this, and check the ->uservisible property; or use
    1454   * the \core_availability\info_module::is_user_visible() static function.
    1455   *
    1456   * @global object
    1457  
    1458   * @param $moduletype Name of the module eg 'forum'
    1459   * @param $module Object which is the instance of the module
    1460   * @return bool Success
    1461   */
    1462  function instance_is_visible($moduletype, $module) {
    1463      global $DB;
    1464  
    1465      if (!empty($module->id)) {
    1466          $params = array('courseid'=>$module->course, 'moduletype'=>$moduletype, 'moduleid'=>$module->id);
    1467          if ($records = $DB->get_records_sql("SELECT cm.instance, cm.visible, cm.groupingid, cm.id, cm.course
    1468                                                 FROM {course_modules} cm, {modules} m
    1469                                                WHERE cm.course = :courseid AND
    1470                                                      cm.module = m.id AND
    1471                                                      m.name = :moduletype AND
    1472                                                      cm.instance = :moduleid", $params)) {
    1473  
    1474              foreach ($records as $record) { // there should only be one - use the first one
    1475                  return $record->visible;
    1476              }
    1477          }
    1478      }
    1479      return true;  // visible by default!
    1480  }
    1481  
    1482  
    1483  /// LOG FUNCTIONS /////////////////////////////////////////////////////
    1484  
    1485  /**
    1486   * Get instance of log manager.
    1487   *
    1488   * @param bool $forcereload
    1489   * @return \core\log\manager
    1490   */
    1491  function get_log_manager($forcereload = false) {
    1492      /** @var \core\log\manager $singleton */
    1493      static $singleton = null;
    1494  
    1495      if ($forcereload and isset($singleton)) {
    1496          $singleton->dispose();
    1497          $singleton = null;
    1498      }
    1499  
    1500      if (isset($singleton)) {
    1501          return $singleton;
    1502      }
    1503  
    1504      $classname = '\tool_log\log\manager';
    1505      if (defined('LOG_MANAGER_CLASS')) {
    1506          $classname = LOG_MANAGER_CLASS;
    1507      }
    1508  
    1509      if (!class_exists($classname)) {
    1510          if (!empty($classname)) {
    1511              debugging("Cannot find log manager class '$classname'.", DEBUG_DEVELOPER);
    1512          }
    1513          $classname = '\core\log\dummy_manager';
    1514      }
    1515  
    1516      $singleton = new $classname();
    1517      return $singleton;
    1518  }
    1519  
    1520  /**
    1521   * Add an entry to the config log table.
    1522   *
    1523   * These are "action" focussed rather than web server hits,
    1524   * and provide a way to easily reconstruct changes to Moodle configuration.
    1525   *
    1526   * @package core
    1527   * @category log
    1528   * @global moodle_database $DB
    1529   * @global stdClass $USER
    1530   * @param    string  $name     The name of the configuration change action
    1531                                 For example 'filter_active' when activating or deactivating a filter
    1532   * @param    string  $oldvalue The config setting's previous value
    1533   * @param    string  $value    The config setting's new value
    1534   * @param    string  $plugin   Plugin name, for example a filter name when changing filter configuration
    1535   * @return void
    1536   */
    1537  function add_to_config_log($name, $oldvalue, $value, $plugin) {
    1538      global $USER, $DB;
    1539  
    1540      $log = new stdClass();
    1541      // Use 0 as user id during install.
    1542      $log->userid       = during_initial_install() ? 0 : $USER->id;
    1543      $log->timemodified = time();
    1544      $log->name         = $name;
    1545      $log->oldvalue  = $oldvalue;
    1546      $log->value     = $value;
    1547      $log->plugin    = $plugin;
    1548  
    1549      $id = $DB->insert_record('config_log', $log);
    1550  
    1551      $event = core\event\config_log_created::create(array(
    1552              'objectid' => $id,
    1553              'userid' => $log->userid,
    1554              'context' => \context_system::instance(),
    1555              'other' => array(
    1556                  'name' => $log->name,
    1557                  'oldvalue' => $log->oldvalue,
    1558                  'value' => $log->value,
    1559                  'plugin' => $log->plugin
    1560              )
    1561          ));
    1562      $event->trigger();
    1563  }
    1564  
    1565  /**
    1566   * Store user last access times - called when use enters a course or site
    1567   *
    1568   * @package core
    1569   * @category log
    1570   * @global stdClass $USER
    1571   * @global stdClass $CFG
    1572   * @global moodle_database $DB
    1573   * @uses LASTACCESS_UPDATE_SECS
    1574   * @uses SITEID
    1575   * @param int $courseid  empty courseid means site
    1576   * @return void
    1577   */
    1578  function user_accesstime_log($courseid=0) {
    1579      global $USER, $CFG, $DB;
    1580  
    1581      if (!isloggedin() or \core\session\manager::is_loggedinas()) {
    1582          // no access tracking
    1583          return;
    1584      }
    1585  
    1586      if (isguestuser()) {
    1587          // Do not update guest access times/ips for performance.
    1588          return;
    1589      }
    1590  
    1591      if (empty($courseid)) {
    1592          $courseid = SITEID;
    1593      }
    1594  
    1595      $timenow = time();
    1596  
    1597  /// Store site lastaccess time for the current user
    1598      if ($timenow - $USER->lastaccess > LASTACCESS_UPDATE_SECS) {
    1599      /// Update $USER->lastaccess for next checks
    1600          $USER->lastaccess = $timenow;
    1601  
    1602          $last = new stdClass();
    1603          $last->id         = $USER->id;
    1604          $last->lastip     = getremoteaddr();
    1605          $last->lastaccess = $timenow;
    1606  
    1607          $DB->update_record_raw('user', $last);
    1608      }
    1609  
    1610      if ($courseid == SITEID) {
    1611      ///  no user_lastaccess for frontpage
    1612          return;
    1613      }
    1614  
    1615  /// Store course lastaccess times for the current user
    1616      if (empty($USER->currentcourseaccess[$courseid]) or ($timenow - $USER->currentcourseaccess[$courseid] > LASTACCESS_UPDATE_SECS)) {
    1617  
    1618          $lastaccess = $DB->get_field('user_lastaccess', 'timeaccess', array('userid'=>$USER->id, 'courseid'=>$courseid));
    1619  
    1620          if ($lastaccess === false) {
    1621              // Update course lastaccess for next checks
    1622              $USER->currentcourseaccess[$courseid] = $timenow;
    1623  
    1624              $last = new stdClass();
    1625              $last->userid     = $USER->id;
    1626              $last->courseid   = $courseid;
    1627              $last->timeaccess = $timenow;
    1628              try {
    1629                  $DB->insert_record_raw('user_lastaccess', $last, false);
    1630              } catch (dml_write_exception $e) {
    1631                  // During a race condition we can fail to find the data, then it appears.
    1632                  // If we still can't find it, rethrow the exception.
    1633                  $lastaccess = $DB->get_field('user_lastaccess', 'timeaccess', array('userid' => $USER->id,
    1634                                                                                      'courseid' => $courseid));
    1635                  if ($lastaccess === false) {
    1636                      throw $e;
    1637                  }
    1638                  // If we did find it, the race condition was true and another thread has inserted the time for us.
    1639                  // We can just continue without having to do anything.
    1640              }
    1641  
    1642          } else if ($timenow - $lastaccess <  LASTACCESS_UPDATE_SECS) {
    1643              // no need to update now, it was updated recently in concurrent login ;-)
    1644  
    1645          } else {
    1646              // Update course lastaccess for next checks
    1647              $USER->currentcourseaccess[$courseid] = $timenow;
    1648  
    1649              $DB->set_field('user_lastaccess', 'timeaccess', $timenow, array('userid'=>$USER->id, 'courseid'=>$courseid));
    1650          }
    1651      }
    1652  }
    1653  
    1654  /// GENERAL HELPFUL THINGS  ///////////////////////////////////
    1655  
    1656  /**
    1657   * Dumps a given object's information for debugging purposes
    1658   *
    1659   * When used in a CLI script, the object's information is written to the standard
    1660   * error output stream. When used in a web script, the object is dumped to a
    1661   * pre-formatted block with the "notifytiny" CSS class.
    1662   *
    1663   * @param mixed $object The data to be printed
    1664   * @return void output is echo'd
    1665   */
    1666  function print_object($object) {
    1667  
    1668      // we may need a lot of memory here
    1669      raise_memory_limit(MEMORY_EXTRA);
    1670  
    1671      if (CLI_SCRIPT) {
    1672          fwrite(STDERR, print_r($object, true));
    1673          fwrite(STDERR, PHP_EOL);
    1674      } else if (AJAX_SCRIPT) {
    1675          foreach (explode("\n", print_r($object, true)) as $line) {
    1676              error_log($line);
    1677          }
    1678      } else {
    1679          echo html_writer::tag('pre', s(print_r($object, true)), array('class' => 'notifytiny'));
    1680      }
    1681  }
    1682  
    1683  /**
    1684   * This function is the official hook inside XMLDB stuff to delegate its debug to one
    1685   * external function.
    1686   *
    1687   * Any script can avoid calls to this function by defining XMLDB_SKIP_DEBUG_HOOK before
    1688   * using XMLDB classes. Obviously, also, if this function doesn't exist, it isn't invoked ;-)
    1689   *
    1690   * @uses DEBUG_DEVELOPER
    1691   * @param string $message string contains the error message
    1692   * @param object $object object XMLDB object that fired the debug
    1693   */
    1694  function xmldb_debug($message, $object) {
    1695  
    1696      debugging($message, DEBUG_DEVELOPER);
    1697  }
    1698  
    1699  /**
    1700   * @global object
    1701   * @uses CONTEXT_COURSECAT
    1702   * @return boolean Whether the user can create courses in any category in the system.
    1703   */
    1704  function user_can_create_courses() {
    1705      global $DB;
    1706      $catsrs = $DB->get_recordset('course_categories');
    1707      foreach ($catsrs as $cat) {
    1708          if (has_capability('moodle/course:create', context_coursecat::instance($cat->id))) {
    1709              $catsrs->close();
    1710              return true;
    1711          }
    1712      }
    1713      $catsrs->close();
    1714      return false;
    1715  }
    1716  
    1717  /**
    1718   * This method can update the values in mulitple database rows for a colum with
    1719   * a unique index, without violating that constraint.
    1720   *
    1721   * Suppose we have a table with a unique index on (otherid, sortorder), and
    1722   * for a particular value of otherid, we want to change all the sort orders.
    1723   * You have to do this carefully or you will violate the unique index at some time.
    1724   * This method takes care of the details for you.
    1725   *
    1726   * Note that, it is the responsibility of the caller to make sure that the
    1727   * requested rename is legal. For example, if you ask for [1 => 2, 2 => 2]
    1728   * then you will get a unique key violation error from the database.
    1729   *
    1730   * @param string $table The database table to modify.
    1731   * @param string $field the field that contains the values we are going to change.
    1732   * @param array $newvalues oldvalue => newvalue how to change the values.
    1733   *      E.g. [1 => 4, 2 => 1, 3 => 3, 4 => 2].
    1734   * @param array $otherconditions array fieldname => requestedvalue extra WHERE clause
    1735   *      conditions to restrict which rows are affected. E.g. array('otherid' => 123).
    1736   * @param int $unusedvalue (defaults to -1) a value that is never used in $ordercol.
    1737   */
    1738  function update_field_with_unique_index($table, $field, array $newvalues,
    1739          array $otherconditions, $unusedvalue = -1) {
    1740      global $DB;
    1741      $safechanges = decompose_update_into_safe_changes($newvalues, $unusedvalue);
    1742  
    1743      $transaction = $DB->start_delegated_transaction();
    1744      foreach ($safechanges as $change) {
    1745          list($from, $to) = $change;
    1746          $otherconditions[$field] = $from;
    1747          $DB->set_field($table, $field, $to, $otherconditions);
    1748      }
    1749      $transaction->allow_commit();
    1750  }
    1751  
    1752  /**
    1753   * Helper used by {@link update_field_with_unique_index()}. Given a desired
    1754   * set of changes, break them down into single udpates that can be done one at
    1755   * a time without breaking any unique index constraints.
    1756   *
    1757   * Suppose the input is array(1 => 2, 2 => 1) and -1. Then the output will be
    1758   * array (array(1, -1), array(2, 1), array(-1, 2)). This function solves this
    1759   * problem in the general case, not just for simple swaps. The unit tests give
    1760   * more examples.
    1761   *
    1762   * Note that, it is the responsibility of the caller to make sure that the
    1763   * requested rename is legal. For example, if you ask for something impossible
    1764   * like array(1 => 2, 2 => 2) then the results are undefined. (You will probably
    1765   * get a unique key violation error from the database later.)
    1766   *
    1767   * @param array $newvalues The desired re-ordering.
    1768   *      E.g. array(1 => 4, 2 => 1, 3 => 3, 4 => 2).
    1769   * @param int $unusedvalue A value that is not currently used.
    1770   * @return array A safe way to perform the re-order. An array of two-element
    1771   *      arrays array($from, $to).
    1772   *      E.g. array(array(1, -1), array(2, 1), array(4, 2), array(-1, 4)).
    1773   */
    1774  function decompose_update_into_safe_changes(array $newvalues, $unusedvalue) {
    1775      $nontrivialmap = array();
    1776      foreach ($newvalues as $from => $to) {
    1777          if ($from == $unusedvalue || $to == $unusedvalue) {
    1778              throw new \coding_exception('Supposedly unused value ' . $unusedvalue . ' is actually used!');
    1779          }
    1780          if ($from != $to) {
    1781              $nontrivialmap[$from] = $to;
    1782          }
    1783      }
    1784  
    1785      if (empty($nontrivialmap)) {
    1786          return array();
    1787      }
    1788  
    1789      // First we deal with all renames that are not part of cycles.
    1790      // This bit is O(n^2) and it ought to be possible to do better,
    1791      // but it does not seem worth the effort.
    1792      $safechanges = array();
    1793      $nontrivialmapchanged = true;
    1794      while ($nontrivialmapchanged) {
    1795          $nontrivialmapchanged = false;
    1796  
    1797          foreach ($nontrivialmap as $from => $to) {
    1798              if (array_key_exists($to, $nontrivialmap)) {
    1799                  continue; // Cannot currenly do this rename.
    1800              }
    1801              // Is safe to do this rename now.
    1802              $safechanges[] = array($from, $to);
    1803              unset($nontrivialmap[$from]);
    1804              $nontrivialmapchanged = true;
    1805          }
    1806      }
    1807  
    1808      // Are we done?
    1809      if (empty($nontrivialmap)) {
    1810          return $safechanges;
    1811      }
    1812  
    1813      // Now what is left in $nontrivialmap must be a permutation,
    1814      // which must be a combination of disjoint cycles. We need to break them.
    1815      while (!empty($nontrivialmap)) {
    1816          // Extract the first cycle.
    1817          reset($nontrivialmap);
    1818          $current = $cyclestart = key($nontrivialmap);
    1819          $cycle = array();
    1820          do {
    1821              $cycle[] = $current;
    1822              $next = $nontrivialmap[$current];
    1823              unset($nontrivialmap[$current]);
    1824              $current = $next;
    1825          } while ($current != $cyclestart);
    1826  
    1827          // Now convert it to a sequence of safe renames by using a temp.
    1828          $safechanges[] = array($cyclestart, $unusedvalue);
    1829          $cycle[0] = $unusedvalue;
    1830          $to = $cyclestart;
    1831          while ($from = array_pop($cycle)) {
    1832              $safechanges[] = array($from, $to);
    1833              $to = $from;
    1834          }
    1835      }
    1836  
    1837      return $safechanges;
    1838  }
    1839  
    1840  /**
    1841   * Return maximum number of courses in a category
    1842   *
    1843   * @uses MAX_COURSES_IN_CATEGORY
    1844   * @return int number of courses
    1845   */
    1846  function get_max_courses_in_category() {
    1847      global $CFG;
    1848      // Use default MAX_COURSES_IN_CATEGORY if $CFG->maxcoursesincategory is not set or invalid.
    1849      if (!isset($CFG->maxcoursesincategory) || clean_param($CFG->maxcoursesincategory, PARAM_INT) == 0) {
    1850          return MAX_COURSES_IN_CATEGORY;
    1851      } else {
    1852          return $CFG->maxcoursesincategory;
    1853      }
    1854  }
    1855  
    1856  /**
    1857   * Prepare a safe ORDER BY statement from user interactable requests.
    1858   *
    1859   * This allows safe user specified sorting (ORDER BY), by abstracting the SQL from the value being requested by the user.
    1860   * A standard string (and optional direction) can be specified, which will be mapped to a predefined allow list of SQL ordering.
    1861   * The mapping can optionally include a 'default', which will be used if the key provided is invalid.
    1862   *
    1863   * Example usage:
    1864   *      -If $orderbymap = [
    1865   *              'courseid' => 'c.id',
    1866   *              'somecustomvalue'=> 'c.startdate, c.shortname',
    1867   *              'default' => 'c.fullname',
    1868   *       ]
    1869   *      -A value from the map array's keys can be passed in by a user interaction (eg web service) along with an optional direction.
    1870   *      -get_safe_orderby($orderbymap, 'courseid', 'DESC') would return: ORDER BY c.id DESC
    1871   *      -get_safe_orderby($orderbymap, 'somecustomvalue') would return: ORDER BY c.startdate, c.shortname
    1872   *      -get_safe_orderby($orderbymap, 'invalidblah', 'DESC') would return: ORDER BY c.fullname DESC
    1873   *      -If no default key was specified in $orderbymap, the invalidblah example above would return empty string.
    1874   *
    1875   * @param array $orderbymap An array in the format [keystring => sqlstring]. A default fallback can be set with the key 'default'.
    1876   * @param string $orderbykey A string to be mapped to a key in $orderbymap.
    1877   * @param string $direction Optional ORDER BY direction (ASC/DESC, case insensitive).
    1878   * @param bool $useprefix Whether ORDER BY is prefixed to the output (true by default). This should not be modified in most cases.
    1879   *                        It is included to enable get_safe_orderby_multiple() to use this function multiple times.
    1880   * @return string The ORDER BY statement, or empty string if $orderbykey is invalid and no default is mapped.
    1881   */
    1882  function get_safe_orderby(array $orderbymap, string $orderbykey, string $direction = '', bool $useprefix = true): string {
    1883      $orderby = $useprefix ? ' ORDER BY ' : '';
    1884      $output = '';
    1885  
    1886      // Only include an order direction if ASC/DESC is explicitly specified (case insensitive).
    1887      $direction = strtoupper($direction);
    1888      if (!in_array($direction, ['ASC', 'DESC'], true)) {
    1889          $direction = '';
    1890      } else {
    1891          $direction = " {$direction}";
    1892      }
    1893  
    1894      // Prepare the statement if the key maps to a defined sort parameter.
    1895      if (isset($orderbymap[$orderbykey])) {
    1896          $output = "{$orderby}{$orderbymap[$orderbykey]}{$direction}";
    1897      } else if (array_key_exists('default', $orderbymap)) {
    1898          // Fall back to use the default if one is specified.
    1899          $output = "{$orderby}{$orderbymap['default']}{$direction}";
    1900      }
    1901  
    1902      return $output;
    1903  }
    1904  
    1905  /**
    1906   * Prepare a safe ORDER BY statement from user interactable requests using multiple values.
    1907   *
    1908   * This allows safe user specified sorting (ORDER BY) similar to get_safe_orderby(), but supports multiple keys and directions.
    1909   * This is useful in cases where combinations of columns are needed and/or each item requires a specified direction (ASC/DESC).
    1910   * The mapping can optionally include a 'default', which will be used if the key provided is invalid.
    1911   *
    1912   * Example usage:
    1913   *      -If $orderbymap = [
    1914   *              'courseid' => 'c.id',
    1915   *              'fullname'=> 'c.fullname',
    1916   *              'default' => 'c.startdate',
    1917   *          ]
    1918   *      -An array of values from the map's keys can be passed in by a user interaction (eg web service), with optional directions.
    1919   *      -get_safe_orderby($orderbymap, ['courseid', 'fullname'], ['DESC', 'ASC']) would return: ORDER BY c.id DESC, c.fullname ASC
    1920   *      -get_safe_orderby($orderbymap, ['courseid', 'invalidblah'], ['aaa', 'DESC']) would return: ORDER BY c.id, c.startdate DESC
    1921   *      -If no default key was specified in $orderbymap, the invalidblah example above would return: ORDER BY c.id
    1922   *
    1923   * @param array $orderbymap An array in the format [keystring => sqlstring]. A default fallback can be set with the key 'default'.
    1924   * @param array $orderbykeys An array of strings to be mapped to keys in $orderbymap.
    1925   * @param array $directions Optional array of ORDER BY direction (ASC/DESC, case insensitive).
    1926   *                          The array keys should match array keys in $orderbykeys.
    1927   * @return string The ORDER BY statement, or empty string if $orderbykeys contains no valid items and no default is mapped.
    1928   */
    1929  function get_safe_orderby_multiple(array $orderbymap, array $orderbykeys, array $directions = []): string {
    1930      $output = '';
    1931  
    1932      // Check each key for a valid mapping and add to the ORDER BY statement (invalid entries will be empty strings).
    1933      foreach ($orderbykeys as $index => $orderbykey) {
    1934          $direction = $directions[$index] ?? '';
    1935          $safeorderby = get_safe_orderby($orderbymap, $orderbykey, $direction, false);
    1936  
    1937          if (!empty($safeorderby)) {
    1938              $output .= ", {$safeorderby}";
    1939          }
    1940      }
    1941  
    1942      // Prefix with ORDER BY if any valid ordering is specified (and remove comma from the start).
    1943      if (!empty($output)) {
    1944          $output = ' ORDER BY' . ltrim($output, ',');
    1945      }
    1946  
    1947      return $output;
    1948  }