[ Index ]

PHP Cross Reference of Unnamed Project

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   * @param array $requiredcapabilities Extra list of capabilities used to filter courses
 745   * @return object {@link $COURSE} records
 746   */
 747  function get_courses_search($searchterms, $sort, $page, $recordsperpage, &$totalcount,
 748                              $requiredcapabilities = array()) {
 749      global $CFG, $DB;
 750  
 751      if ($DB->sql_regex_supported()) {
 752          $REGEXP    = $DB->sql_regex(true);
 753          $NOTREGEXP = $DB->sql_regex(false);
 754      }
 755  
 756      $searchcond = array();
 757      $params     = array();
 758      $i = 0;
 759  
 760      // Thanks Oracle for your non-ansi concat and type limits in coalesce. MDL-29912
 761      if ($DB->get_dbfamily() == 'oracle') {
 762          $concat = "(c.summary|| ' ' || c.fullname || ' ' || c.idnumber || ' ' || c.shortname)";
 763      } else {
 764          $concat = $DB->sql_concat("COALESCE(c.summary, '')", "' '", 'c.fullname', "' '", 'c.idnumber', "' '", 'c.shortname');
 765      }
 766  
 767      foreach ($searchterms as $searchterm) {
 768          $i++;
 769  
 770          $NOT = false; /// Initially we aren't going to perform NOT LIKE searches, only MSSQL and Oracle
 771                     /// will use it to simulate the "-" operator with LIKE clause
 772  
 773      /// Under Oracle and MSSQL, trim the + and - operators and perform
 774      /// simpler LIKE (or NOT LIKE) queries
 775          if (!$DB->sql_regex_supported()) {
 776              if (substr($searchterm, 0, 1) == '-') {
 777                  $NOT = true;
 778              }
 779              $searchterm = trim($searchterm, '+-');
 780          }
 781  
 782          // TODO: +- may not work for non latin languages
 783  
 784          if (substr($searchterm,0,1) == '+') {
 785              $searchterm = trim($searchterm, '+-');
 786              $searchterm = preg_quote($searchterm, '|');
 787              $searchcond[] = "$concat $REGEXP :ss$i";
 788              $params['ss'.$i] = "(^|[^a-zA-Z0-9])$searchterm([^a-zA-Z0-9]|$)";
 789  
 790          } else if (substr($searchterm,0,1) == "-") {
 791              $searchterm = trim($searchterm, '+-');
 792              $searchterm = preg_quote($searchterm, '|');
 793              $searchcond[] = "$concat $NOTREGEXP :ss$i";
 794              $params['ss'.$i] = "(^|[^a-zA-Z0-9])$searchterm([^a-zA-Z0-9]|$)";
 795  
 796          } else {
 797              $searchcond[] = $DB->sql_like($concat,":ss$i", false, true, $NOT);
 798              $params['ss'.$i] = "%$searchterm%";
 799          }
 800      }
 801  
 802      if (empty($searchcond)) {
 803          $searchcond = array('1 = 1');
 804      }
 805  
 806      $searchcond = implode(" AND ", $searchcond);
 807  
 808      $courses = array();
 809      $c = 0; // counts how many visible courses we've seen
 810  
 811      // Tiki pagination
 812      $limitfrom = $page * $recordsperpage;
 813      $limitto   = $limitfrom + $recordsperpage;
 814  
 815      $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
 816      $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
 817      $params['contextlevel'] = CONTEXT_COURSE;
 818  
 819      $sql = "SELECT c.* $ccselect
 820                FROM {course} c
 821             $ccjoin
 822               WHERE $searchcond AND c.id <> ".SITEID."
 823            ORDER BY $sort";
 824  
 825      $rs = $DB->get_recordset_sql($sql, $params);
 826      foreach($rs as $course) {
 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 (!$course->visible && !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
 831              continue;
 832          }
 833          if (!empty($requiredcapabilities)) {
 834              if (!has_all_capabilities($requiredcapabilities, $coursecontext)) {
 835                  continue;
 836              }
 837          }
 838          // Don't exit this loop till the end
 839          // we need to count all the visible courses
 840          // to update $totalcount
 841          if ($c >= $limitfrom && $c < $limitto) {
 842              $courses[$course->id] = $course;
 843          }
 844          $c++;
 845      }
 846      $rs->close();
 847  
 848      // our caller expects 2 bits of data - our return
 849      // array, and an updated $totalcount
 850      $totalcount = $c;
 851      return $courses;
 852  }
 853  
 854  /**
 855   * Fixes course category and course sortorder, also verifies category and course parents and paths.
 856   * (circular references are not fixed)
 857   *
 858   * @global object
 859   * @global object
 860   * @uses MAX_COURSES_IN_CATEGORY
 861   * @uses MAX_COURSE_CATEGORIES
 862   * @uses SITEID
 863   * @uses CONTEXT_COURSE
 864   * @return void
 865   */
 866  function fix_course_sortorder() {
 867      global $DB, $SITE;
 868  
 869      //WARNING: this is PHP5 only code!
 870  
 871      // if there are any changes made to courses or categories we will trigger
 872      // the cache events to purge all cached courses/categories data
 873      $cacheevents = array();
 874  
 875      if ($unsorted = $DB->get_records('course_categories', array('sortorder'=>0))) {
 876          //move all categories that are not sorted yet to the end
 877          $DB->set_field('course_categories', 'sortorder', MAX_COURSES_IN_CATEGORY*MAX_COURSE_CATEGORIES, array('sortorder'=>0));
 878          $cacheevents['changesincoursecat'] = true;
 879      }
 880  
 881      $allcats = $DB->get_records('course_categories', null, 'sortorder, id', 'id, sortorder, parent, depth, path');
 882      $topcats    = array();
 883      $brokencats = array();
 884      foreach ($allcats as $cat) {
 885          $sortorder = (int)$cat->sortorder;
 886          if (!$cat->parent) {
 887              while(isset($topcats[$sortorder])) {
 888                  $sortorder++;
 889              }
 890              $topcats[$sortorder] = $cat;
 891              continue;
 892          }
 893          if (!isset($allcats[$cat->parent])) {
 894              $brokencats[] = $cat;
 895              continue;
 896          }
 897          if (!isset($allcats[$cat->parent]->children)) {
 898              $allcats[$cat->parent]->children = array();
 899          }
 900          while(isset($allcats[$cat->parent]->children[$sortorder])) {
 901              $sortorder++;
 902          }
 903          $allcats[$cat->parent]->children[$sortorder] = $cat;
 904      }
 905      unset($allcats);
 906  
 907      // add broken cats to category tree
 908      if ($brokencats) {
 909          $defaultcat = reset($topcats);
 910          foreach ($brokencats as $cat) {
 911              $topcats[] = $cat;
 912          }
 913      }
 914  
 915      // now walk recursively the tree and fix any problems found
 916      $sortorder = 0;
 917      $fixcontexts = array();
 918      if (_fix_course_cats($topcats, $sortorder, 0, 0, '', $fixcontexts)) {
 919          $cacheevents['changesincoursecat'] = true;
 920      }
 921  
 922      // detect if there are "multiple" frontpage courses and fix them if needed
 923      $frontcourses = $DB->get_records('course', array('category'=>0), 'id');
 924      if (count($frontcourses) > 1) {
 925          if (isset($frontcourses[SITEID])) {
 926              $frontcourse = $frontcourses[SITEID];
 927              unset($frontcourses[SITEID]);
 928          } else {
 929              $frontcourse = array_shift($frontcourses);
 930          }
 931          $defaultcat = reset($topcats);
 932          foreach ($frontcourses as $course) {
 933              $DB->set_field('course', 'category', $defaultcat->id, array('id'=>$course->id));
 934              $context = context_course::instance($course->id);
 935              $fixcontexts[$context->id] = $context;
 936              $cacheevents['changesincourse'] = true;
 937          }
 938          unset($frontcourses);
 939      } else {
 940          $frontcourse = reset($frontcourses);
 941      }
 942  
 943      // now fix the paths and depths in context table if needed
 944      if ($fixcontexts) {
 945          foreach ($fixcontexts as $fixcontext) {
 946              $fixcontext->reset_paths(false);
 947          }
 948          context_helper::build_all_paths(false);
 949          unset($fixcontexts);
 950          $cacheevents['changesincourse'] = true;
 951          $cacheevents['changesincoursecat'] = true;
 952      }
 953  
 954      // release memory
 955      unset($topcats);
 956      unset($brokencats);
 957      unset($fixcontexts);
 958  
 959      // fix frontpage course sortorder
 960      if ($frontcourse->sortorder != 1) {
 961          $DB->set_field('course', 'sortorder', 1, array('id'=>$frontcourse->id));
 962          $cacheevents['changesincourse'] = true;
 963      }
 964  
 965      // now fix the course counts in category records if needed
 966      $sql = "SELECT cc.id, cc.coursecount, COUNT(c.id) AS newcount
 967                FROM {course_categories} cc
 968                LEFT JOIN {course} c ON c.category = cc.id
 969            GROUP BY cc.id, cc.coursecount
 970              HAVING cc.coursecount <> COUNT(c.id)";
 971  
 972      if ($updatecounts = $DB->get_records_sql($sql)) {
 973          // categories with more courses than MAX_COURSES_IN_CATEGORY
 974          $categories = array();
 975          foreach ($updatecounts as $cat) {
 976              $cat->coursecount = $cat->newcount;
 977              if ($cat->coursecount >= MAX_COURSES_IN_CATEGORY) {
 978                  $categories[] = $cat->id;
 979              }
 980              unset($cat->newcount);
 981              $DB->update_record_raw('course_categories', $cat, true);
 982          }
 983          if (!empty($categories)) {
 984              $str = implode(', ', $categories);
 985              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);
 986          }
 987          $cacheevents['changesincoursecat'] = true;
 988      }
 989  
 990      // now make sure that sortorders in course table are withing the category sortorder ranges
 991      $sql = "SELECT DISTINCT cc.id, cc.sortorder
 992                FROM {course_categories} cc
 993                JOIN {course} c ON c.category = cc.id
 994               WHERE c.sortorder < cc.sortorder OR c.sortorder > cc.sortorder + ".MAX_COURSES_IN_CATEGORY;
 995  
 996      if ($fixcategories = $DB->get_records_sql($sql)) {
 997          //fix the course sortorder ranges
 998          foreach ($fixcategories as $cat) {
 999              $sql = "UPDATE {course}
1000                         SET sortorder = ".$DB->sql_modulo('sortorder', MAX_COURSES_IN_CATEGORY)." + ?
1001                       WHERE category = ?";
1002              $DB->execute($sql, array($cat->sortorder, $cat->id));
1003          }
1004          $cacheevents['changesincoursecat'] = true;
1005      }
1006      unset($fixcategories);
1007  
1008      // categories having courses with sortorder duplicates or having gaps in sortorder
1009      $sql = "SELECT DISTINCT c1.category AS id , cc.sortorder
1010                FROM {course} c1
1011                JOIN {course} c2 ON c1.sortorder = c2.sortorder
1012                JOIN {course_categories} cc ON (c1.category = cc.id)
1013               WHERE c1.id <> c2.id";
1014      $fixcategories = $DB->get_records_sql($sql);
1015  
1016      $sql = "SELECT cc.id, cc.sortorder, cc.coursecount, MAX(c.sortorder) AS maxsort, MIN(c.sortorder) AS minsort
1017                FROM {course_categories} cc
1018                JOIN {course} c ON c.category = cc.id
1019            GROUP BY cc.id, cc.sortorder, cc.coursecount
1020              HAVING (MAX(c.sortorder) <>  cc.sortorder + cc.coursecount) OR (MIN(c.sortorder) <>  cc.sortorder + 1)";
1021      $gapcategories = $DB->get_records_sql($sql);
1022  
1023      foreach ($gapcategories as $cat) {
1024          if (isset($fixcategories[$cat->id])) {
1025              // duplicates detected already
1026  
1027          } else if ($cat->minsort == $cat->sortorder and $cat->maxsort == $cat->sortorder + $cat->coursecount - 1) {
1028              // easy - new course inserted with sortorder 0, the rest is ok
1029              $sql = "UPDATE {course}
1030                         SET sortorder = sortorder + 1
1031                       WHERE category = ?";
1032              $DB->execute($sql, array($cat->id));
1033  
1034          } else {
1035              // it needs full resorting
1036              $fixcategories[$cat->id] = $cat;
1037          }
1038          $cacheevents['changesincourse'] = true;
1039      }
1040      unset($gapcategories);
1041  
1042      // fix course sortorders in problematic categories only
1043      foreach ($fixcategories as $cat) {
1044          $i = 1;
1045          $courses = $DB->get_records('course', array('category'=>$cat->id), 'sortorder ASC, id DESC', 'id, sortorder');
1046          foreach ($courses as $course) {
1047              if ($course->sortorder != $cat->sortorder + $i) {
1048                  $course->sortorder = $cat->sortorder + $i;
1049                  $DB->update_record_raw('course', $course, true);
1050                  $cacheevents['changesincourse'] = true;
1051              }
1052              $i++;
1053          }
1054      }
1055  
1056      // advise all caches that need to be rebuilt
1057      foreach (array_keys($cacheevents) as $event) {
1058          cache_helper::purge_by_event($event);
1059      }
1060  }
1061  
1062  /**
1063   * Internal recursive category verification function, do not use directly!
1064   *
1065   * @todo Document the arguments of this function better
1066   *
1067   * @global object
1068   * @uses MAX_COURSES_IN_CATEGORY
1069   * @uses CONTEXT_COURSECAT
1070   * @param array $children
1071   * @param int $sortorder
1072   * @param string $parent
1073   * @param int $depth
1074   * @param string $path
1075   * @param array $fixcontexts
1076   * @return bool if changes were made
1077   */
1078  function _fix_course_cats($children, &$sortorder, $parent, $depth, $path, &$fixcontexts) {
1079      global $DB;
1080  
1081      $depth++;
1082      $changesmade = false;
1083  
1084      foreach ($children as $cat) {
1085          $sortorder = $sortorder + MAX_COURSES_IN_CATEGORY;
1086          $update = false;
1087          if ($parent != $cat->parent or $depth != $cat->depth or $path.'/'.$cat->id != $cat->path) {
1088              $cat->parent = $parent;
1089              $cat->depth  = $depth;
1090              $cat->path   = $path.'/'.$cat->id;
1091              $update = true;
1092  
1093              // make sure context caches are rebuild and dirty contexts marked
1094              $context = context_coursecat::instance($cat->id);
1095              $fixcontexts[$context->id] = $context;
1096          }
1097          if ($cat->sortorder != $sortorder) {
1098              $cat->sortorder = $sortorder;
1099              $update = true;
1100          }
1101          if ($update) {
1102              $DB->update_record('course_categories', $cat, true);
1103              $changesmade = true;
1104          }
1105          if (isset($cat->children)) {
1106              if (_fix_course_cats($cat->children, $sortorder, $cat->id, $cat->depth, $cat->path, $fixcontexts)) {
1107                  $changesmade = true;
1108              }
1109          }
1110      }
1111      return $changesmade;
1112  }
1113  
1114  /**
1115   * List of remote courses that a user has access to via MNET.
1116   * Works only on the IDP
1117   *
1118   * @global object
1119   * @global object
1120   * @param int @userid The user id to get remote courses for
1121   * @return array Array of {@link $COURSE} of course objects
1122   */
1123  function get_my_remotecourses($userid=0) {
1124      global $DB, $USER;
1125  
1126      if (empty($userid)) {
1127          $userid = $USER->id;
1128      }
1129  
1130      // we can not use SELECT DISTINCT + text field (summary) because of MS SQL and Oracle, subselect used therefore
1131      $sql = "SELECT c.id, c.remoteid, c.shortname, c.fullname,
1132                     c.hostid, c.summary, c.summaryformat, c.categoryname AS cat_name,
1133                     h.name AS hostname
1134                FROM {mnetservice_enrol_courses} c
1135                JOIN (SELECT DISTINCT hostid, remotecourseid
1136                        FROM {mnetservice_enrol_enrolments}
1137                       WHERE userid = ?
1138                     ) e ON (e.hostid = c.hostid AND e.remotecourseid = c.remoteid)
1139                JOIN {mnet_host} h ON h.id = c.hostid";
1140  
1141      return $DB->get_records_sql($sql, array($userid));
1142  }
1143  
1144  /**
1145   * List of remote hosts that a user has access to via MNET.
1146   * Works on the SP
1147   *
1148   * @global object
1149   * @global object
1150   * @return array|bool Array of host objects or false
1151   */
1152  function get_my_remotehosts() {
1153      global $CFG, $USER;
1154  
1155      if ($USER->mnethostid == $CFG->mnet_localhost_id) {
1156          return false; // Return nothing on the IDP
1157      }
1158      if (!empty($USER->mnet_foreign_host_array) && is_array($USER->mnet_foreign_host_array)) {
1159          return $USER->mnet_foreign_host_array;
1160      }
1161      return false;
1162  }
1163  
1164  
1165  /**
1166   * Returns a menu of all available scales from the site as well as the given course
1167   *
1168   * @global object
1169   * @param int $courseid The id of the course as found in the 'course' table.
1170   * @return array
1171   */
1172  function get_scales_menu($courseid=0) {
1173      global $DB;
1174  
1175      $sql = "SELECT id, name
1176                FROM {scale}
1177               WHERE courseid = 0 or courseid = ?
1178            ORDER BY courseid ASC, name ASC";
1179      $params = array($courseid);
1180  
1181      return $scales = $DB->get_records_sql_menu($sql, $params);
1182  }
1183  
1184  /**
1185   * Increment standard revision field.
1186   *
1187   * The revision are based on current time and are incrementing.
1188   * There is a protection for runaway revisions, it may not go further than
1189   * one hour into future.
1190   *
1191   * The field has to be XMLDB_TYPE_INTEGER with size 10.
1192   *
1193   * @param string $table
1194   * @param string $field name of the field containing revision
1195   * @param string $select use empty string when updating all records
1196   * @param array $params optional select parameters
1197   */
1198  function increment_revision_number($table, $field, $select, array $params = null) {
1199      global $DB;
1200  
1201      $now = time();
1202      $sql = "UPDATE {{$table}}
1203                     SET $field = (CASE
1204                         WHEN $field IS NULL THEN $now
1205                         WHEN $field < $now THEN $now
1206                         WHEN $field > $now + 3600 THEN $now
1207                         ELSE $field + 1 END)";
1208      if ($select) {
1209          $sql = $sql . " WHERE $select";
1210      }
1211      $DB->execute($sql, $params);
1212  }
1213  
1214  
1215  /// MODULE FUNCTIONS /////////////////////////////////////////////////
1216  
1217  /**
1218   * Just gets a raw list of all modules in a course
1219   *
1220   * @global object
1221   * @param int $courseid The id of the course as found in the 'course' table.
1222   * @return array
1223   */
1224  function get_course_mods($courseid) {
1225      global $DB;
1226  
1227      if (empty($courseid)) {
1228          return false; // avoid warnings
1229      }
1230  
1231      return $DB->get_records_sql("SELECT cm.*, m.name as modname
1232                                     FROM {modules} m, {course_modules} cm
1233                                    WHERE cm.course = ? AND cm.module = m.id AND m.visible = 1",
1234                                  array($courseid)); // no disabled mods
1235  }
1236  
1237  
1238  /**
1239   * Given an id of a course module, finds the coursemodule description
1240   *
1241   * Please note that this function performs 1-2 DB queries. When possible use cached
1242   * course modinfo. For example get_fast_modinfo($courseorid)->get_cm($cmid)
1243   * See also {@link cm_info::get_course_module_record()}
1244   *
1245   * @global object
1246   * @param string $modulename name of module type, eg. resource, assignment,... (optional, slower and less safe if not specified)
1247   * @param int $cmid course module id (id in course_modules table)
1248   * @param int $courseid optional course id for extra validation
1249   * @param bool $sectionnum include relative section number (0,1,2 ...)
1250   * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
1251   *                        IGNORE_MULTIPLE means return first, ignore multiple records found(not recommended);
1252   *                        MUST_EXIST means throw exception if no record or multiple records found
1253   * @return stdClass
1254   */
1255  function get_coursemodule_from_id($modulename, $cmid, $courseid=0, $sectionnum=false, $strictness=IGNORE_MISSING) {
1256      global $DB;
1257  
1258      $params = array('cmid'=>$cmid);
1259  
1260      if (!$modulename) {
1261          if (!$modulename = $DB->get_field_sql("SELECT md.name
1262                                                   FROM {modules} md
1263                                                   JOIN {course_modules} cm ON cm.module = md.id
1264                                                  WHERE cm.id = :cmid", $params, $strictness)) {
1265              return false;
1266          }
1267      } else {
1268          if (!core_component::is_valid_plugin_name('mod', $modulename)) {
1269              throw new coding_exception('Invalid modulename parameter');
1270          }
1271      }
1272  
1273      $params['modulename'] = $modulename;
1274  
1275      $courseselect = "";
1276      $sectionfield = "";
1277      $sectionjoin  = "";
1278  
1279      if ($courseid) {
1280          $courseselect = "AND cm.course = :courseid";
1281          $params['courseid'] = $courseid;
1282      }
1283  
1284      if ($sectionnum) {
1285          $sectionfield = ", cw.section AS sectionnum";
1286          $sectionjoin  = "LEFT JOIN {course_sections} cw ON cw.id = cm.section";
1287      }
1288  
1289      $sql = "SELECT cm.*, m.name, md.name AS modname $sectionfield
1290                FROM {course_modules} cm
1291                     JOIN {modules} md ON md.id = cm.module
1292                     JOIN {".$modulename."} m ON m.id = cm.instance
1293                     $sectionjoin
1294               WHERE cm.id = :cmid AND md.name = :modulename
1295                     $courseselect";
1296  
1297      return $DB->get_record_sql($sql, $params, $strictness);
1298  }
1299  
1300  /**
1301   * Given an instance number of a module, finds the coursemodule description
1302   *
1303   * Please note that this function performs DB query. When possible use cached course
1304   * modinfo. For example get_fast_modinfo($courseorid)->instances[$modulename][$instance]
1305   * See also {@link cm_info::get_course_module_record()}
1306   *
1307   * @global object
1308   * @param string $modulename name of module type, eg. resource, assignment,...
1309   * @param int $instance module instance number (id in resource, assignment etc. table)
1310   * @param int $courseid optional course id for extra validation
1311   * @param bool $sectionnum include relative section number (0,1,2 ...)
1312   * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
1313   *                        IGNORE_MULTIPLE means return first, ignore multiple records found(not recommended);
1314   *                        MUST_EXIST means throw exception if no record or multiple records found
1315   * @return stdClass
1316   */
1317  function get_coursemodule_from_instance($modulename, $instance, $courseid=0, $sectionnum=false, $strictness=IGNORE_MISSING) {
1318      global $DB;
1319  
1320      if (!core_component::is_valid_plugin_name('mod', $modulename)) {
1321          throw new coding_exception('Invalid modulename parameter');
1322      }
1323  
1324      $params = array('instance'=>$instance, 'modulename'=>$modulename);
1325  
1326      $courseselect = "";
1327      $sectionfield = "";
1328      $sectionjoin  = "";
1329  
1330      if ($courseid) {
1331          $courseselect = "AND cm.course = :courseid";
1332          $params['courseid'] = $courseid;
1333      }
1334  
1335      if ($sectionnum) {
1336          $sectionfield = ", cw.section AS sectionnum";
1337          $sectionjoin  = "LEFT JOIN {course_sections} cw ON cw.id = cm.section";
1338      }
1339  
1340      $sql = "SELECT cm.*, m.name, md.name AS modname $sectionfield
1341                FROM {course_modules} cm
1342                     JOIN {modules} md ON md.id = cm.module
1343                     JOIN {".$modulename."} m ON m.id = cm.instance
1344                     $sectionjoin
1345               WHERE m.id = :instance AND md.name = :modulename
1346                     $courseselect";
1347  
1348      return $DB->get_record_sql($sql, $params, $strictness);
1349  }
1350  
1351  /**
1352   * Returns all course modules of given activity in course
1353   *
1354   * @param string $modulename The module name (forum, quiz, etc.)
1355   * @param int $courseid The course id to get modules for
1356   * @param string $extrafields extra fields starting with m.
1357   * @return array Array of results
1358   */
1359  function get_coursemodules_in_course($modulename, $courseid, $extrafields='') {
1360      global $DB;
1361  
1362      if (!core_component::is_valid_plugin_name('mod', $modulename)) {
1363          throw new coding_exception('Invalid modulename parameter');
1364      }
1365  
1366      if (!empty($extrafields)) {
1367          $extrafields = ", $extrafields";
1368      }
1369      $params = array();
1370      $params['courseid'] = $courseid;
1371      $params['modulename'] = $modulename;
1372  
1373  
1374      return $DB->get_records_sql("SELECT cm.*, m.name, md.name as modname $extrafields
1375                                     FROM {course_modules} cm, {modules} md, {".$modulename."} m
1376                                    WHERE cm.course = :courseid AND
1377                                          cm.instance = m.id AND
1378                                          md.name = :modulename AND
1379                                          md.id = cm.module", $params);
1380  }
1381  
1382  /**
1383   * Returns an array of all the active instances of a particular module in given courses, sorted in the order they are defined
1384   *
1385   * Returns an array of all the active instances of a particular
1386   * module in given courses, sorted in the order they are defined
1387   * in the course. Returns an empty array on any errors.
1388   *
1389   * The returned objects includle the columns cw.section, cm.visible,
1390   * cm.groupmode, and cm.groupingid, and are indexed by cm.id.
1391   *
1392   * @global object
1393   * @global object
1394   * @param string $modulename The name of the module to get instances for
1395   * @param array $courses an array of course objects.
1396   * @param int $userid
1397   * @param int $includeinvisible
1398   * @return array of module instance objects, including some extra fields from the course_modules
1399   *          and course_sections tables, or an empty array if an error occurred.
1400   */
1401  function get_all_instances_in_courses($modulename, $courses, $userid=NULL, $includeinvisible=false) {
1402      global $CFG, $DB;
1403  
1404      if (!core_component::is_valid_plugin_name('mod', $modulename)) {
1405          throw new coding_exception('Invalid modulename parameter');
1406      }
1407  
1408      $outputarray = array();
1409  
1410      if (empty($courses) || !is_array($courses) || count($courses) == 0) {
1411          return $outputarray;
1412      }
1413  
1414      list($coursessql, $params) = $DB->get_in_or_equal(array_keys($courses), SQL_PARAMS_NAMED, 'c0');
1415      $params['modulename'] = $modulename;
1416  
1417      if (!$rawmods = $DB->get_records_sql("SELECT cm.id AS coursemodule, m.*, cw.section, cm.visible AS visible,
1418                                                   cm.groupmode, cm.groupingid
1419                                              FROM {course_modules} cm, {course_sections} cw, {modules} md,
1420                                                   {".$modulename."} m
1421                                             WHERE cm.course $coursessql AND
1422                                                   cm.instance = m.id AND
1423                                                   cm.section = cw.id AND
1424                                                   md.name = :modulename AND
1425                                                   md.id = cm.module", $params)) {
1426          return $outputarray;
1427      }
1428  
1429      foreach ($courses as $course) {
1430          $modinfo = get_fast_modinfo($course, $userid);
1431  
1432          if (empty($modinfo->instances[$modulename])) {
1433              continue;
1434          }
1435  
1436          foreach ($modinfo->instances[$modulename] as $cm) {
1437              if (!$includeinvisible and !$cm->uservisible) {
1438                  continue;
1439              }
1440              if (!isset($rawmods[$cm->id])) {
1441                  continue;
1442              }
1443              $instance = $rawmods[$cm->id];
1444              if (!empty($cm->extra)) {
1445                  $instance->extra = $cm->extra;
1446              }
1447              $outputarray[] = $instance;
1448          }
1449      }
1450  
1451      return $outputarray;
1452  }
1453  
1454  /**
1455   * Returns an array of all the active instances of a particular module in a given course,
1456   * sorted in the order they are defined.
1457   *
1458   * Returns an array of all the active instances of a particular
1459   * module in a given course, sorted in the order they are defined
1460   * in the course. Returns an empty array on any errors.
1461   *
1462   * The returned objects includle the columns cw.section, cm.visible,
1463   * cm.groupmode, and cm.groupingid, and are indexed by cm.id.
1464   *
1465   * Simply calls {@link all_instances_in_courses()} with a single provided course
1466   *
1467   * @param string $modulename The name of the module to get instances for
1468   * @param object $course The course obect.
1469   * @return array of module instance objects, including some extra fields from the course_modules
1470   *          and course_sections tables, or an empty array if an error occurred.
1471   * @param int $userid
1472   * @param int $includeinvisible
1473   */
1474  function get_all_instances_in_course($modulename, $course, $userid=NULL, $includeinvisible=false) {
1475      return get_all_instances_in_courses($modulename, array($course->id => $course), $userid, $includeinvisible);
1476  }
1477  
1478  
1479  /**
1480   * Determine whether a module instance is visible within a course
1481   *
1482   * Given a valid module object with info about the id and course,
1483   * and the module's type (eg "forum") returns whether the object
1484   * is visible or not according to the 'eye' icon only.
1485   *
1486   * NOTE: This does NOT take into account visibility to a particular user.
1487   * To get visibility access for a specific user, use get_fast_modinfo, get a
1488   * cm_info object from this, and check the ->uservisible property; or use
1489   * the \core_availability\info_module::is_user_visible() static function.
1490   *
1491   * @global object
1492  
1493   * @param $moduletype Name of the module eg 'forum'
1494   * @param $module Object which is the instance of the module
1495   * @return bool Success
1496   */
1497  function instance_is_visible($moduletype, $module) {
1498      global $DB;
1499  
1500      if (!empty($module->id)) {
1501          $params = array('courseid'=>$module->course, 'moduletype'=>$moduletype, 'moduleid'=>$module->id);
1502          if ($records = $DB->get_records_sql("SELECT cm.instance, cm.visible, cm.groupingid, cm.id, cm.course
1503                                                 FROM {course_modules} cm, {modules} m
1504                                                WHERE cm.course = :courseid AND
1505                                                      cm.module = m.id AND
1506                                                      m.name = :moduletype AND
1507                                                      cm.instance = :moduleid", $params)) {
1508  
1509              foreach ($records as $record) { // there should only be one - use the first one
1510                  return $record->visible;
1511              }
1512          }
1513      }
1514      return true;  // visible by default!
1515  }
1516  
1517  
1518  /// LOG FUNCTIONS /////////////////////////////////////////////////////
1519  
1520  /**
1521   * Get instance of log manager.
1522   *
1523   * @param bool $forcereload
1524   * @return \core\log\manager
1525   */
1526  function get_log_manager($forcereload = false) {
1527      /** @var \core\log\manager $singleton */
1528      static $singleton = null;
1529  
1530      if ($forcereload and isset($singleton)) {
1531          $singleton->dispose();
1532          $singleton = null;
1533      }
1534  
1535      if (isset($singleton)) {
1536          return $singleton;
1537      }
1538  
1539      $classname = '\tool_log\log\manager';
1540      if (defined('LOG_MANAGER_CLASS')) {
1541          $classname = LOG_MANAGER_CLASS;
1542      }
1543  
1544      if (!class_exists($classname)) {
1545          if (!empty($classname)) {
1546              debugging("Cannot find log manager class '$classname'.", DEBUG_DEVELOPER);
1547          }
1548          $classname = '\core\log\dummy_manager';
1549      }
1550  
1551      $singleton = new $classname();
1552      return $singleton;
1553  }
1554  
1555  /**
1556   * Add an entry to the config log table.
1557   *
1558   * These are "action" focussed rather than web server hits,
1559   * and provide a way to easily reconstruct changes to Moodle configuration.
1560   *
1561   * @package core
1562   * @category log
1563   * @global moodle_database $DB
1564   * @global stdClass $USER
1565   * @param    string  $name     The name of the configuration change action
1566                                 For example 'filter_active' when activating or deactivating a filter
1567   * @param    string  $oldvalue The config setting's previous value
1568   * @param    string  $value    The config setting's new value
1569   * @param    string  $plugin   Plugin name, for example a filter name when changing filter configuration
1570   * @return void
1571   */
1572  function add_to_config_log($name, $oldvalue, $value, $plugin) {
1573      global $USER, $DB;
1574  
1575      $log = new stdClass();
1576      $log->userid       = during_initial_install() ? 0 :$USER->id; // 0 as user id during install
1577      $log->timemodified = time();
1578      $log->name         = $name;
1579      $log->oldvalue  = $oldvalue;
1580      $log->value     = $value;
1581      $log->plugin    = $plugin;
1582      $DB->insert_record('config_log', $log);
1583  }
1584  
1585  /**
1586   * Store user last access times - called when use enters a course or site
1587   *
1588   * @package core
1589   * @category log
1590   * @global stdClass $USER
1591   * @global stdClass $CFG
1592   * @global moodle_database $DB
1593   * @uses LASTACCESS_UPDATE_SECS
1594   * @uses SITEID
1595   * @param int $courseid  empty courseid means site
1596   * @return void
1597   */
1598  function user_accesstime_log($courseid=0) {
1599      global $USER, $CFG, $DB;
1600  
1601      if (!isloggedin() or \core\session\manager::is_loggedinas()) {
1602          // no access tracking
1603          return;
1604      }
1605  
1606      if (isguestuser()) {
1607          // Do not update guest access times/ips for performance.
1608          return;
1609      }
1610  
1611      if (empty($courseid)) {
1612          $courseid = SITEID;
1613      }
1614  
1615      $timenow = time();
1616  
1617  /// Store site lastaccess time for the current user
1618      if ($timenow - $USER->lastaccess > LASTACCESS_UPDATE_SECS) {
1619      /// Update $USER->lastaccess for next checks
1620          $USER->lastaccess = $timenow;
1621  
1622          $last = new stdClass();
1623          $last->id         = $USER->id;
1624          $last->lastip     = getremoteaddr();
1625          $last->lastaccess = $timenow;
1626  
1627          $DB->update_record_raw('user', $last);
1628      }
1629  
1630      if ($courseid == SITEID) {
1631      ///  no user_lastaccess for frontpage
1632          return;
1633      }
1634  
1635  /// Store course lastaccess times for the current user
1636      if (empty($USER->currentcourseaccess[$courseid]) or ($timenow - $USER->currentcourseaccess[$courseid] > LASTACCESS_UPDATE_SECS)) {
1637  
1638          $lastaccess = $DB->get_field('user_lastaccess', 'timeaccess', array('userid'=>$USER->id, 'courseid'=>$courseid));
1639  
1640          if ($lastaccess === false) {
1641              // Update course lastaccess for next checks
1642              $USER->currentcourseaccess[$courseid] = $timenow;
1643  
1644              $last = new stdClass();
1645              $last->userid     = $USER->id;
1646              $last->courseid   = $courseid;
1647              $last->timeaccess = $timenow;
1648              try {
1649                  $DB->insert_record_raw('user_lastaccess', $last, false);
1650              } catch (dml_write_exception $e) {
1651                  // During a race condition we can fail to find the data, then it appears.
1652                  // If we still can't find it, rethrow the exception.
1653                  $lastaccess = $DB->get_field('user_lastaccess', 'timeaccess', array('userid' => $USER->id,
1654                                                                                      'courseid' => $courseid));
1655                  if ($lastaccess === false) {
1656                      throw $e;
1657                  }
1658                  // If we did find it, the race condition was true and another thread has inserted the time for us.
1659                  // We can just continue without having to do anything.
1660              }
1661  
1662          } else if ($timenow - $lastaccess <  LASTACCESS_UPDATE_SECS) {
1663              // no need to update now, it was updated recently in concurrent login ;-)
1664  
1665          } else {
1666              // Update course lastaccess for next checks
1667              $USER->currentcourseaccess[$courseid] = $timenow;
1668  
1669              $DB->set_field('user_lastaccess', 'timeaccess', $timenow, array('userid'=>$USER->id, 'courseid'=>$courseid));
1670          }
1671      }
1672  }
1673  
1674  /**
1675   * Select all log records based on SQL criteria
1676   *
1677   * @package core
1678   * @category log
1679   * @global moodle_database $DB
1680   * @param string $select SQL select criteria
1681   * @param array $params named sql type params
1682   * @param string $order SQL order by clause to sort the records returned
1683   * @param string $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set)
1684   * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set)
1685   * @param int $totalcount Passed in by reference.
1686   * @return array
1687   */
1688  function get_logs($select, array $params=null, $order='l.time DESC', $limitfrom='', $limitnum='', &$totalcount) {
1689      global $DB;
1690  
1691      if ($order) {
1692          $order = "ORDER BY $order";
1693      }
1694  
1695      $selectsql = "";
1696      $countsql  = "";
1697  
1698      if ($select) {
1699          $select = "WHERE $select";
1700      }
1701  
1702      $sql = "SELECT COUNT(*)
1703                FROM {log} l
1704             $select";
1705  
1706      $totalcount = $DB->count_records_sql($sql, $params);
1707      $allnames = get_all_user_name_fields(true, 'u');
1708      $sql = "SELECT l.*, $allnames, u.picture
1709                FROM {log} l
1710                LEFT JOIN {user} u ON l.userid = u.id
1711             $select
1712              $order";
1713  
1714      return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum) ;
1715  }
1716  
1717  
1718  /**
1719   * Select all log records for a given course and user
1720   *
1721   * @package core
1722   * @category log
1723   * @global moodle_database $DB
1724   * @uses DAYSECS
1725   * @param int $userid The id of the user as found in the 'user' table.
1726   * @param int $courseid The id of the course as found in the 'course' table.
1727   * @param string $coursestart unix timestamp representing course start date and time.
1728   * @return array
1729   */
1730  function get_logs_usercourse($userid, $courseid, $coursestart) {
1731      global $DB;
1732  
1733      $params = array();
1734  
1735      $courseselect = '';
1736      if ($courseid) {
1737          $courseselect = "AND course = :courseid";
1738          $params['courseid'] = $courseid;
1739      }
1740      $params['userid'] = $userid;
1741      // We have to sanitize this param ourselves here instead of relying on DB.
1742      // Postgres complains if you use name parameter or column alias in GROUP BY.
1743      // See MDL-27696 and 51c3e85 for details.
1744      $coursestart = (int)$coursestart;
1745  
1746      return $DB->get_records_sql("SELECT FLOOR((time - $coursestart)/". DAYSECS .") AS day, COUNT(*) AS num
1747                                     FROM {log}
1748                                    WHERE userid = :userid
1749                                          AND time > $coursestart $courseselect
1750                                 GROUP BY FLOOR((time - $coursestart)/". DAYSECS .")", $params);
1751  }
1752  
1753  /**
1754   * Select all log records for a given course, user, and day
1755   *
1756   * @package core
1757   * @category log
1758   * @global moodle_database $DB
1759   * @uses HOURSECS
1760   * @param int $userid The id of the user as found in the 'user' table.
1761   * @param int $courseid The id of the course as found in the 'course' table.
1762   * @param string $daystart unix timestamp of the start of the day for which the logs needs to be retrived
1763   * @return array
1764   */
1765  function get_logs_userday($userid, $courseid, $daystart) {
1766      global $DB;
1767  
1768      $params = array('userid'=>$userid);
1769  
1770      $courseselect = '';
1771      if ($courseid) {
1772          $courseselect = "AND course = :courseid";
1773          $params['courseid'] = $courseid;
1774      }
1775      $daystart = (int)$daystart; // note: unfortunately pg complains if you use name parameter or column alias in GROUP BY
1776  
1777      return $DB->get_records_sql("SELECT FLOOR((time - $daystart)/". HOURSECS .") AS hour, COUNT(*) AS num
1778                                     FROM {log}
1779                                    WHERE userid = :userid
1780                                          AND time > $daystart $courseselect
1781                                 GROUP BY FLOOR((time - $daystart)/". HOURSECS .") ", $params);
1782  }
1783  
1784  /// GENERAL HELPFUL THINGS  ///////////////////////////////////
1785  
1786  /**
1787   * Dumps a given object's information for debugging purposes
1788   *
1789   * When used in a CLI script, the object's information is written to the standard
1790   * error output stream. When used in a web script, the object is dumped to a
1791   * pre-formatted block with the "notifytiny" CSS class.
1792   *
1793   * @param mixed $object The data to be printed
1794   * @return void output is echo'd
1795   */
1796  function print_object($object) {
1797  
1798      // we may need a lot of memory here
1799      raise_memory_limit(MEMORY_EXTRA);
1800  
1801      if (CLI_SCRIPT) {
1802          fwrite(STDERR, print_r($object, true));
1803          fwrite(STDERR, PHP_EOL);
1804      } else {
1805          echo html_writer::tag('pre', s(print_r($object, true)), array('class' => 'notifytiny'));
1806      }
1807  }
1808  
1809  /**
1810   * This function is the official hook inside XMLDB stuff to delegate its debug to one
1811   * external function.
1812   *
1813   * Any script can avoid calls to this function by defining XMLDB_SKIP_DEBUG_HOOK before
1814   * using XMLDB classes. Obviously, also, if this function doesn't exist, it isn't invoked ;-)
1815   *
1816   * @uses DEBUG_DEVELOPER
1817   * @param string $message string contains the error message
1818   * @param object $object object XMLDB object that fired the debug
1819   */
1820  function xmldb_debug($message, $object) {
1821  
1822      debugging($message, DEBUG_DEVELOPER);
1823  }
1824  
1825  /**
1826   * @global object
1827   * @uses CONTEXT_COURSECAT
1828   * @return boolean Whether the user can create courses in any category in the system.
1829   */
1830  function user_can_create_courses() {
1831      global $DB;
1832      $catsrs = $DB->get_recordset('course_categories');
1833      foreach ($catsrs as $cat) {
1834          if (has_capability('moodle/course:create', context_coursecat::instance($cat->id))) {
1835              $catsrs->close();
1836              return true;
1837          }
1838      }
1839      $catsrs->close();
1840      return false;
1841  }
1842  
1843  /**
1844   * This method can update the values in mulitple database rows for a colum with
1845   * a unique index, without violating that constraint.
1846   *
1847   * Suppose we have a table with a unique index on (otherid, sortorder), and
1848   * for a particular value of otherid, we want to change all the sort orders.
1849   * You have to do this carefully or you will violate the unique index at some time.
1850   * This method takes care of the details for you.
1851   *
1852   * Note that, it is the responsibility of the caller to make sure that the
1853   * requested rename is legal. For example, if you ask for [1 => 2, 2 => 2]
1854   * then you will get a unique key violation error from the database.
1855   *
1856   * @param string $table The database table to modify.
1857   * @param string $field the field that contains the values we are going to change.
1858   * @param array $newvalues oldvalue => newvalue how to change the values.
1859   *      E.g. [1 => 4, 2 => 1, 3 => 3, 4 => 2].
1860   * @param array $otherconditions array fieldname => requestedvalue extra WHERE clause
1861   *      conditions to restrict which rows are affected. E.g. array('otherid' => 123).
1862   * @param int $unusedvalue (defaults to -1) a value that is never used in $ordercol.
1863   */
1864  function update_field_with_unique_index($table, $field, array $newvalues,
1865          array $otherconditions, $unusedvalue = -1) {
1866      global $DB;
1867      $safechanges = decompose_update_into_safe_changes($newvalues, $unusedvalue);
1868  
1869      $transaction = $DB->start_delegated_transaction();
1870      foreach ($safechanges as $change) {
1871          list($from, $to) = $change;
1872          $otherconditions[$field] = $from;
1873          $DB->set_field($table, $field, $to, $otherconditions);
1874      }
1875      $transaction->allow_commit();
1876  }
1877  
1878  /**
1879   * Helper used by {@link update_field_with_unique_index()}. Given a desired
1880   * set of changes, break them down into single udpates that can be done one at
1881   * a time without breaking any unique index constraints.
1882   *
1883   * Suppose the input is array(1 => 2, 2 => 1) and -1. Then the output will be
1884   * array (array(1, -1), array(2, 1), array(-1, 2)). This function solves this
1885   * problem in the general case, not just for simple swaps. The unit tests give
1886   * more examples.
1887   *
1888   * Note that, it is the responsibility of the caller to make sure that the
1889   * requested rename is legal. For example, if you ask for something impossible
1890   * like array(1 => 2, 2 => 2) then the results are undefined. (You will probably
1891   * get a unique key violation error from the database later.)
1892   *
1893   * @param array $newvalues The desired re-ordering.
1894   *      E.g. array(1 => 4, 2 => 1, 3 => 3, 4 => 2).
1895   * @param int $unusedvalue A value that is not currently used.
1896   * @return array A safe way to perform the re-order. An array of two-element
1897   *      arrays array($from, $to).
1898   *      E.g. array(array(1, -1), array(2, 1), array(4, 2), array(-1, 4)).
1899   */
1900  function decompose_update_into_safe_changes(array $newvalues, $unusedvalue) {
1901      $nontrivialmap = array();
1902      foreach ($newvalues as $from => $to) {
1903          if ($from == $unusedvalue || $to == $unusedvalue) {
1904              throw new \coding_exception('Supposedly unused value ' . $unusedvalue . ' is actually used!');
1905          }
1906          if ($from != $to) {
1907              $nontrivialmap[$from] = $to;
1908          }
1909      }
1910  
1911      if (empty($nontrivialmap)) {
1912          return array();
1913      }
1914  
1915      // First we deal with all renames that are not part of cycles.
1916      // This bit is O(n^2) and it ought to be possible to do better,
1917      // but it does not seem worth the effort.
1918      $safechanges = array();
1919      $nontrivialmapchanged = true;
1920      while ($nontrivialmapchanged) {
1921          $nontrivialmapchanged = false;
1922  
1923          foreach ($nontrivialmap as $from => $to) {
1924              if (array_key_exists($to, $nontrivialmap)) {
1925                  continue; // Cannot currenly do this rename.
1926              }
1927              // Is safe to do this rename now.
1928              $safechanges[] = array($from, $to);
1929              unset($nontrivialmap[$from]);
1930              $nontrivialmapchanged = true;
1931          }
1932      }
1933  
1934      // Are we done?
1935      if (empty($nontrivialmap)) {
1936          return $safechanges;
1937      }
1938  
1939      // Now what is left in $nontrivialmap must be a permutation,
1940      // which must be a combination of disjoint cycles. We need to break them.
1941      while (!empty($nontrivialmap)) {
1942          // Extract the first cycle.
1943          reset($nontrivialmap);
1944          $current = $cyclestart = key($nontrivialmap);
1945          $cycle = array();
1946          do {
1947              $cycle[] = $current;
1948              $next = $nontrivialmap[$current];
1949              unset($nontrivialmap[$current]);
1950              $current = $next;
1951          } while ($current != $cyclestart);
1952  
1953          // Now convert it to a sequence of safe renames by using a temp.
1954          $safechanges[] = array($cyclestart, $unusedvalue);
1955          $cycle[0] = $unusedvalue;
1956          $to = $cyclestart;
1957          while ($from = array_pop($cycle)) {
1958              $safechanges[] = array($from, $to);
1959              $to = $from;
1960          }
1961      }
1962  
1963      return $safechanges;
1964  }


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