06b7c56b17182a9483fef0d48ef22c767a28c219
[moodle.git] / lib / datalib.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * Library of functions for database manipulation.
20  *
21  * Other main libraries:
22  * - weblib.php - functions that produce web output
23  * - moodlelib.php - general-purpose Moodle functions
24  *
25  * @package   moodlecore
26  * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
27  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28  */
30 /**
31  * The maximum courses in a category
32  * MAX_COURSES_IN_CATEGORY * MAX_COURSE_CATEGORIES must not be more than max integer!
33  */
34 define('MAX_COURSES_IN_CATEGORY', 10000);
36 /**
37   * The maximum number of course categories
38   * MAX_COURSES_IN_CATEGORY * MAX_COURSE_CATEGORIES must not be more than max integer!
39   */
40 define('MAX_COURSE_CATEGORIES', 10000);
42 /**
43  * Number of seconds to wait before updating lastaccess information in DB.
44  */
45 define('LASTACCESS_UPDATE_SECS', 60);
47 /**
48  * Returns $user object of the main admin user
49  * primary admin = admin with lowest role_assignment id among admins
50  *
51  * @global object
52  * @static object $myadmin
53  * @return object An associative array representing the admin user.
54  */
55 function get_admin() {
56     static $mainadmin = null;
58     if (!isset($mainadmin)) {
59         if (! $admins = get_admins()) {
60             return false;
61         }
62         //TODO: add some admin setting for specifying of THE main admin
63         //      for now return the first assigned admin
64         $mainadmin = reset($admins);
65     }
66     return $mainadmin;
67 }
69 /**
70  * Returns list of all admins, using 1 DB query
71  *
72  * @return array
73  */
74 function get_admins() {
75     global $DB, $CFG;
77     $sql = "SELECT u.*
78               FROM {user} u
79              WHERE u.deleted = 0 AND u.id IN ($CFG->siteadmins)";
81     return $DB->get_records_sql($sql);
82 }
84 /**
85  * Get all of the courses in a given meta course
86  *
87  * @global object
88  * @param int $metacourseid The metacourse id
89  * @return array
90  */
91 function get_courses_in_metacourse($metacourseid) {
92     global $DB;
94     $sql = "SELECT c.id, c.shortname, c.fullname
95               FROM {course} c, {course_meta} mc
96              WHERE mc.parent_course = ? AND mc.child_course = c.id
97           ORDER BY c.shortname";
98     $params = array($metacourseid);
100     return $DB->get_records_sql($sql, $params);
103 /**
104  * @todo Document this function
105  *
106  * @global object
107  * @uses SITEID
108  * @param int $metacourseid
109  * @return array
110  */
111 function get_courses_notin_metacourse($metacourseid) {
112     global $DB;
114     if ($alreadycourses = get_courses_in_metacourse($metacourseid)) {
115         $alreadycourses = implode(',',array_keys($alreadycourses));
116         $alreadycourses = "AND c.id NOT IN ($alreadycourses)";
117     } else {
118         $alreadycourses = "";
119     }
121     $sql = "SELECT c.id,c.shortname,c.fullname
122               FROM {course} c
123              WHERE c.id != ? and c.id != ".SITEID." and c.metacourse != 1
124                    $alreadycourses
125           ORDER BY c.shortname";
126     $params = array($metacourseid);
128     return $DB->get_records_sql($sql, $params);
131 /**
132  * @todo Document this function
133  *
134  * This function is nearly identical to {@link get_courses_notin_metacourse()}
135  *
136  * @global object
137  * @uses SITEID
138  * @param int $metacourseid
139  * @return int The count
140  */
141 function count_courses_notin_metacourse($metacourseid) {
142     global $DB;
144     if ($alreadycourses = get_courses_in_metacourse($metacourseid)) {
145         $alreadycourses = implode(',',array_keys($alreadycourses));
146         $alreadycourses = "AND c.id NOT IN ($alreadycourses)";
147     } else {
148         $alreadycourses = "";
149     }
151     $sql = "SELECT COUNT(c.id)
152               FROM {course} c
153              WHERE c.id != ? and c.id != ".SITEID." and c.metacourse != 1
154                    $alreadycourses";
155     $params = array($metacourseid);
157     return $DB->count_records_sql($sql, $params);
160 /**
161  * Search through course users
162  *
163  * If $coursid specifies the site course then this function searches
164  * through all undeleted and confirmed users
165  *
166  * @global object
167  * @uses SITEID
168  * @uses SQL_PARAMS_NAMED
169  * @uses CONTEXT_COURSE
170  * @param int $courseid The course in question.
171  * @param int $groupid The group in question.
172  * @param string $searchtext The string to search for
173  * @param string $sort A field to sort by
174  * @param array $exceptions A list of IDs to ignore, eg 2,4,5,8,9,10
175  * @return array
176  */
177 function search_users($courseid, $groupid, $searchtext, $sort='', array $exceptions=null) {
178     global $DB;
180     $LIKE      = $DB->sql_ilike();
181     $fullname  = $DB->sql_fullname('u.firstname', 'u.lastname');
183     if (!empty($exceptions)) {
184         list($exceptions, $params) = $DB->get_in_or_equal($exceptions, SQL_PARAMS_NAMED, 'ex0000', false);
185         $except = "AND u.id $exceptions";
186     } else {
187         $except = "";
188         $params = array();
189     }
191     if (!empty($sort)) {
192         $order = "ORDER BY $sort";
193     } else {
194         $order = "";
195     }
197     $select = "u.deleted = 0 AND u.confirmed = 1 AND ($fullname $LIKE :search1 OR u.email $LIKE :search2)";
198     $params['search1'] = "%$searchtext%";
199     $params['search2'] = "%$searchtext%";
201     if (!$courseid or $courseid == SITEID) {
202         $sql = "SELECT u.id, u.firstname, u.lastname, u.email
203                   FROM {user} u
204                  WHERE $select
205                        $except
206                 $order";
207         return $DB->get_records_sql($sql, $params);
209     } else {
210         if ($groupid) {
211             $sql = "SELECT u.id, u.firstname, u.lastname, u.email
212                       FROM {user} u
213                       JOIN {groups_members} gm ON gm.userid = u.id
214                      WHERE $select AND gm.groupid = :groupid
215                            $except
216                      $order";
217             $params['groupid'] = $groupid;
218             return $DB->get_records_sql($sql, $params);
220         } else {
221             $context = get_context_instance(CONTEXT_COURSE, $courseid);
222             $contextlists = get_related_contexts_string($context);
224             $sql = "SELECT u.id, u.firstname, u.lastname, u.email
225                       FROM {user} u
226                       JOIN {role_assignments} ra ON ra.userid = u.id
227                      WHERE $select AND ra.contextid $contextlists
228                            $except
229                     $order";
230             return $DB->get_records_sql($sql, $params);
231         }
232     }
235 /**
236  * Returns a subset of users
237  *
238  * @global object
239  * @uses DEBUG_DEVELOPER
240  * @uses SQL_PARAMS_NAMED
241  * @param bool $get If false then only a count of the records is returned
242  * @param string $search A simple string to search for
243  * @param bool $confirmed A switch to allow/disallow unconfirmed users
244  * @param array $exceptions A list of IDs to ignore, eg 2,4,5,8,9,10
245  * @param string $sort A SQL snippet for the sorting criteria to use
246  * @param string $firstinitial Users whose first name starts with $firstinitial
247  * @param string $lastinitial Users whose last name starts with $lastinitial
248  * @param string $page The page or records to return
249  * @param string $recordsperpage The number of records to return per page
250  * @param string $fields A comma separated list of fields to be returned from the chosen table.
251  * @return array|int|bool  {@link $USER} records unless get is false in which case the integer count of the records found is returned.
252   *                        False is returned if an error is encountered.
253  */
254 function get_users($get=true, $search='', $confirmed=false, array $exceptions=null, $sort='firstname ASC',
255                    $firstinitial='', $lastinitial='', $page='', $recordsperpage='', $fields='*', $extraselect='', array $extraparams=null) {
256     global $DB;
258     if ($get && !$recordsperpage) {
259         debugging('Call to get_users with $get = true no $recordsperpage limit. ' .
260                 'On large installations, this will probably cause an out of memory error. ' .
261                 'Please think again and change your code so that it does not try to ' .
262                 'load so much data into memory.', DEBUG_DEVELOPER);
263     }
265     $LIKE      = $DB->sql_ilike();
266     $fullname  = $DB->sql_fullname();
268     $select = " username <> :guest AND deleted = 0";
269     $params = array('guest'=>'guest');
271     if (!empty($search)){
272         $search = trim($search);
273         $select .= " AND ($fullname $LIKE :search1 OR email $LIKE :search2 OR username = :search3)";
274         $params['search1'] = "%$search%";
275         $params['search2'] = "%$search%";
276         $params['search3'] = "$search";
277     }
279     if ($confirmed) {
280         $select .= " AND confirmed = 1";
281     }
283     if ($exceptions) {
284         list($exceptions, $eparams) = $DB->get_in_or_equal($exceptions, SQL_PARAMS_NAMED, 'ex0000', false);
285         $params = $params + $eparams;
286         $except = " AND id $exceptions";
287     }
289     if ($firstinitial) {
290         $select .= " AND firstname $LIKE :fni";
291         $params['fni'] = "$firstinitial%";
292     }
293     if ($lastinitial) {
294         $select .= " AND lastname $LIKE :lni";
295         $params['lni'] = "$lastinitial%";
296     }
298     if ($extraselect) {
299         $select .= " AND $extraselect";
300         $params = $params + (array)$extraparams;
301     }
303     if ($get) {
304         return $DB->get_records_select('user', $select, $params, $sort, $fields, $page, $recordsperpage);
305     } else {
306         return $DB->count_records_select('user', $select, $params);
307     }
311 /**
312  * @todo Finish documenting this function
313  *
314  * @param string $sort An SQL field to sort by
315  * @param string $dir The sort direction ASC|DESC
316  * @param int $page The page or records to return
317  * @param int $recordsperpage The number of records to return per page
318  * @param string $search A simple string to search for
319  * @param string $firstinitial Users whose first name starts with $firstinitial
320  * @param string $lastinitial Users whose last name starts with $lastinitial
321  * @param string $extraselect An additional SQL select statement to append to the query
322  * @param array $extraparams Additional parameters to use for the above $extraselect
323  * @return array Array of {@link $USER} records
324  */
326 function get_users_listing($sort='lastaccess', $dir='ASC', $page=0, $recordsperpage=0,
327                            $search='', $firstinitial='', $lastinitial='', $extraselect='', array $extraparams=null) {
328     global $DB;
330     $LIKE      = $DB->sql_ilike();
331     $fullname  = $DB->sql_fullname();
333     $select = "deleted <> 1";
334     $params = array();
336     if (!empty($search)) {
337         $search = trim($search);
338         $select .= " AND ($fullname $LIKE :search1 OR email $LIKE :search2 OR username = :search3)";
339         $params['search1'] = "%$search%";
340         $params['search2'] = "%$search%";
341         $params['search3'] = "$search";
342     }
344     if ($firstinitial) {
345         $select .= " AND firstname $LIKE :fni";
346         $params['fni'] = "$firstinitial%";
347     }
348     if ($lastinitial) {
349         $select .= " AND lastname $LIKE :lni";
350         $params['lni'] = "$lastinitial%";
351     }
353     if ($extraselect) {
354         $select .= " AND $extraselect";
355         $params = $params + (array)$extraparams;
356     }
358     if ($sort) {
359         $sort = " ORDER BY $sort $dir";
360     }
362 /// warning: will return UNCONFIRMED USERS
363     return $DB->get_records_sql("SELECT id, username, email, firstname, lastname, city, country, lastaccess, confirmed, mnethostid
364                                    FROM {user}
365                                   WHERE $select
366                                   $sort", $params, $page, $recordsperpage);
371 /**
372  * Full list of users that have confirmed their accounts.
373  *
374  * @global object
375  * @return array of unconfirmed users
376  */
377 function get_users_confirmed() {
378     global $DB;
379     return $DB->get_records_sql("SELECT *
380                                    FROM {user}
381                                   WHERE confirmed = 1 AND deleted = 0 AND username <> ?", array('guest'));
385 /// OTHER SITE AND COURSE FUNCTIONS /////////////////////////////////////////////
388 /**
389  * Returns $course object of the top-level site.
390  *
391  * @return object A {@link $COURSE} object for the site, exception if not found
392  */
393 function get_site() {
394     global $SITE, $DB;
396     if (!empty($SITE->id)) {   // We already have a global to use, so return that
397         return $SITE;
398     }
400     if ($course = $DB->get_record('course', array('category'=>0))) {
401         return $course;
402     } else {
403         // course table exists, but the site is not there,
404         // unfortunately there is no automatic way to recover
405         throw new moodle_exception('nosite', 'error');
406     }
409 /**
410  * Returns list of courses, for whole site, or category
411  *
412  * Returns list of courses, for whole site, or category
413  * Important: Using c.* for fields is extremely expensive because
414  *            we are using distinct. You almost _NEVER_ need all the fields
415  *            in such a large SELECT
416  *
417  * @global object
418  * @global object
419  * @global object
420  * @uses CONTEXT_COURSE
421  * @param string|int $categoryid Either a category id or 'all' for everything
422  * @param string $sort A field and direction to sort by
423  * @param string $fields The additional fields to return
424  * @return array Array of courses
425  */
426 function get_courses($categoryid="all", $sort="c.sortorder ASC", $fields="c.*") {
428     global $USER, $CFG, $DB;
430     $params = array();
432     if ($categoryid !== "all" && is_numeric($categoryid)) {
433         $categoryselect = "WHERE c.category = :catid";
434         $params['catid'] = $categoryid;
435     } else {
436         $categoryselect = "";
437     }
439     if (empty($sort)) {
440         $sortstatement = "";
441     } else {
442         $sortstatement = "ORDER BY $sort";
443     }
445     $visiblecourses = array();
447     list($ccselect, $ccjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
449     $sql = "SELECT $fields $ccselect
450               FROM {course} c
451            $ccjoin
452               $categoryselect
453               $sortstatement";
455     // pull out all course matching the cat
456     if ($courses = $DB->get_records_sql($sql, $params)) {
458         // loop throught them
459         foreach ($courses as $course) {
460             context_instance_preload($course);
461             if (isset($course->visible) && $course->visible <= 0) {
462                 // for hidden courses, require visibility check
463                 if (has_capability('moodle/course:viewhiddencourses', get_context_instance(CONTEXT_COURSE, $course->id))) {
464                     $visiblecourses [$course->id] = $course;
465                 }
466             } else {
467                 $visiblecourses [$course->id] = $course;
468             }
469         }
470     }
471     return $visiblecourses;
475 /**
476  * Returns list of courses, for whole site, or category
477  *
478  * Similar to get_courses, but allows paging
479  * Important: Using c.* for fields is extremely expensive because
480  *            we are using distinct. You almost _NEVER_ need all the fields
481  *            in such a large SELECT
482  *
483  * @global object
484  * @global object
485  * @global object
486  * @uses CONTEXT_COURSE
487  * @param string|int $categoryid Either a category id or 'all' for everything
488  * @param string $sort A field and direction to sort by
489  * @param string $fields The additional fields to return
490  * @param int $totalcount Reference for the number of courses
491  * @param string $limitfrom The course to start from
492  * @param string $limitnum The number of courses to limit to
493  * @return array Array of courses
494  */
495 function get_courses_page($categoryid="all", $sort="c.sortorder ASC", $fields="c.*",
496                           &$totalcount, $limitfrom="", $limitnum="") {
497     global $USER, $CFG, $DB;
499     $params = array();
501     $categoryselect = "";
502     if ($categoryid != "all" && is_numeric($categoryid)) {
503         $categoryselect = "WHERE c.category = :catid";
504         $params['catid'] = $categoryid;
505     } else {
506         $categoryselect = "";
507     }
509     list($ccselect, $ccjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
511     $sql = "SELECT $fields $ccselect
512               FROM {course} c
513               $ccjoin
514            $categoryselect
515           ORDER BY $sort";
517     // pull out all course matching the cat
518     if (!$rs = $DB->get_recordset_sql($sql, $params)) {
519         return array();
520     }
521     $totalcount = 0;
523     if (!$limitfrom) {
524         $limitfrom = 0;
525     }
527     // iteration will have to be done inside loop to keep track of the limitfrom and limitnum
528     $visiblecourses = array();
529     foreach($rs as $course) {
530         context_instance_preload($course);
531         if ($course->visible <= 0) {
532             // for hidden courses, require visibility check
533             if (has_capability('moodle/course:viewhiddencourses', get_context_instance(CONTEXT_COURSE, $course->id))) {
534                 $totalcount++;
535                 if ($totalcount > $limitfrom && (!$limitnum or count($visiblecourses) < $limitnum)) {
536                     $visiblecourses [$course->id] = $course;
537                 }
538             }
539         } else {
540             $totalcount++;
541             if ($totalcount > $limitfrom && (!$limitnum or count($visiblecourses) < $limitnum)) {
542                 $visiblecourses [$course->id] = $course;
543             }
544         }
545     }
546     $rs->close();
547     return $visiblecourses;
550 /**
551  * Retrieve course records with the course managers and other related records
552  * that we need for print_course(). This allows print_courses() to do its job
553  * in a constant number of DB queries, regardless of the number of courses,
554  * role assignments, etc.
555  *
556  * The returned array is indexed on c.id, and each course will have
557  * - $course->managers - array containing RA objects that include a $user obj
558  *                       with the minimal fields needed for fullname()
559  *
560  * @global object
561  * @global object
562  * @global object
563  * @uses CONTEXT_COURSE
564  * @uses CONTEXT_SYSTEM
565  * @uses CONTEXT_COURSECAT
566  * @uses SITEID
567  * @param int|string $categoryid Either the categoryid for the courses or 'all'
568  * @param string $sort A SQL sort field and direction
569  * @param array $fields An array of additional fields to fetch
570  * @return array
571  */
572 function get_courses_wmanagers($categoryid=0, $sort="c.sortorder ASC", $fields=array()) {
573     /*
574      * The plan is to
575      *
576      * - Grab the courses JOINed w/context
577      *
578      * - Grab the interesting course-manager RAs
579      *   JOINed with a base user obj and add them to each course
580      *
581      * So as to do all the work in 2 DB queries. The RA+user JOIN
582      * ends up being pretty expensive if it happens over _all_
583      * courses on a large site. (Are we surprised!?)
584      *
585      * So this should _never_ get called with 'all' on a large site.
586      *
587      */
588     global $USER, $CFG, $DB;
590     $params = array();
591     $allcats = false; // bool flag
592     if ($categoryid === 'all') {
593         $categoryclause   = '';
594         $allcats = true;
595     } elseif (is_numeric($categoryid)) {
596         $categoryclause = "c.category = :catid";
597         $params['catid'] = $categoryid;
598     } else {
599         debugging("Could not recognise categoryid = $categoryid");
600         $categoryclause = '';
601     }
603     $basefields = array('id', 'category', 'sortorder',
604                         'shortname', 'fullname', 'idnumber',
605                         'guest', 'startdate', 'visible',
606                         'newsitems',  'cost', 'enrol',
607                         'groupmode', 'groupmodeforce');
609     if (!is_null($fields) && is_string($fields)) {
610         if (empty($fields)) {
611             $fields = $basefields;
612         } else {
613             // turn the fields from a string to an array that
614             // get_user_courses_bycap() will like...
615             $fields = explode(',',$fields);
616             $fields = array_map('trim', $fields);
617             $fields = array_unique(array_merge($basefields, $fields));
618         }
619     } elseif (is_array($fields)) {
620         $fields = array_merge($basefields,$fields);
621     }
622     $coursefields = 'c.' .join(',c.', $fields);
624     if (empty($sort)) {
625         $sortstatement = "";
626     } else {
627         $sortstatement = "ORDER BY $sort";
628     }
630     $where = 'WHERE c.id != ' . SITEID;
631     if ($categoryclause !== ''){
632         $where = "$where AND $categoryclause";
633     }
635     // pull out all courses matching the cat
636     list($ccselect, $ccjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
637     $sql = "SELECT $coursefields $ccselect
638               FROM {course} c
639            $ccjoin
640                $where
641                $sortstatement";
643     $catpaths = array();
644     $catpath  = NULL;
645     if ($courses = $DB->get_records_sql($sql, $params)) {
646         // loop on courses materialising
647         // the context, and prepping data to fetch the
648         // managers efficiently later...
649         foreach ($courses as $k => $course) {
650             context_instance_preload($course);
651             $coursecontext = get_context_instance(CONTEXT_COURSE, $course->id);
652             $courses[$k] = $course;
653             $courses[$k]->managers = array();
654             if ($allcats === false) {
655                 // single cat, so take just the first one...
656                 if ($catpath === NULL) {
657                     $catpath = preg_replace(':/\d+$:', '', $coursecontext->path);
658                 }
659             } else {
660                 // chop off the contextid of the course itself
661                 // like dirname() does...
662                 $catpaths[] = preg_replace(':/\d+$:', '', $coursecontext->path);
663             }
664         }
665     } else {
666         return array(); // no courses!
667     }
669     $CFG->coursemanager = trim($CFG->coursemanager);
670     if (empty($CFG->coursemanager)) {
671         return $courses;
672     }
674     $managerroles = split(',', $CFG->coursemanager);
675     $catctxids = '';
676     if (count($managerroles)) {
677         if ($allcats === true) {
678             $catpaths  = array_unique($catpaths);
679             $ctxids = array();
680             foreach ($catpaths as $cpath) {
681                 $ctxids = array_merge($ctxids, explode('/',substr($cpath,1)));
682             }
683             $ctxids = array_unique($ctxids);
684             $catctxids = implode( ',' , $ctxids);
685             unset($catpaths);
686             unset($cpath);
687         } else {
688             // take the ctx path from the first course
689             // as all categories will be the same...
690             $catpath = substr($catpath,1);
691             $catpath = preg_replace(':/\d+$:','',$catpath);
692             $catctxids = str_replace('/',',',$catpath);
693         }
694         if ($categoryclause !== '') {
695             $categoryclause = "AND $categoryclause";
696         }
697         /*
698          * Note: Here we use a LEFT OUTER JOIN that can
699          * "optionally" match to avoid passing a ton of context
700          * ids in an IN() clause. Perhaps a subselect is faster.
701          *
702          * In any case, this SQL is not-so-nice over large sets of
703          * courses with no $categoryclause.
704          *
705          */
706         $sql = "SELECT ctx.path, ctx.instanceid, ctx.contextlevel,
707                        r.id AS roleid, r.name as rolename,
708                        u.id AS userid, u.firstname, u.lastname
709                   FROM {role_assignments} ra
710                   JOIN {context} ctx ON ra.contextid = ctx.id
711                   JOIN {user} u ON ra.userid = u.id
712                   JOIN {role} r ON ra.roleid = r.id
713                   LEFT OUTER JOIN {course} c
714                        ON (ctx.instanceid=c.id AND ctx.contextlevel=".CONTEXT_COURSE.")
715                 WHERE ( c.id IS NOT NULL";
716         // under certain conditions, $catctxids is NULL
717         if($catctxids == NULL){
718             $sql .= ") ";
719         }else{
720             $sql .= " OR ra.contextid  IN ($catctxids) )";
721         }
723         $sql .= "AND ra.roleid IN ({$CFG->coursemanager})
724                       $categoryclause
725                 ORDER BY r.sortorder ASC, ctx.contextlevel ASC, ra.sortorder ASC";
726         $rs = $DB->get_recordset_sql($sql, $params);
728         // This loop is fairly stupid as it stands - might get better
729         // results doing an initial pass clustering RAs by path.
730         foreach($rs as $ra) {
731             $user = new stdClass;
732             $user->id        = $ra->userid;    unset($ra->userid);
733             $user->firstname = $ra->firstname; unset($ra->firstname);
734             $user->lastname  = $ra->lastname;  unset($ra->lastname);
735             $ra->user = $user;
736             if ($ra->contextlevel == CONTEXT_SYSTEM) {
737                 foreach ($courses as $k => $course) {
738                     $courses[$k]->managers[] = $ra;
739                 }
740             } else if ($ra->contextlevel == CONTEXT_COURSECAT) {
741                 if ($allcats === false) {
742                     // It always applies
743                     foreach ($courses as $k => $course) {
744                         $courses[$k]->managers[] = $ra;
745                     }
746                 } else {
747                     foreach ($courses as $k => $course) {
748                         $coursecontext = get_context_instance(CONTEXT_COURSE, $course->id);
749                         // Note that strpos() returns 0 as "matched at pos 0"
750                         if (strpos($coursecontext->path, $ra->path.'/') === 0) {
751                             // Only add it to subpaths
752                             $courses[$k]->managers[] = $ra;
753                         }
754                     }
755                 }
756             } else { // course-level
757                 if (!array_key_exists($ra->instanceid, $courses)) {
758                     //this course is not in a list, probably a frontpage course
759                     continue;
760                 }
761                 $courses[$ra->instanceid]->managers[] = $ra;
762             }
763         }
764         $rs->close();
765     }
767     return $courses;
770 /**
771  * Convenience function - lists courses that a user has access to view.
772  *
773  * For admins and others with access to "every" course in the system, we should
774  * try to get courses with explicit RAs.
775  *
776  * NOTE: this function is heavily geared towards the perspective of the user
777  *       passed in $userid. So it will hide courses that the user cannot see
778  *       (for any reason) even if called from cron or from another $USER's
779  *       perspective.
780  *
781  *       If you really want to know what courses are assigned to the user,
782  *       without any hiding or scheming, call the lower-level
783  *       get_user_courses_bycap().
784  *
785  *
786  * Notes inherited from get_user_courses_bycap():
787  *
788  * - $fields is an array of fieldnames to ADD
789  *   so name the fields you really need, which will
790  *   be added and uniq'd
791  *
792  * - the course records have $c->context which is a fully
793  *   valid context object. Saves you a query per course!
794  *
795  * @global object
796  * @global object
797  * @global object
798  * @uses CONTEXT_SYSTEM
799  * @uses CONTEXT_COURSE
800  * @uses CONTEXT_COURSECAT
801  * @param int $userid The user of interest
802  * @param string $sort the sortorder in the course table
803  * @param array $fields names of _additional_ fields to return (also accepts a string)
804  * @param bool $doanything True if using the doanything flag
805  * @param int $limit Maximum number of records to return, or 0 for unlimited
806  * @return array Array of {@link $COURSE} of course objects
807  */
808 function get_my_courses($userid, $sort='visible DESC,sortorder ASC', $fields=NULL, $doanything=false,$limit=0) {
809     global $CFG, $USER, $DB;
811     // Guest account does not have any courses
812     if (isguestuser()) {
813         return(array());
814     }
816     $basefields = array('id', 'category', 'sortorder',
817                         'shortname', 'fullname', 'idnumber',
818                         'guest', 'startdate', 'visible',
819                         'newsitems',  'cost', 'enrol',
820                         'groupmode', 'groupmodeforce');
822     if (!is_null($fields) && is_string($fields)) {
823         if (empty($fields)) {
824             $fields = $basefields;
825         } else {
826             // turn the fields from a string to an array that
827             // get_user_courses_bycap() will like...
828             $fields = explode(',',$fields);
829             $fields = array_map('trim', $fields);
830             $fields = array_unique(array_merge($basefields, $fields));
831         }
832     } elseif (is_array($fields)) {
833         $fields = array_unique(array_merge($basefields, $fields));
834     } else {
835         $fields = $basefields;
836     }
838     $orderby = '';
839     $sort    = trim($sort);
840     if (!empty($sort)) {
841         $rawsorts = explode(',', $sort);
842         $sorts = array();
843         foreach ($rawsorts as $rawsort) {
844             $rawsort = trim($rawsort);
845             if (strpos($rawsort, 'c.') === 0) {
846                 $rawsort = substr($rawsort, 2);
847             }
848             $sorts[] = trim($rawsort);
849         }
850         $sort = 'c.'.implode(',c.', $sorts);
851         $orderby = "ORDER BY $sort";
852     }
854     //
855     // Logged-in user - Check cached courses
856     //
857     // NOTE! it's a _string_ because
858     // - it's all we'll ever use
859     // - it serialises much more compact than an array
860     //   this a big concern here - cost of serialise
861     //   and unserialise gets huge as the session grows
862     //
863     // If the courses are too many - it won't be set
864     // for large numbers of courses, caching in the session
865     // has marginal benefits (costs too much, not
866     // worthwhile...) and we may hit SQL parser limits
867     // because we use IN()
868     //
869     if ($userid === $USER->id) {
870         if (isset($USER->loginascontext)
871             && $USER->loginascontext->contextlevel == CONTEXT_COURSE) {
872             // list _only_ this course
873             // anything else is asking for trouble...
874             $courseids = $USER->loginascontext->instanceid;
875         } elseif (isset($USER->mycourses)
876                   && is_string($USER->mycourses)) {
877             if ($USER->mycourses === '') {
878                 // empty str means: user has no courses
879                 // ... so do the easy thing...
880                 return array();
881             } else {
882                 $courseids = $USER->mycourses;
883             }
884         }
885         if (isset($courseids)) {
886             // The data massaging here MUST be kept in sync with
887             // get_user_courses_bycap() so we return
888             // the same...
889             // (but here we don't need to check has_cap)
890             $coursefields = 'c.' .join(',c.', $fields);
891             list($ccselect, $ccjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
892             $sql = "SELECT $coursefields $ccselect, cc.path AS categorypath
893                       FROM {course} c
894                       JOIN {course_categories} cc ON c.category=cc.id
895                    $ccjoin
896                      WHERE c.id IN ($courseids)
897                   $orderby";
898             $rs = $DB->get_recordset_sql($sql);
899             $courses = array();
900             $cc = 0; // keep count
901             foreach ($rs as $c) {
902                 // build the context obj
903                 context_instance_preload($c);
905                 if ($limit > 0 && $cc >= $limit) {
906                     break;
907                 }
909                 $courses[$c->id] = $c;
910                 $cc++;
911             }
912             $rs->close();
913             return $courses;
914         }
915     }
917     // Non-cached - get accessinfo
918     if ($userid === $USER->id && isset($USER->access)) {
919         $accessinfo = $USER->access;
920     } else {
921         $accessinfo = get_user_access_sitewide($userid);
922     }
925     $courses = get_user_courses_bycap($userid, 'moodle/course:participate', $accessinfo,
926                                       $doanything, $sort, $fields,
927                                       $limit);
929     $cats = NULL;
930     // If we have to walk category visibility
931     // to eval course visibility, get the categories
932     if (empty($CFG->allowvisiblecoursesinhiddencategories)) {
933         list($ccselect, $ccjoin) = context_instance_preload_sql('cc.id', CONTEXT_COURSECAT, 'ctx');
934         $sql = "SELECT cc.id, cc.path, cc.visible $ccselect
935                   FROM {course_categories} cc
936                $ccjoin
937               ORDER BY cc.id";
938         $rs = $DB->get_recordset_sql($sql);
940         // Using a temporary array instead of $cats here, to avoid a "true" result when isnull($cats) further down
941         $categories = array();
942         foreach($rs as $course_cat) {
943             // build the context obj
944             context_instance_preload($course_cat);
945             $categories[$course_cat->id] = $course_cat;
946         }
947         $rs->close();
949         if (!empty($categories)) {
950             $cats = $categories;
951         }
953         unset($course_cat);
954     }
955     //
956     // Strangely, get_my_courses() is expected to return the
957     // array keyed on id, which messes up the sorting
958     // So do that, and also cache the ids in the session if appropriate
959     //
960     $kcourses = array();
961     $courses_count = count($courses);
962     $cacheids = NULL;
963     $vcatpaths = array();
964     if ($userid === $USER->id && $courses_count < 500) {
965         $cacheids = array();
966     }
967     for ($n=0; $n<$courses_count; $n++) {
969         //
970         // Check whether $USER (not $userid) can _actually_ see them
971         // Easy if $CFG->allowvisiblecoursesinhiddencategories
972         // is set, and we don't have to care about categories.
973         // Lots of work otherwise... (all in mem though!)
974         //
975         $cansee = false;
976         if (is_null($cats)) { // easy rules!
977             if ($courses[$n]->visible == true) {
978                 $cansee = true;
979             } elseif (has_capability('moodle/course:viewhiddencourses',
980                                      $courses[$n]->context, $USER->id)) {
981                 $cansee = true;
982             }
983         } else {
984             //
985             // Is the cat visible?
986             // we have to assume it _is_ visible
987             // so we can shortcut when we find a hidden one
988             //
989             $viscat = true;
990             $cpath = $courses[$n]->categorypath;
991             if (isset($vcatpaths[$cpath])) {
992                 $viscat = $vcatpaths[$cpath];
993             } else {
994                 $cpath = substr($cpath,1); // kill leading slash
995                 $cpath = explode('/',$cpath);
996                 $ccct  = count($cpath);
997                 for ($m=0;$m<$ccct;$m++) {
998                     $ccid = $cpath[$m];
999                     if ($cats[$ccid]->visible==false) {
1000                         $viscat = false;
1001                         break;
1002                     }
1003                 }
1004                 $vcatpaths[$courses[$n]->categorypath] = $viscat;
1005             }
1007             //
1008             // Perhaps it's actually visible to $USER
1009             // check moodle/category:viewhiddencategories
1010             //
1011             // The name isn't obvious, but the description says
1012             // "See hidden categories" so the user shall see...
1013             // But also check if the allowvisiblecoursesinhiddencategories setting is true, and check for course visibility
1014             if ($viscat === false) {
1015                 $catctx = $cats[$courses[$n]->category]->context;
1016                 if (has_capability('moodle/category:viewhiddencategories', $catctx, $USER->id)) {
1017                     $vcatpaths[$courses[$n]->categorypath] = true;
1018                     $viscat = true;
1019                 } elseif ($CFG->allowvisiblecoursesinhiddencategories && $courses[$n]->visible == true) {
1020                     $viscat = true;
1021                 }
1022             }
1024             //
1025             // Decision matrix
1026             //
1027             if ($viscat === true) {
1028                 if ($courses[$n]->visible == true) {
1029                     $cansee = true;
1030                 } elseif (has_capability('moodle/course:viewhiddencourses',
1031                                         $courses[$n]->context, $USER->id)) {
1032                     $cansee = true;
1033                 }
1034             }
1035         }
1036         if ($cansee === true) {
1037             $kcourses[$courses[$n]->id] = $courses[$n];
1038             if (is_array($cacheids)) {
1039                 $cacheids[] = $courses[$n]->id;
1040             }
1041         }
1042     }
1043     if (is_array($cacheids)) {
1044         // Only happens
1045         // - for the logged in user
1046         // - below the threshold (500)
1047         // empty string is _valid_
1048         $USER->mycourses = join(',',$cacheids);
1049     } elseif ($userid === $USER->id && isset($USER->mycourses)) {
1050         // cheap sanity check
1051         unset($USER->mycourses);
1052     }
1054     return $kcourses;
1057 /**
1058  * A list of courses that match a search
1059  *
1060  * @global object
1061  * @global object
1062  * @param array $searchterms An array of search criteria
1063  * @param string $sort A field and direction to sort by
1064  * @param int $page The page number to get
1065  * @param int $recordsperpage The number of records per page
1066  * @param int $totalcount Passed in by reference.
1067  * @return object {@link $COURSE} records
1068  */
1069 function get_courses_search($searchterms, $sort='fullname ASC', $page=0, $recordsperpage=50, &$totalcount) {
1070     global $CFG, $DB;
1072     if ($DB->sql_regex_supported()) {
1073         $REGEXP    = $DB->sql_regex(true);
1074         $NOTREGEXP = $DB->sql_regex(false);
1075     }
1076     $LIKE = $DB->sql_ilike(); // case-insensitive
1078     $searchcond = array();
1079     $params     = array();
1080     $i = 0;
1082     $concat = $DB->sql_concat('c.summary', "' '", 'c.fullname');
1084     foreach ($searchterms as $searchterm) {
1085         $i++;
1087         $NOT = ''; /// Initially we aren't going to perform NOT LIKE searches, only MSSQL and Oracle
1088                    /// will use it to simulate the "-" operator with LIKE clause
1090     /// Under Oracle and MSSQL, trim the + and - operators and perform
1091     /// simpler LIKE (or NOT LIKE) queries
1092         if (!$DB->sql_regex_supported()) {
1093             if (substr($searchterm, 0, 1) == '-') {
1094                 $NOT = ' NOT ';
1095             }
1096             $searchterm = trim($searchterm, '+-');
1097         }
1099         // TODO: +- may not work for non latin languages
1101         if (substr($searchterm,0,1) == '+') {
1102             $searchterm = trim($searchterm, '+-');
1103             $searchterm = preg_quote($searchterm, '|');
1104             $searchcond[] = "$concat $REGEXP :ss$i";
1105             $params['ss'.$i] = "(^|[^a-zA-Z0-9])$searchterm([^a-zA-Z0-9]|$)";
1107         } else if (substr($searchterm,0,1) == "-") {
1108             $searchterm = trim($searchterm, '+-');
1109             $searchterm = preg_quote($searchterm, '|');
1110             $searchcond[] = "$concat $NOTREGEXP :ss$i";
1111             $params['ss'.$i] = "(^|[^a-zA-Z0-9])$searchterm([^a-zA-Z0-9]|$)";
1113         } else {
1114             $searchcond[] = "$concat $NOT $LIKE :ss$i";
1115             $params['ss'.$i] = "%$searchterm%";
1116         }
1117     }
1119     if (empty($searchcond)) {
1120         $totalcount = 0;
1121         return array();
1122     }
1124     $searchcond = implode(" AND ", $searchcond);
1126     list($ccselect, $ccjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
1127     $sql = "SELECT c.* $ccselect
1128               FROM {course} c
1129            $ccjoin
1130              WHERE $searchcond AND c.id <> ".SITEID."
1131           ORDER BY $sort";
1132     $courses = array();
1133     $c = 0; // counts how many visible courses we've seen
1135     if ($rs = $DB->get_recordset_sql($sql, $params)) {
1136         // Tiki pagination
1137         $limitfrom = $page * $recordsperpage;
1138         $limitto   = $limitfrom + $recordsperpage;
1140         foreach($rs as $course) {
1141             context_instance_preload($course);
1142             $coursecontext = get_context_instance(CONTEXT_COURSE, $course->id);
1143             if ($course->visible || has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
1144                 // Don't exit this loop till the end
1145                 // we need to count all the visible courses
1146                 // to update $totalcount
1147                 if ($c >= $limitfrom && $c < $limitto) {
1148                     $courses[$course->id] = $course;
1149                 }
1150                 $c++;
1151             }
1152         }
1153         $rs->close();
1154     }
1156     // our caller expects 2 bits of data - our return
1157     // array, and an updated $totalcount
1158     $totalcount = $c;
1159     return $courses;
1163 /**
1164  * Returns a sorted list of categories. Each category object has a context
1165  * property that is a context object.
1166  *
1167  * When asking for $parent='none' it will return all the categories, regardless
1168  * of depth. Wheen asking for a specific parent, the default is to return
1169  * a "shallow" resultset. Pass false to $shallow and it will return all
1170  * the child categories as well.
1171  *
1172  * @global object
1173  * @uses CONTEXT_COURSECAT
1174  * @param string $parent The parent category if any
1175  * @param string $sort the sortorder
1176  * @param bool   $shallow - set to false to get the children too
1177  * @return array of categories
1178  */
1179 function get_categories($parent='none', $sort=NULL, $shallow=true) {
1180     global $DB;
1182     if ($sort === NULL) {
1183         $sort = 'ORDER BY cc.sortorder ASC';
1184     } elseif ($sort ==='') {
1185         // leave it as empty
1186     } else {
1187         $sort = "ORDER BY $sort";
1188     }
1190     list($ccselect, $ccjoin) = context_instance_preload_sql('cc.id', CONTEXT_COURSECAT, 'ctx');
1192     if ($parent === 'none') {
1193         $sql = "SELECT cc.* $ccselect
1194                   FROM {course_categories} cc
1195                $ccjoin
1196                 $sort";
1197         $params = array();
1199     } elseif ($shallow) {
1200         $sql = "SELECT cc.* $ccselect
1201                   FROM {course_categories} cc
1202                $ccjoin
1203                  WHERE cc.parent=?
1204                 $sort";
1205         $params = array($parent);
1207     } else {
1208         $sql = "SELECT cc.* $ccselect
1209                   FROM {course_categories} cc
1210                $ccjoin
1211                   JOIN {course_categories} ccp
1212                        ON (cc.path LIKE ".$DB->sql_concat('ccp.path',"'%'").")
1213                  WHERE ccp.id=?
1214                 $sort";
1215         $params = array($parent);
1216     }
1217     $categories = array();
1219     if( $rs = $DB->get_recordset_sql($sql, $params) ){
1220         foreach($rs as $cat) {
1221             context_instance_preload($cat);
1222             $catcontext = get_context_instance(CONTEXT_COURSECAT, $cat->id);
1223             if ($cat->visible || has_capability('moodle/category:viewhiddencategories', $catcontext)) {
1224                 $categories[$cat->id] = $cat;
1225             }
1226         }
1227         $rs->close();
1228     }
1229     return $categories;
1233 /**
1234  * Returns an array of category ids of all the subcategories for a given
1235  * category.
1236  *
1237  * @global object
1238  * @param int $catid - The id of the category whose subcategories we want to find.
1239  * @return array of category ids.
1240  */
1241 function get_all_subcategories($catid) {
1242     global $DB;
1244     $subcats = array();
1246     if ($categories = $DB->get_records('course_categories', array('parent'=>$catid))) {
1247         foreach ($categories as $cat) {
1248             array_push($subcats, $cat->id);
1249             $subcats = array_merge($subcats, get_all_subcategories($cat->id));
1250         }
1251     }
1252     return $subcats;
1255 /**
1256  * Return specified category, default if given does not exist
1257  *
1258  * @global object
1259  * @uses MAX_COURSES_IN_CATEGORY
1260  * @uses CONTEXT_COURSECAT
1261  * @uses SYSCONTEXTID
1262  * @param int $catid course category id
1263  * @return object caregory
1264  */
1265 function get_course_category($catid=0) {
1266     global $DB;
1268     $category = false;
1270     if (!empty($catid)) {
1271         $category = $DB->get_record('course_categories', array('id'=>$catid));
1272     }
1274     if (!$category) {
1275         // the first category is considered default for now
1276         if ($category = $DB->get_records('course_categories', null, 'sortorder', '*', 0, 1)) {
1277             $category = reset($category);
1279         } else {
1280             $cat = new object();
1281             $cat->name         = get_string('miscellaneous');
1282             $cat->depth        = 1;
1283             $cat->sortorder    = MAX_COURSES_IN_CATEGORY;
1284             $cat->timemodified = time();
1285             $catid = $DB->insert_record('course_categories', $cat);
1286             // make sure category context exists
1287             get_context_instance(CONTEXT_COURSECAT, $catid);
1288             mark_context_dirty('/'.SYSCONTEXTID);
1289             fix_course_sortorder(); // Required to build course_categories.depth and .path.
1290             $category = $DB->get_record('course_categories', array('id'=>$catid));
1291         }
1292     }
1294     return $category;
1297 /**
1298  * Fixes course category and course sortorder, also verifies category and course parents and paths.
1299  * (circular references are not fixed)
1300  *
1301  * @global object
1302  * @global object
1303  * @uses MAX_COURSES_IN_CATEGORY
1304  * @uses MAX_COURSE_CATEGORIES
1305  * @uses SITEID
1306  * @uses CONTEXT_COURSE
1307  * @return void
1308  */
1309 function fix_course_sortorder() {
1310     global $DB, $SITE;
1312     //WARNING: this is PHP5 only code!
1314     if ($unsorted = $DB->get_records('course_categories', array('sortorder'=>0))) {
1315         //move all categories that are not sorted yet to the end
1316         $DB->set_field('course_categories', 'sortorder', MAX_COURSES_IN_CATEGORY*MAX_COURSE_CATEGORIES, array('sortorder'=>0));
1317     }
1319     $allcats = $DB->get_records('course_categories', null, 'sortorder, id', 'id, sortorder, parent, depth, path');
1320     $topcats    = array();
1321     $brokencats = array();
1322     foreach ($allcats as $cat) {
1323         $sortorder = (int)$cat->sortorder;
1324         if (!$cat->parent) {
1325             while(isset($topcats[$sortorder])) {
1326                 $sortorder++;
1327             }
1328             $topcats[$sortorder] = $cat;
1329             continue;
1330         }
1331         if (!isset($allcats[$cat->parent])) {
1332             $brokencats[] = $cat;
1333             continue;
1334         }
1335         if (!isset($allcats[$cat->parent]->children)) {
1336             $allcats[$cat->parent]->children = array();
1337         }
1338         while(isset($allcats[$cat->parent]->children[$sortorder])) {
1339             $sortorder++;
1340         }
1341         $allcats[$cat->parent]->children[$sortorder] = $cat;
1342     }
1343     unset($allcats);
1345     // add broken cats to category tree
1346     if ($brokencats) {
1347         $defaultcat = reset($topcats);
1348         foreach ($brokencats as $cat) {
1349             $topcats[] = $cat;
1350         }
1351     }
1353     // now walk recursively the tree and fix any problems found
1354     $sortorder = 0;
1355     $fixcontexts = array();
1356     _fix_course_cats($topcats, $sortorder, 0, 0, '', $fixcontexts);
1358     // detect if there are "multiple" frontpage courses and fix them if needed
1359     $frontcourses = $DB->get_records('course', array('category'=>0), 'id');
1360     if (count($frontcourses) > 1) {
1361         if (isset($frontcourses[SITEID])) {
1362             $frontcourse = $frontcourses[SITEID];
1363             unset($frontcourses[SITEID]);
1364         } else {
1365             $frontcourse = array_shift($frontcourses);
1366         }
1367         $defaultcat = reset($topcats);
1368         foreach ($frontcourses as $course) {
1369             $DB->set_field('course', 'category', $defaultcat->id, array('id'=>$course->id));
1370             $context = get_context_instance(CONTEXT_COURSE, $course->id);
1371             $fixcontexts[$context->id] = $context;
1372         }
1373         unset($frontcourses);
1374     } else {
1375         $frontcourse = reset($frontcourses);
1376     }
1378     // now fix the paths and depths in context table if needed
1379     if ($fixcontexts) {
1380         rebuild_contexts($fixcontexts);
1381     }
1383     // release memory
1384     unset($topcats);
1385     unset($brokencats);
1386     unset($fixcontexts);
1388     // fix frontpage course sortorder
1389     if ($frontcourse->sortorder != 1) {
1390         $DB->set_field('course', 'sortorder', 1, array('id'=>$frontcourse->id));
1391     }
1393     // now fix the course counts in category records if needed
1394     $sql = "SELECT cc.id, cc.coursecount, COUNT(c.id) AS newcount
1395               FROM {course_categories} cc
1396               LEFT JOIN {course} c ON c.category = cc.id
1397           GROUP BY cc.id, cc.coursecount
1398             HAVING cc.coursecount <> COUNT(c.id)";
1400     if ($updatecounts = $DB->get_records_sql($sql)) {
1401         foreach ($updatecounts as $cat) {
1402             $cat->coursecount = $cat->newcount;
1403             unset($cat->newcount);
1404             $DB->update_record_raw('course_categories', $cat, true);
1405         }
1406     }
1408     // now make sure that sortorders in course table are withing the category sortorder ranges
1409     $sql = "SELECT DISTINCT cc.id, cc.sortorder
1410               FROM {course_categories} cc
1411               JOIN {course} c ON c.category = cc.id
1412              WHERE c.sortorder < cc.sortorder OR c.sortorder > cc.sortorder + ".MAX_COURSES_IN_CATEGORY;
1414     if ($fixcategories = $DB->get_records_sql($sql)) {
1415         //fix the course sortorder ranges
1416         foreach ($fixcategories as $cat) {
1417             $sql = "UPDATE {course}
1418                        SET sortorder = ".$DB->sql_modulo('sortorder', MAX_COURSES_IN_CATEGORY)." + ?
1419                      WHERE category = ?";
1420             $DB->execute($sql, array($cat->sortorder, $cat->id));
1421         }
1422     }
1423     unset($fixcategories);
1425     // categories having courses with sortorder duplicates or having gaps in sortorder
1426     $sql = "SELECT DISTINCT c1.category AS id , cc.sortorder
1427               FROM {course} c1
1428               JOIN {course} c2 ON c1.sortorder = c2.sortorder
1429               JOIN {course_categories} cc ON (c1.category = cc.id)
1430              WHERE c1.id <> c2.id";
1431     $fixcategories = $DB->get_records_sql($sql);
1433     $sql = "SELECT cc.id, cc.sortorder, cc.coursecount, MAX(c.sortorder) AS maxsort, MIN(c.sortorder) AS minsort
1434               FROM {course_categories} cc
1435               JOIN {course} c ON c.category = cc.id
1436           GROUP BY cc.id, cc.sortorder, cc.coursecount
1437             HAVING (MAX(c.sortorder) <>  cc.sortorder + cc.coursecount) OR (MIN(c.sortorder) <>  cc.sortorder + 1)";
1438     $gapcategories = $DB->get_records_sql($sql);
1440     foreach ($gapcategories as $cat) {
1441         if (isset($fixcategories[$cat->id])) {
1442             // duplicates detected already
1444         } else if ($cat->minsort == $cat->sortorder and $cat->maxsort == $cat->sortorder + $cat->coursecount - 1) {
1445             // easy - new course inserted with sortorder 0, the rest is ok
1446             $sql = "UPDATE {course}
1447                        SET sortorder = sortorder + 1
1448                      WHERE category = ?";
1449             $DB->execute($sql, array($cat->id));
1451         } else {
1452             // it needs full resorting
1453             $fixcategories[$cat->id] = $cat;
1454         }
1455     }
1456     unset($gapcategories);
1458     // fix course sortorders in problematic categories only
1459     foreach ($fixcategories as $cat) {
1460         $i = 1;
1461         $courses = $DB->get_records('course', array('category'=>$cat->id), 'sortorder ASC, id DESC', 'id, sortorder');
1462         foreach ($courses as $course) {
1463             if ($course->sortorder != $cat->sortorder + $i) {
1464                 $course->sortorder = $cat->sortorder + $i;
1465                 $DB->update_record_raw('course', $course, true);
1466             }
1467             $i++;
1468         }
1469     }
1472 /**
1473  * Internal recursive category verification function, do not use directly!
1474  *
1475  * @todo Document the arguments of this function better
1476  *
1477  * @global object
1478  * @uses MAX_COURSES_IN_CATEGORY
1479  * @uses CONTEXT_COURSECAT
1480  * @param array $children
1481  * @param int $sortorder
1482  * @param string $parent
1483  * @param int $depth
1484  * @param string $path
1485  * @param array $fixcontexts
1486  * @return void
1487  */
1488 function _fix_course_cats($children, &$sortorder, $parent, $depth, $path, &$fixcontexts) {
1489     global $DB;
1491     $depth++;
1493     foreach ($children as $cat) {
1494         $sortorder = $sortorder + MAX_COURSES_IN_CATEGORY;
1495         $update = false;
1496         if ($parent != $cat->parent or $depth != $cat->depth or $path.'/'.$cat->id != $cat->path) {
1497             $cat->parent = $parent;
1498             $cat->depth  = $depth;
1499             $cat->path   = $path.'/'.$cat->id;
1500             $update = true;
1502             // make sure context caches are rebuild and dirty contexts marked
1503             $context = get_context_instance(CONTEXT_COURSECAT, $cat->id);
1504             $fixcontexts[$context->id] = $context;
1505         }
1506         if ($cat->sortorder != $sortorder) {
1507             $cat->sortorder = $sortorder;
1508             $update = true;
1509         }
1510         if ($update) {
1511             $DB->update_record('course_categories', $cat, true);
1512         }
1513         if (isset($cat->children)) {
1514             _fix_course_cats($cat->children, $sortorder, $cat->id, $cat->depth, $cat->path, $fixcontexts);
1515         }
1516     }
1519 /**
1520  * List of remote courses that a user has access to via MNET.
1521  * Works only on the IDP
1522  *
1523  * @global object
1524  * @global object
1525  * @param int @userid The user id to get remote courses for
1526  * @return array Array of {@link $COURSE} of course objects
1527  */
1528 function get_my_remotecourses($userid=0) {
1529     global $DB, $USER;
1531     if (empty($userid)) {
1532         $userid = $USER->id;
1533     }
1535     $sql = "SELECT c.id, c.remoteid, c.shortname, c.fullname,
1536                    c.hostid, c.summary, c.cat_name,
1537                    h.name AS hostname
1538               FROM {mnet_enrol_course} c
1539               JOIN {mnet_enrol_assignments} a ON c.id=a.courseid
1540               JOIN {mnet_host} h              ON c.hostid=h.id
1541              WHERE a.userid=?";
1543     return $DB->get_records_sql($sql, array($userid));
1546 /**
1547  * List of remote hosts that a user has access to via MNET.
1548  * Works on the SP
1549  *
1550  * @global object
1551  * @global object
1552  * @return array|bool Array of host objects or false
1553  */
1554 function get_my_remotehosts() {
1555     global $CFG, $USER;
1557     if ($USER->mnethostid == $CFG->mnet_localhost_id) {
1558         return false; // Return nothing on the IDP
1559     }
1560     if (!empty($USER->mnet_foreign_host_array) && is_array($USER->mnet_foreign_host_array)) {
1561         return $USER->mnet_foreign_host_array;
1562     }
1563     return false;
1566 /**
1567  * This function creates a default separated/connected scale
1568  *
1569  * This function creates a default separated/connected scale
1570  * so there's something in the database.  The locations of
1571  * strings and files is a bit odd, but this is because we
1572  * need to maintain backward compatibility with many different
1573  * existing language translations and older sites.
1574  *
1575  * @global object
1576  * @global object
1577  * @return void
1578  */
1579 function make_default_scale() {
1580     global $CFG, $DB;
1582     $defaultscale = NULL;
1583     $defaultscale->courseid = 0;
1584     $defaultscale->userid = 0;
1585     $defaultscale->name  = get_string('separateandconnected');
1586     $defaultscale->scale = get_string('postrating1', 'forum').','.
1587                            get_string('postrating2', 'forum').','.
1588                            get_string('postrating3', 'forum');
1589     $defaultscale->timemodified = time();
1591     /// Read in the big description from the file.  Note this is not
1592     /// HTML (despite the file extension) but Moodle format text.
1593     $parentlang = get_string('parentlanguage');
1594     if ($parentlang[0] == '[') {
1595         $parentlang = '';
1596     }
1597     if (is_readable($CFG->dataroot .'/lang/'. $CFG->lang .'/help/forum/ratings.html')) {
1598         $file = file($CFG->dataroot .'/lang/'. $CFG->lang .'/help/forum/ratings.html');
1599     } else if (is_readable($CFG->dirroot .'/lang/'. $CFG->lang .'/help/forum/ratings.html')) {
1600         $file = file($CFG->dirroot .'/lang/'. $CFG->lang .'/help/forum/ratings.html');
1601     } else if ($parentlang and is_readable($CFG->dataroot .'/lang/'. $parentlang .'/help/forum/ratings.html')) {
1602         $file = file($CFG->dataroot .'/lang/'. $parentlang .'/help/forum/ratings.html');
1603     } else if ($parentlang and is_readable($CFG->dirroot .'/lang/'. $parentlang .'/help/forum/ratings.html')) {
1604         $file = file($CFG->dirroot .'/lang/'. $parentlang .'/help/forum/ratings.html');
1605     } else if (is_readable($CFG->dirroot .'/lang/en/help/forum/ratings.html')) {
1606         $file = file($CFG->dirroot .'/lang/en/help/forum/ratings.html');
1607     } else {
1608         $file = '';
1609     }
1611     $defaultscale->description = implode('', $file);
1613     if ($defaultscale->id = $DB->insert_record('scale', $defaultscale)) {
1614         $DB->execute("UPDATE {forum} SET scale = ?", array($defaultscale->id));
1615     }
1619 /**
1620  * Returns a menu of all available scales from the site as well as the given course
1621  *
1622  * @global object
1623  * @param int $courseid The id of the course as found in the 'course' table.
1624  * @return array
1625  */
1626 function get_scales_menu($courseid=0) {
1627     global $DB;
1629     $sql = "SELECT id, name
1630               FROM {scale}
1631              WHERE courseid = 0 or courseid = ?
1632           ORDER BY courseid ASC, name ASC";
1633     $params = array($courseid);
1635     if ($scales = $DB->get_records_sql_menu($sql, $params)) {
1636         return $scales;
1637     }
1639     make_default_scale();
1641     return $DB->get_records_sql_menu($sql, $params);
1646 /**
1647  * Given a set of timezone records, put them in the database,  replacing what is there
1648  *
1649  * @global object
1650  * @param array $timezones An array of timezone records
1651  * @return void
1652  */
1653 function update_timezone_records($timezones) {
1654     global $DB;
1656 /// Clear out all the old stuff
1657     $DB->delete_records('timezone');
1659 /// Insert all the new stuff
1660     foreach ($timezones as $timezone) {
1661         if (is_array($timezone)) {
1662             $timezone = (object)$timezone;
1663         }
1664         $DB->insert_record('timezone', $timezone);
1665     }
1669 /// MODULE FUNCTIONS /////////////////////////////////////////////////
1671 /**
1672  * Just gets a raw list of all modules in a course
1673  *
1674  * @global object
1675  * @param int $courseid The id of the course as found in the 'course' table.
1676  * @return array
1677  */
1678 function get_course_mods($courseid) {
1679     global $DB;
1681     if (empty($courseid)) {
1682         return false; // avoid warnings
1683     }
1685     return $DB->get_records_sql("SELECT cm.*, m.name as modname
1686                                    FROM {modules} m, {course_modules} cm
1687                                   WHERE cm.course = ? AND cm.module = m.id AND m.visible = 1",
1688                                 array($courseid)); // no disabled mods
1692 /**
1693  * Given an id of a course module, finds the coursemodule description
1694  *
1695  * @global object
1696  * @param string $modulename name of module type, eg. resource, assignment,... (optional, slower and less safe if not specified)
1697  * @param int $cmid course module id (id in course_modules table)
1698  * @param int $courseid optional course id for extra validation
1699  * @param bool $sectionnum include relative section number (0,1,2 ...)
1700  * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
1701  *                        IGNORE_MULTIPLE means return first, ignore multiple records found(not recommended);
1702  *                        MUST_EXIST means throw exception if no record or multiple records found
1703  * @return array Array of results
1704  */
1705 function get_coursemodule_from_id($modulename, $cmid, $courseid=0, $sectionnum=false, $strictness=IGNORE_MISSING) {
1706     global $DB;
1708     $params = array('cmid'=>$cmid);
1710     if (!$modulename) {
1711         if (!$modulename = $DB->get_field_sql("SELECT md.name
1712                                                  FROM {modules} md
1713                                                  JOIN {course_modules} cm ON cm.module = md.id
1714                                                 WHERE cm.id = :cmid", $params, $strictness)) {
1715             return false;
1716         }
1717     }
1719     $params['modulename'] = $modulename;
1721     $courseselect = "";
1722     $sectionfield = "";
1723     $sectionjoin  = "";
1725     if ($courseid) {
1726         $courseselect = "AND cm.course = :courseid";
1727         $params['courseid'] = $courseid;
1728     }
1730     if ($sectionnum) {
1731         $sectionfield = ", cw.section AS sectionnum";
1732         $sectionjoin  = "LEFT JOIN {course_sections} cw ON cw.id = cm.section";
1733     }
1735     $sql = "SELECT cm.*, m.name, md.name AS modname $sectionfield
1736               FROM {course_modules} cm
1737                    JOIN {modules} md ON md.id = cm.module
1738                    JOIN {".$modulename."} m ON m.id = cm.instance
1739                    $sectionjoin
1740              WHERE cm.id = :cmid AND md.name = :modulename
1741                    $courseselect";
1743     return $DB->get_record_sql($sql, $params, $strictness);
1746 /**
1747  * Given an instance number of a module, finds the coursemodule description
1748  *
1749  * @global object
1750  * @param string $modulename name of module type, eg. resource, assignment,...
1751  * @param int $instance module instance number (id in resource, assignment etc. table)
1752  * @param int $courseid optional course id for extra validation
1753  * @param bool $sectionnum include relative section number (0,1,2 ...)
1754  * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
1755  *                        IGNORE_MULTIPLE means return first, ignore multiple records found(not recommended);
1756  *                        MUST_EXIST means throw exception if no record or multiple records found
1757  * @return array Array of results
1758  */
1759 function get_coursemodule_from_instance($modulename, $instance, $courseid=0, $sectionnum=false, $strictness=IGNORE_MISSING) {
1760     global $DB;
1762     $params = array('instance'=>$instance, 'modulename'=>$modulename);
1764     $courseselect = "";
1765     $sectionfield = "";
1766     $sectionjoin  = "";
1768     if ($courseid) {
1769         $courseselect = "AND cm.course = :courseid";
1770         $params['courseid'] = $courseid;
1771     }
1773     if ($sectionnum) {
1774         $sectionfield = ", cw.section AS sectionnum";
1775         $sectionjoin  = "LEFT JOIN {course_sections} cw ON cw.id = cm.section";
1776     }
1778     $sql = "SELECT cm.*, m.name, md.name AS modname $sectionfield
1779               FROM {course_modules} cm
1780                    JOIN {modules} md ON md.id = cm.module
1781                    JOIN {".$modulename."} m ON m.id = cm.instance
1782                    $sectionjoin
1783              WHERE m.id = :instance AND md.name = :modulename
1784                    $courseselect";
1786     return $DB->get_record_sql($sql, $params, $strictness);
1789 /**
1790  * Returns all course modules of given activity in course
1791  *
1792  * @param string $modulename The module name (forum, quiz, etc.)
1793  * @param int $courseid The course id to get modules for
1794  * @param string $extrafields extra fields starting with m.
1795  * @return array Array of results
1796  */
1797 function get_coursemodules_in_course($modulename, $courseid, $extrafields='') {
1798     global $DB;
1800     if (!empty($extrafields)) {
1801         $extrafields = ", $extrafields";
1802     }
1803     $params = array();
1804     $params['courseid'] = $courseid;
1805     $params['modulename'] = $modulename;
1808     return $DB->get_records_sql("SELECT cm.*, m.name, md.name as modname $extrafields
1809                                    FROM {course_modules} cm, {modules} md, {".$modulename."} m
1810                                   WHERE cm.course = :courseid AND
1811                                         cm.instance = m.id AND
1812                                         md.name = :modulename AND
1813                                         md.id = cm.module", $params);
1816 /**
1817  * Returns an array of all the active instances of a particular module in given courses, sorted in the order they are defined
1818  *
1819  * Returns an array of all the active instances of a particular
1820  * module in given courses, sorted in the order they are defined
1821  * in the course. Returns an empty array on any errors.
1822  *
1823  * The returned objects includle the columns cw.section, cm.visible,
1824  * cm.groupmode and cm.groupingid, cm.groupmembersonly, and are indexed by cm.id.
1825  *
1826  * @global object
1827  * @global object
1828  * @param string $modulename The name of the module to get instances for
1829  * @param array $courses an array of course objects.
1830  * @param int $userid
1831  * @param int $includeinvisible
1832  * @return array of module instance objects, including some extra fields from the course_modules
1833  *          and course_sections tables, or an empty array if an error occurred.
1834  */
1835 function get_all_instances_in_courses($modulename, $courses, $userid=NULL, $includeinvisible=false) {
1836     global $CFG, $DB;
1838     $outputarray = array();
1840     if (empty($courses) || !is_array($courses) || count($courses) == 0) {
1841         return $outputarray;
1842     }
1844     list($coursessql, $params) = $DB->get_in_or_equal(array_keys($courses), SQL_PARAMS_NAMED, 'c0');
1845     $params['modulename'] = $modulename;
1847     if (!$rawmods = $DB->get_records_sql("SELECT cm.id AS coursemodule, m.*, cw.section, cm.visible AS visible,
1848                                                  cm.groupmode, cm.groupingid, cm.groupmembersonly
1849                                             FROM {course_modules} cm, {course_sections} cw, {modules} md,
1850                                                  {".$modulename."} m
1851                                            WHERE cm.course $coursessql AND
1852                                                  cm.instance = m.id AND
1853                                                  cm.section = cw.id AND
1854                                                  md.name = :modulename AND
1855                                                  md.id = cm.module", $params)) {
1856         return $outputarray;
1857     }
1859     foreach ($courses as $course) {
1860         $modinfo = get_fast_modinfo($course, $userid);
1862         if (empty($modinfo->instances[$modulename])) {
1863             continue;
1864         }
1866         foreach ($modinfo->instances[$modulename] as $cm) {
1867             if (!$includeinvisible and !$cm->uservisible) {
1868                 continue;
1869             }
1870             if (!isset($rawmods[$cm->id])) {
1871                 continue;
1872             }
1873             $instance = $rawmods[$cm->id];
1874             if (!empty($cm->extra)) {
1875                 $instance->extra = $cm->extra;
1876             }
1877             $outputarray[] = $instance;
1878         }
1879     }
1881     return $outputarray;
1884 /**
1885  * Returns an array of all the active instances of a particular module in a given course,
1886  * sorted in the order they are defined.
1887  *
1888  * Returns an array of all the active instances of a particular
1889  * module in a given course, sorted in the order they are defined
1890  * in the course. Returns an empty array on any errors.
1891  *
1892  * The returned objects includle the columns cw.section, cm.visible,
1893  * cm.groupmode and cm.groupingid, cm.groupmembersonly, and are indexed by cm.id.
1894  *
1895  * Simply calls {@link all_instances_in_courses()} with a single provided course
1896  *
1897  * @param string $modulename The name of the module to get instances for
1898  * @param object $course The course obect.
1899  * @return array of module instance objects, including some extra fields from the course_modules
1900  *          and course_sections tables, or an empty array if an error occurred.
1901  * @param int $userid
1902  * @param int $includeinvisible
1903  */
1904 function get_all_instances_in_course($modulename, $course, $userid=NULL, $includeinvisible=false) {
1905     return get_all_instances_in_courses($modulename, array($course->id => $course), $userid, $includeinvisible);
1909 /**
1910  * Determine whether a module instance is visible within a course
1911  *
1912  * Given a valid module object with info about the id and course,
1913  * and the module's type (eg "forum") returns whether the object
1914  * is visible or not, groupmembersonly visibility not tested
1915  *
1916  * @global object
1918  * @param $moduletype Name of the module eg 'forum'
1919  * @param $module Object which is the instance of the module
1920  * @return bool Success
1921  */
1922 function instance_is_visible($moduletype, $module) {
1923     global $DB;
1925     if (!empty($module->id)) {
1926         $params = array('courseid'=>$module->course, 'moduletype'=>$moduletype, 'moduleid'=>$module->id);
1927         if ($records = $DB->get_records_sql("SELECT cm.instance, cm.visible, cm.groupingid, cm.id, cm.groupmembersonly, cm.course
1928                                                FROM {course_modules} cm, {modules} m
1929                                               WHERE cm.course = :courseid AND
1930                                                     cm.module = m.id AND
1931                                                     m.name = :moduletype AND
1932                                                     cm.instance = :moduleid", $params)) {
1934             foreach ($records as $record) { // there should only be one - use the first one
1935                 return $record->visible;
1936             }
1937         }
1938     }
1939     return true;  // visible by default!
1942 /**
1943  * Determine whether a course module is visible within a course,
1944  * this is different from instance_is_visible() - faster and visibility for user
1945  *
1946  * @global object
1947  * @global object
1948  * @uses DEBUG_DEVELOPER
1949  * @uses CONTEXT_MODULE
1950  * @uses CONDITION_MISSING_EXTRATABLE
1951  * @param object $cm object
1952  * @param int $userid empty means current user
1953  * @return bool Success
1954  */
1955 function coursemodule_visible_for_user($cm, $userid=0) {
1956     global $USER,$CFG;
1958     if (empty($cm->id)) {
1959         debugging("Incorrect course module parameter!", DEBUG_DEVELOPER);
1960         return false;
1961     }
1962     if (empty($userid)) {
1963         $userid = $USER->id;
1964     }
1965     if (!$cm->visible and !has_capability('moodle/course:viewhiddenactivities', get_context_instance(CONTEXT_MODULE, $cm->id), $userid)) {
1966         return false;
1967     }
1968     if ($CFG->enableavailability) {
1969         require_once($CFG->libdir.'/conditionlib.php');
1970         $ci=new condition_info($cm,CONDITION_MISSING_EXTRATABLE);
1971         if(!$ci->is_available($cm->availableinfo,false,$userid) and
1972             !has_capability('moodle/course:viewhiddenactivities',
1973                 get_context_instance(CONTEXT_MODULE, $cm->id), $userid)) {
1974             return false;
1975         }
1976     }
1977     return groups_course_module_visible($cm, $userid);
1983 /// LOG FUNCTIONS /////////////////////////////////////////////////////
1986 /**
1987  * Add an entry to the log table.
1988  *
1989  * Add an entry to the log table.  These are "action" focussed rather
1990  * than web server hits, and provide a way to easily reconstruct what
1991  * any particular student has been doing.
1992  *
1993  * @global object
1994  * @global object
1995  * @global object
1996  * @uses SITEID
1997  * @uses DEBUG_DEVELOPER
1998  * @uses DEBUG_ALL
1999  * @param    int     $courseid  The course id
2000  * @param    string  $module  The module name - e.g. forum, journal, resource, course, user etc
2001  * @param    string  $action  'view', 'update', 'add' or 'delete', possibly followed by another word to clarify.
2002  * @param    string  $url     The file and parameters used to see the results of the action
2003  * @param    string  $info    Additional description information
2004  * @param    string  $cm      The course_module->id if there is one
2005  * @param    string  $user    If log regards $user other than $USER
2006  * @return void
2007  */
2008 function add_to_log($courseid, $module, $action, $url='', $info='', $cm=0, $user=0) {
2009     // Note that this function intentionally does not follow the normal Moodle DB access idioms.
2010     // This is for a good reason: it is the most frequently used DB update function,
2011     // so it has been optimised for speed.
2012     global $DB, $CFG, $USER;
2014     if ($cm === '' || is_null($cm)) { // postgres won't translate empty string to its default
2015         $cm = 0;
2016     }
2018     if ($user) {
2019         $userid = $user;
2020     } else {
2021         if (session_is_loggedinas()) {  // Don't log
2022             return;
2023         }
2024         $userid = empty($USER->id) ? '0' : $USER->id;
2025     }
2027     $REMOTE_ADDR = getremoteaddr();
2028     if (empty($REMOTE_ADDR)) {
2029         $REMOTE_ADDR = '0.0.0.0';
2030     }
2032     $timenow = time();
2033     $info = $info;
2034     if (!empty($url)) { // could break doing html_entity_decode on an empty var.
2035         $url = html_entity_decode($url); // for php < 4.3.0 this is defined in moodlelib.php
2036     }
2038     // Restrict length of log lines to the space actually available in the
2039     // database so that it doesn't cause a DB error. Log a warning so that
2040     // developers can avoid doing things which are likely to cause this on a
2041     // routine basis.
2042     $tl = textlib_get_instance();
2043     if(!empty($info) && $tl->strlen($info)>255) {
2044         $info = $tl->substr($info,0,252).'...';
2045         debugging('Warning: logged very long info',DEBUG_DEVELOPER);
2046     }
2048     // If the 100 field size is changed, also need to alter print_log in course/lib.php
2049     if(!empty($url) && $tl->strlen($url)>100) {
2050         $url=$tl->substr($url,0,97).'...';
2051         debugging('Warning: logged very long URL',DEBUG_DEVELOPER);
2052     }
2054     if (defined('MDL_PERFDB')) { global $PERF ; $PERF->logwrites++;};
2056     $log = array('time'=>$timenow, 'userid'=>$userid, 'course'=>$courseid, 'ip'=>$REMOTE_ADDR, 'module'=>$module,
2057                  'cmid'=>$cm, 'action'=>$action, 'url'=>$url, 'info'=>$info);
2058     $result = $DB->insert_record_raw('log', $log, false);
2060     // MDL-11893, alert $CFG->supportemail if insert into log failed
2061     if (!$result and $CFG->supportemail and empty($CFG->noemailever)) {
2062         // email_to_user is not usable because email_to_user tries to write to the logs table,
2063         // and this will get caught in an infinite loop, if disk is full
2064         $site = get_site();
2065         $subject = 'Insert into log failed at your moodle site '.$site->fullname;
2066         $message = "Insert into log table failed at ". date('l dS \of F Y h:i:s A') .".\n It is possible that your disk is full.\n\n";
2067         $message .= "The failed query parameters are:\n\n" . var_export($log, true);
2069         $lasttime = get_config('admin', 'lastloginserterrormail');
2070         if(empty($lasttime) || time() - $lasttime > 60*60*24) { // limit to 1 email per day
2071             mail($CFG->supportemail, $subject, $message);
2072             set_config('lastloginserterrormail', time(), 'admin');
2073         }
2074     }
2076     if (!$result) {
2077         debugging('Error: Could not insert a new entry to the Moodle log', DEBUG_ALL);
2078     }
2082 /**
2083  * Store user last access times - called when use enters a course or site
2084  *
2085  * @global object
2086  * @global object
2087  * @global object
2088  * @uses LASTACCESS_UPDATE_SECS
2089  * @uses SITEID
2090  * @param int $courseid, empty means site
2091  * @return void
2092  */
2093 function user_accesstime_log($courseid=0) {
2094     global $USER, $CFG, $DB;
2096     if (!isloggedin() or session_is_loggedinas()) {
2097         // no access tracking
2098         return;
2099     }
2101     if (empty($courseid)) {
2102         $courseid = SITEID;
2103     }
2105     $timenow = time();
2107 /// Store site lastaccess time for the current user
2108     if ($timenow - $USER->lastaccess > LASTACCESS_UPDATE_SECS) {
2109     /// Update $USER->lastaccess for next checks
2110         $USER->lastaccess = $timenow;
2112         $last = new object();
2113         $last->id         = $USER->id;
2114         $last->lastip     = getremoteaddr();
2115         $last->lastaccess = $timenow;
2117         $DB->update_record_raw('user', $last);
2118     }
2120     if ($courseid == SITEID) {
2121     ///  no user_lastaccess for frontpage
2122         return;
2123     }
2125 /// Store course lastaccess times for the current user
2126     if (empty($USER->currentcourseaccess[$courseid]) or ($timenow - $USER->currentcourseaccess[$courseid] > LASTACCESS_UPDATE_SECS)) {
2128         $lastaccess = $DB->get_field('user_lastaccess', 'timeaccess', array('userid'=>$USER->id, 'courseid'=>$courseid));
2130         if ($lastaccess === false) {
2131             // Update course lastaccess for next checks
2132             $USER->currentcourseaccess[$courseid] = $timenow;
2134             $last = new object();
2135             $last->userid     = $USER->id;
2136             $last->courseid   = $courseid;
2137             $last->timeaccess = $timenow;
2138             $DB->insert_record_raw('user_lastaccess', $last, false);
2140         } else if ($timenow - $lastaccess <  LASTACCESS_UPDATE_SECS) {
2141             // no need to update now, it was updated recently in concurrent login ;-)
2143         } else {
2144             // Update course lastaccess for next checks
2145             $USER->currentcourseaccess[$courseid] = $timenow;
2147             $DB->set_field('user_lastaccess', 'timeaccess', $timenow, array('userid'=>$USER->id, 'courseid'=>$courseid));
2148         }
2149     }
2152 /**
2153  * Select all log records based on SQL criteria
2154  *
2155  * @todo Finish documenting this function
2156  *
2157  * @global object
2158  * @param string $select SQL select criteria
2159  * @param array $params named sql type params
2160  * @param string $order SQL order by clause to sort the records returned
2161  * @param string $limitfrom ?
2162  * @param int $limitnum ?
2163  * @param int $totalcount Passed in by reference.
2164  * @return object
2165  */
2166 function get_logs($select, array $params=null, $order='l.time DESC', $limitfrom='', $limitnum='', &$totalcount) {
2167     global $DB;
2169     if ($order) {
2170         $order = "ORDER BY $order";
2171     }
2173     $selectsql = "";
2174     $countsql  = "";
2176     if ($select) {
2177         $select = "WHERE $select";
2178     }
2180     $sql = "SELECT COUNT(*)
2181               FROM {log} l
2182            $select";
2184     $totalcount = $DB->count_records_sql($sql, $params);
2186     $sql = "SELECT l.*, u.firstname, u.lastname, u.picture
2187               FROM {log} l
2188               LEFT JOIN {user} u ON l.userid = u.id
2189            $select
2190             $order";
2192     return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum) ;
2196 /**
2197  * Select all log records for a given course and user
2198  *
2199  * @todo Finish documenting this function
2200  *
2201  * @global object
2202  * @uses DAYSECS
2203  * @param int $userid The id of the user as found in the 'user' table.
2204  * @param int $courseid The id of the course as found in the 'course' table.
2205  * @param string $coursestart ?
2206  */
2207 function get_logs_usercourse($userid, $courseid, $coursestart) {
2208     global $DB;
2210     $params = array();
2212     $courseselect = '';
2213     if ($courseid) {
2214         $courseselect = "AND course = :courseid";
2215         $params['courseid'] = $courseid;
2216     }
2217     $params['userid'] = $userid;
2218     $params['coursestart'] = $coursestart;
2220     return $DB->get_records_sql("SELECT FLOOR((time - :coursestart)/". DAYSECS .") AS day, COUNT(*) AS num
2221                                    FROM {log}
2222                                   WHERE userid = :userid
2223                                         AND time > :coursestart $courseselect
2224                                GROUP BY FLOOR((time - :coursestart)/". DAYSECS .")", $params);
2227 /**
2228  * Select all log records for a given course, user, and day
2229  *
2230  * @global object
2231  * @uses HOURSECS
2232  * @param int $userid The id of the user as found in the 'user' table.
2233  * @param int $courseid The id of the course as found in the 'course' table.
2234  * @param string $daystart ?
2235  * @return object
2236  */
2237 function get_logs_userday($userid, $courseid, $daystart) {
2238     global $DB;
2240     $params = array($daystart, $userid, $daystart);
2242     $courseselect = '';
2243     if ($courseid) {
2244         $courseselect = "AND course = ?";
2245         $params[] = $courseid;
2246     }
2247     $params[] = $daystart;
2249     return $DB->get_records_sql("SELECT FLOOR((time - ?)/". HOURSECS .") AS hour, COUNT(*) AS num
2250                                    FROM {log}
2251                                   WHERE userid = ?
2252                                         AND time > ? $courseselect
2253                                GROUP BY FLOOR((time - ?)/". HOURSECS .") ", $params);
2256 /**
2257  * Returns an object with counts of failed login attempts
2258  *
2259  * Returns information about failed login attempts.  If the current user is
2260  * an admin, then two numbers are returned:  the number of attempts and the
2261  * number of accounts.  For non-admins, only the attempts on the given user
2262  * are shown.
2263  *
2264  * @global object
2265  * @uses CONTEXT_SYSTEM
2266  * @param string $mode Either 'admin' or 'everybody'
2267  * @param string $username The username we are searching for
2268  * @param string $lastlogin The date from which we are searching
2269  * @return int
2270  */
2271 function count_login_failures($mode, $username, $lastlogin) {
2272     global $DB;
2274     $params = array('mode'=>$mode, 'username'=>$username, 'lastlogin'=>$lastlogin);
2275     $select = "module='login' AND action='error' AND time > :lastlogin";
2277     $count = new object();
2279     if (is_siteadmin()) {
2280         if ($count->attempts = $DB->count_records_select('log', $select, $params)) {
2281             $count->accounts = $DB->count_records_select('log', $select, $params, 'COUNT(DISTINCT info)');
2282             return $count;
2283         }
2284     } else if ($mode == 'everybody') {
2285         if ($count->attempts = $DB->count_records_select('log', "$select AND info = :username", $params)) {
2286             return $count;
2287         }
2288     }
2289     return NULL;
2293 /// GENERAL HELPFUL THINGS  ///////////////////////////////////
2295 /**
2296  * Dump a given object's information in a PRE block.
2297  *
2298  * Mostly just used for debugging.
2299  *
2300  * @param mixed $object The data to be printed
2301  * @return void OUtput is echo'd
2302  */
2303 function print_object($object) {
2304     echo '<pre class="notifytiny">' . htmlspecialchars(print_r($object,true)) . '</pre>';
2307 /**
2308  * Check whether a course is visible through its parents
2309  * path.
2310  *
2311  * Notes:
2312  *
2313  * - All we need from the course is ->category. _However_
2314  *   if the course object has a categorypath property,
2315  *   we'll save a dbquery
2316  *
2317  * - If we return false, you'll still need to check if
2318  *   the user can has the 'moodle/category:viewhiddencategories'
2319  *   capability...
2320  *
2321  * - Will generate 2 DB calls.
2322  *
2323  * - It does have a small local cache, however...
2324  *
2325  * - Do NOT call this over many courses as it'll generate
2326  *   DB traffic. Instead, see what get_my_courses() does.
2327  *
2328  * @global object
2329  * @global object
2330  * @staticvar array $mycache
2331  * @param object $course A course object
2332  * @return bool
2333  */
2334 function course_parent_visible($course = null) {
2335     global $CFG, $DB;
2336     //return true;
2337     static $mycache;
2339     if (!is_object($course)) {
2340         return true;
2341     }
2342     if (!empty($CFG->allowvisiblecoursesinhiddencategories)) {
2343         return true;
2344     }
2346     if (!isset($mycache)) {
2347         $mycache = array();
2348     } else {
2349         // cast to force assoc array
2350         $k = (string)$course->category;
2351         if (isset($mycache[$k])) {
2352             return $mycache[$k];
2353         }
2354     }
2356     if (isset($course->categorypath)) {
2357         $path = $course->categorypath;
2358     } else {
2359         $path = $DB->get_field('course_categories', 'path', array('id'=>$course->category));
2360     }
2361     $catids = substr($path,1); // strip leading slash
2362     $catids = str_replace('/',',',$catids);
2364     $sql = "SELECT MIN(visible)
2365               FROM {course_categories}
2366              WHERE id IN ($catids)";
2367     $vis = $DB->get_field_sql($sql);
2369     // cast to force assoc array
2370     $k = (string)$course->category;
2371     $mycache[$k] = $vis;
2373     return $vis;
2376 /**
2377  * This function is the official hook inside XMLDB stuff to delegate its debug to one
2378  * external function.
2379  *
2380  * Any script can avoid calls to this function by defining XMLDB_SKIP_DEBUG_HOOK before
2381  * using XMLDB classes. Obviously, also, if this function doesn't exist, it isn't invoked ;-)
2382  *
2383  * @uses DEBUG_DEVELOPER
2384  * @param string $message string contains the error message
2385  * @param object $object object XMLDB object that fired the debug
2386  */
2387 function xmldb_debug($message, $object) {
2389     debugging($message, DEBUG_DEVELOPER);
2392 /**
2393  * @global object
2394  * @uses CONTEXT_COURSECAT
2395  * @return boolean Whether the user can create courses in any category in the system.
2396  */
2397 function user_can_create_courses() {
2398     global $DB;
2399     $catsrs = $DB->get_recordset('course_categories');
2400     foreach ($catsrs as $cat) {
2401         if (has_capability('moodle/course:create', get_context_instance(CONTEXT_COURSECAT, $cat->id))) {
2402             $catsrs->close();
2403             return true;
2404         }
2405     }
2406     $catsrs->close();
2407     return false;