[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/lib/ -> datalib.php (source)

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


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1