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