Index: temp_moodle_dev/moodle/lib/datalib.php =================================================================== diff -u --- temp_moodle_dev/moodle/lib/datalib.php (revision 0) +++ temp_moodle_dev/moodle/lib/datalib.php (revision 3163c863d928a9e392249661266bb38a09936312) @@ -0,0 +1,2280 @@ +$value) { + $a[$key] = addslashes( $value ); + } + return (object)$a; +} + +/// USER DATABASE //////////////////////////////////////////////// + +/** + * Returns $user object of the main admin user + * primary admin = admin with lowest role_assignment id among admins + * @uses $CFG + * @return object(admin) An associative array representing the admin user. + */ +function get_admin () { + + global $CFG; + static $myadmin; + + if (isset($myadmin)) { + return $myadmin; + } + + if ( $admins = get_admins() ) { + foreach ($admins as $admin) { + $myadmin = $admin; + return $admin; // ie the first one + } + } else { + return false; + } +} + +/** + * Returns list of all admins, using 1 DB query. It depends on DB schema v1.7 + * but does not depend on the v1.9 datastructures (context.path, etc). + * + * @uses $CFG + * @return object + */ +function get_admins() { + + global $CFG; + + $sql = "SELECT ra.userid, SUM(rc.permission) AS permission, MIN(ra.id) AS adminid + FROM " . $CFG->prefix . "role_capabilities rc + JOIN " . $CFG->prefix . "context ctx + ON ctx.id=rc.contextid + JOIN " . $CFG->prefix . "role_assignments ra + ON ra.roleid=rc.roleid AND ra.contextid=ctx.id + WHERE ctx.contextlevel=10 + AND rc.capability IN ('moodle/site:config', + 'moodle/legacy:admin', + 'moodle/site:doanything') + GROUP BY ra.userid + HAVING SUM(rc.permission) > 0"; + + $sql = "SELECT u.*, ra.adminid + FROM " . $CFG->prefix . "user u + JOIN ($sql) ra + ON u.id=ra.userid + ORDER BY ra.adminid ASC"; + + return get_records_sql($sql); +} + + +function get_courses_in_metacourse($metacourseid) { + global $CFG; + + $sql = "SELECT c.id,c.shortname,c.fullname FROM {$CFG->prefix}course c, {$CFG->prefix}course_meta mc WHERE mc.parent_course = $metacourseid + AND mc.child_course = c.id ORDER BY c.shortname"; + + return get_records_sql($sql); +} + +function get_courses_notin_metacourse($metacourseid,$count=false) { + + global $CFG; + + if ($count) { + $sql = "SELECT COUNT(c.id)"; + } else { + $sql = "SELECT c.id,c.shortname,c.fullname"; + } + + $alreadycourses = get_courses_in_metacourse($metacourseid); + + $sql .= " FROM {$CFG->prefix}course c WHERE ".((!empty($alreadycourses)) ? "c.id NOT IN (".implode(',',array_keys($alreadycourses)).") + AND " : "")." c.id !=$metacourseid and c.id != ".SITEID." and c.metacourse != 1 ".((empty($count)) ? " ORDER BY c.shortname" : ""); + + return get_records_sql($sql); +} + +function count_courses_notin_metacourse($metacourseid) { + global $CFG; + + $alreadycourses = get_courses_in_metacourse($metacourseid); + + $sql = "SELECT COUNT(c.id) AS notin FROM {$CFG->prefix}course c + WHERE ".((!empty($alreadycourses)) ? "c.id NOT IN (".implode(',',array_keys($alreadycourses)).") + AND " : "")." c.id !=$metacourseid and c.id != ".SITEID." and c.metacourse != 1"; + + if (!$count = get_record_sql($sql)) { + return 0; + } + + return $count->notin; +} + +/** + * Search through course users + * + * If $coursid specifies the site course then this function searches + * through all undeleted and confirmed users + * + * @uses $CFG + * @uses SITEID + * @param int $courseid The course in question. + * @param int $groupid The group in question. + * @param string $searchtext ? + * @param string $sort ? + * @param string $exceptions ? + * @return object + */ +function search_users($courseid, $groupid, $searchtext, $sort='', $exceptions='') { + global $CFG; + + $LIKE = sql_ilike(); + $fullname = sql_fullname('u.firstname', 'u.lastname'); + + if (!empty($exceptions)) { + $except = ' AND u.id NOT IN ('. $exceptions .') '; + } else { + $except = ''; + } + + if (!empty($sort)) { + $order = ' ORDER BY '. $sort; + } else { + $order = ''; + } + + $select = 'u.deleted = \'0\' AND u.confirmed = \'1\''; + + if (!$courseid or $courseid == SITEID) { + return get_records_sql("SELECT u.id, u.firstname, u.lastname, u.email + FROM {$CFG->prefix}user u + WHERE $select + AND ($fullname $LIKE '%$searchtext%' OR u.email $LIKE '%$searchtext%') + $except $order"); + } else { + + if ($groupid) { +//TODO:check. Remove group DB dependencies. + return get_records_sql("SELECT u.id, u.firstname, u.lastname, u.email + FROM {$CFG->prefix}user u, + {$CFG->prefix}groups_members gm + WHERE $select AND gm.groupid = '$groupid' AND gm.userid = u.id + AND ($fullname $LIKE '%$searchtext%' OR u.email $LIKE '%$searchtext%') + $except $order"); + } else { + $context = get_context_instance(CONTEXT_COURSE, $courseid); + $contextlists = get_related_contexts_string($context); + $users = get_records_sql("SELECT u.id, u.firstname, u.lastname, u.email + FROM {$CFG->prefix}user u, + {$CFG->prefix}role_assignments ra + WHERE $select AND ra.contextid $contextlists AND ra.userid = u.id + AND ($fullname $LIKE '%$searchtext%' OR u.email $LIKE '%$searchtext%') + $except $order"); + } + return $users; + } +} + + +/** + * Returns a list of all site users + * Obsolete, just calls get_course_users(SITEID) + * + * @uses SITEID + * @deprecated Use {@link get_course_users()} instead. + * @param string $fields A comma separated list of fields to be returned from the chosen table. + * @return object|false {@link $USER} records or false if error. + */ +function get_site_users($sort='u.lastaccess DESC', $fields='*', $exceptions='') { + + return get_course_users(SITEID, $sort, $exceptions, $fields); +} + + +/** + * Returns a subset of users + * + * @uses $CFG + * @param bool $get If false then only a count of the records is returned + * @param string $search A simple string to search for + * @param bool $confirmed A switch to allow/disallow unconfirmed users + * @param array(int) $exceptions A list of IDs to ignore, eg 2,4,5,8,9,10 + * @param string $sort A SQL snippet for the sorting criteria to use + * @param string $firstinitial ? + * @param string $lastinitial ? + * @param string $page ? + * @param string $recordsperpage ? + * @param string $fields A comma separated list of fields to be returned from the chosen table. + * @return object|false|int {@link $USER} records unless get is false in which case the integer count of the records found is returned. False is returned if an error is encountered. + */ +function get_users($get=true, $search='', $confirmed=false, $exceptions='', $sort='firstname ASC', + $firstinitial='', $lastinitial='', $page='', $recordsperpage='', $fields='*', $extraselect='') { + + global $CFG; + + if ($get && !$recordsperpage) { + debugging('Call to get_users with $get = true no $recordsperpage limit. ' . + 'On large installations, this will probably cause an out of memory error. ' . + 'Please think again and change your code so that it does not try to ' . + 'load so much data into memory.', DEBUG_DEVELOPER); + } + + $LIKE = sql_ilike(); + $fullname = sql_fullname(); + + $select = 'username <> \'guest\' AND deleted = 0'; + + if (!empty($search)){ + $search = trim($search); + $select .= " AND ($fullname $LIKE '%$search%' OR email $LIKE '%$search%') "; + } + + if ($confirmed) { + $select .= ' AND confirmed = \'1\' '; + } + + if ($exceptions) { + $select .= ' AND id NOT IN ('. $exceptions .') '; + } + + if ($firstinitial) { + $select .= ' AND firstname '. $LIKE .' \''. $firstinitial .'%\''; + } + if ($lastinitial) { + $select .= ' AND lastname '. $LIKE .' \''. $lastinitial .'%\''; + } + + if ($extraselect) { + $select .= " AND $extraselect "; + } + + if ($get) { + return get_records_select('user', $select, $sort, $fields, $page, $recordsperpage); + } else { + return count_records_select('user', $select); + } +} + + +/** + * shortdesc (optional) + * + * longdesc + * + * @uses $CFG + * @param string $sort ? + * @param string $dir ? + * @param int $categoryid ? + * @param int $categoryid ? + * @param string $search ? + * @param string $firstinitial ? + * @param string $lastinitial ? + * @returnobject {@link $USER} records + * @todo Finish documenting this function + */ + +function get_users_listing($sort='lastaccess', $dir='ASC', $page=0, $recordsperpage=0, + $search='', $firstinitial='', $lastinitial='', $extraselect='') { + + global $CFG; + + $LIKE = sql_ilike(); + $fullname = sql_fullname(); + + $select = "deleted <> '1'"; + + if (!empty($search)) { + $search = trim($search); + $select .= " AND ($fullname $LIKE '%$search%' OR email $LIKE '%$search%' OR username='$search') "; + } + + if ($firstinitial) { + $select .= ' AND firstname '. $LIKE .' \''. $firstinitial .'%\' '; + } + + if ($lastinitial) { + $select .= ' AND lastname '. $LIKE .' \''. $lastinitial .'%\' '; + } + + if ($extraselect) { + $select .= " AND $extraselect "; + } + + if ($sort) { + $sort = ' ORDER BY '. $sort .' '. $dir; + } + +/// warning: will return UNCONFIRMED USERS + return get_records_sql("SELECT id, username, email, firstname, lastname, city, country, lastaccess, confirmed, mnethostid + FROM {$CFG->prefix}user + WHERE $select $sort", $page, $recordsperpage); + +} + + +/** + * Full list of users that have confirmed their accounts. + * + * @uses $CFG + * @return object + */ +function get_users_confirmed() { + global $CFG; + return get_records_sql("SELECT * + FROM {$CFG->prefix}user + WHERE confirmed = 1 + AND deleted = 0 + AND username <> 'guest'"); +} + + +/// OTHER SITE AND COURSE FUNCTIONS ///////////////////////////////////////////// + + +/** + * Returns $course object of the top-level site. + * + * @return course A {@link $COURSE} object for the site + */ +function get_site() { + + global $SITE; + + if (!empty($SITE->id)) { // We already have a global to use, so return that + return $SITE; + } + + if ($course = get_record('course', 'category', 0)) { + return $course; + } else { + return false; + } +} + +/** + * Returns list of courses, for whole site, or category + * + * Returns list of courses, for whole site, or category + * Important: Using c.* for fields is extremely expensive because + * we are using distinct. You almost _NEVER_ need all the fields + * in such a large SELECT + * + * @param type description + * + */ +function get_courses($categoryid="all", $sort="c.sortorder ASC", $fields="c.*") { + + global $USER, $CFG; + + if ($categoryid != "all" && is_numeric($categoryid)) { + $categoryselect = "WHERE c.category = '$categoryid'"; + } else { + $categoryselect = ""; + } + + if (empty($sort)) { + $sortstatement = ""; + } else { + $sortstatement = "ORDER BY $sort"; + } + + $visiblecourses = array(); + + // pull out all course matching the cat + if ($courses = get_records_sql("SELECT $fields, + ctx.id AS ctxid, ctx.path AS ctxpath, + ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel + FROM {$CFG->prefix}course c + JOIN {$CFG->prefix}context ctx + ON (c.id = ctx.instanceid + AND ctx.contextlevel=".CONTEXT_COURSE.") + $categoryselect + $sortstatement")) { + + // loop throught them + foreach ($courses as $course) { + $course = make_context_subobj($course); + if (isset($course->visible) && $course->visible <= 0) { + // for hidden courses, require visibility check + if (has_capability('moodle/course:viewhiddencourses', $course->context)) { + $visiblecourses [] = $course; + } + } else { + $visiblecourses [] = $course; + } + } + } + return $visiblecourses; + +/* + $teachertable = ""; + $visiblecourses = ""; + $sqland = ""; + if (!empty($categoryselect)) { + $sqland = "AND "; + } + if (!empty($USER->id)) { // May need to check they are a teacher + if (!has_capability('moodle/course:create', get_context_instance(CONTEXT_SYSTEM))) { + $visiblecourses = "$sqland ((c.visible > 0) OR t.userid = '$USER->id')"; + $teachertable = "LEFT JOIN {$CFG->prefix}user_teachers t ON t.course = c.id"; + } + } else { + $visiblecourses = "$sqland c.visible > 0"; + } + + if ($categoryselect or $visiblecourses) { + $selectsql = "{$CFG->prefix}course c $teachertable WHERE $categoryselect $visiblecourses"; + } else { + $selectsql = "{$CFG->prefix}course c $teachertable"; + } + + $extrafield = str_replace('ASC','',$sort); + $extrafield = str_replace('DESC','',$extrafield); + $extrafield = trim($extrafield); + if (!empty($extrafield)) { + $extrafield = ','.$extrafield; + } + return get_records_sql("SELECT ".((!empty($teachertable)) ? " DISTINCT " : "")." $fields $extrafield FROM $selectsql ".((!empty($sort)) ? "ORDER BY $sort" : "")); + */ +} + + +/** + * Returns list of courses, for whole site, or category + * + * Similar to get_courses, but allows paging + * Important: Using c.* for fields is extremely expensive because + * we are using distinct. You almost _NEVER_ need all the fields + * in such a large SELECT + * + * @param type description + * + */ +function get_courses_page($categoryid="all", $sort="c.sortorder ASC", $fields="c.*", + &$totalcount, $limitfrom="", $limitnum="") { + + global $USER, $CFG; + + $categoryselect = ""; + if ($categoryid != "all" && is_numeric($categoryid)) { + $categoryselect = "WHERE c.category = '$categoryid'"; + } else { + $categoryselect = ""; + } + + // pull out all course matching the cat + $visiblecourses = array(); + if (!($rs = get_recordset_sql("SELECT $fields, + ctx.id AS ctxid, ctx.path AS ctxpath, + ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel + FROM {$CFG->prefix}course c + JOIN {$CFG->prefix}context ctx + ON (c.id = ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.") + $categoryselect + ORDER BY $sort"))) { + return $visiblecourses; + } + $totalcount = 0; + + if (!$limitfrom) { + $limitfrom = 0; + } + + // iteration will have to be done inside loop to keep track of the limitfrom and limitnum + while ($course = rs_fetch_next_record($rs)) { + $course = make_context_subobj($course); + if ($course->visible <= 0) { + // for hidden courses, require visibility check + if (has_capability('moodle/course:viewhiddencourses', $course->context)) { + $totalcount++; + if ($totalcount > $limitfrom && (!$limitnum or count($visiblecourses) < $limitnum)) { + $visiblecourses [] = $course; + } + } + } else { + $totalcount++; + if ($totalcount > $limitfrom && (!$limitnum or count($visiblecourses) < $limitnum)) { + $visiblecourses [] = $course; + } + } + } + rs_close($rs); + return $visiblecourses; + +/** + + $categoryselect = ""; + if ($categoryid != "all" && is_numeric($categoryid)) { + $categoryselect = "c.category = '$categoryid'"; + } + + $teachertable = ""; + $visiblecourses = ""; + $sqland = ""; + if (!empty($categoryselect)) { + $sqland = "AND "; + } + if (!empty($USER) and !empty($USER->id)) { // May need to check they are a teacher + if (!has_capability('moodle/course:create', get_context_instance(CONTEXT_SYSTEM))) { + $visiblecourses = "$sqland ((c.visible > 0) OR t.userid = '$USER->id')"; + $teachertable = "LEFT JOIN {$CFG->prefix}user_teachers t ON t.course=c.id"; + } + } else { + $visiblecourses = "$sqland c.visible > 0"; + } + + if ($limitfrom !== "") { + $limit = sql_paging_limit($limitfrom, $limitnum); + } else { + $limit = ""; + } + + $selectsql = "{$CFG->prefix}course c $teachertable WHERE $categoryselect $visiblecourses"; + + $totalcount = count_records_sql("SELECT COUNT(DISTINCT c.id) FROM $selectsql"); + + return get_records_sql("SELECT $fields FROM $selectsql ".((!empty($sort)) ? "ORDER BY $sort" : "")." $limit"); + */ +} + +/* + * Retrieve course records with the course managers and other related records + * that we need for print_course(). This allows print_courses() to do its job + * in a constant number of DB queries, regardless of the number of courses, + * role assignments, etc. + * + * The returned array is indexed on c.id, and each course will have + * - $course->context - a context obj + * - $course->managers - array containing RA objects that include a $user obj + * with the minimal fields needed for fullname() + * + */ +function get_courses_wmanagers($categoryid=0, $sort="c.sortorder ASC", $fields=array()) { + /* + * The plan is to + * + * - Grab the courses JOINed w/context + * + * - Grab the interesting course-manager RAs + * JOINed with a base user obj and add them to each course + * + * So as to do all the work in 2 DB queries. The RA+user JOIN + * ends up being pretty expensive if it happens over _all_ + * courses on a large site. (Are we surprised!?) + * + * So this should _never_ get called with 'all' on a large site. + * + */ + global $USER, $CFG; + + $allcats = false; // bool flag + if ($categoryid === 'all') { + $categoryclause = ''; + $allcats = true; + } elseif (is_numeric($categoryid)) { + $categoryclause = "c.category = $categoryid"; + } else { + debugging("Could not recognise categoryid = $categoryid"); + $categoryclause = ''; + } + + $basefields = array('id', 'category', 'sortorder', + 'shortname', 'fullname', 'idnumber', + 'teacher', 'teachers', 'student', 'students', + 'guest', 'startdate', 'visible', + 'newsitems', 'cost', 'enrol', + 'groupmode', 'groupmodeforce'); + + if (!is_null($fields) && is_string($fields)) { + if (empty($fields)) { + $fields = $basefields; + } else { + // turn the fields from a string to an array that + // get_user_courses_bycap() will like... + $fields = explode(',',$fields); + $fields = array_map('trim', $fields); + $fields = array_unique(array_merge($basefields, $fields)); + } + } elseif (is_array($fields)) { + $fields = array_merge($basefields,$fields); + } + $coursefields = 'c.' .join(',c.', $fields); + + if (empty($sort)) { + $sortstatement = ""; + } else { + $sortstatement = "ORDER BY $sort"; + } + + $where = 'WHERE c.id != ' . SITEID; + if ($categoryclause !== ''){ + $where = "$where AND $categoryclause"; + } + + // pull out all courses matching the cat + $sql = "SELECT $coursefields, + ctx.id AS ctxid, ctx.path AS ctxpath, + ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel + FROM {$CFG->prefix}course c + JOIN {$CFG->prefix}context ctx + ON (c.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.") + $where + $sortstatement"; + + $catpaths = array(); + $catpath = NULL; + if ($courses = get_records_sql($sql)) { + // loop on courses materialising + // the context, and prepping data to fetch the + // managers efficiently later... + foreach ($courses as $k => $course) { + $courses[$k] = make_context_subobj($courses[$k]); + $courses[$k]->managers = array(); + if ($allcats === false) { + // single cat, so take just the first one... + if ($catpath === NULL) { + $catpath = preg_replace(':/\d+$:', '',$courses[$k]->context->path); + } + } else { + // chop off the contextid of the course itself + // like dirname() does... + $catpaths[] = preg_replace(':/\d+$:', '',$courses[$k]->context->path); + } + } + } else { + return array(); // no courses! + } + + $CFG->coursemanager = trim($CFG->coursemanager); + if (empty($CFG->coursemanager)) { + return $courses; + } + + $managerroles = split(',', $CFG->coursemanager); + $catctxids = ''; + if (count($managerroles)) { + if ($allcats === true) { + $catpaths = array_unique($catpaths); + $ctxids = array(); + foreach ($catpaths as $cpath) { + $ctxids = array_merge($ctxids, explode('/',substr($cpath,1))); + } + $ctxids = array_unique($ctxids); + $catctxids = implode( ',' , $ctxids); + unset($catpaths); + unset($cpath); + } else { + // take the ctx path from the first course + // as all categories will be the same... + $catpath = substr($catpath,1); + $catpath = preg_replace(':/\d+$:','',$catpath); + $catctxids = str_replace('/',',',$catpath); + } + if ($categoryclause !== '') { + $categoryclause = "AND $categoryclause"; + } + /* + * Note: Here we use a LEFT OUTER JOIN that can + * "optionally" match to avoid passing a ton of context + * ids in an IN() clause. Perhaps a subselect is faster. + * + * In any case, this SQL is not-so-nice over large sets of + * courses with no $categoryclause. + * + */ + $sql = "SELECT ctx.path, ctx.instanceid, ctx.contextlevel, + ra.hidden, + r.id AS roleid, r.name as rolename, + u.id AS userid, u.firstname, u.lastname + FROM {$CFG->prefix}role_assignments ra + JOIN {$CFG->prefix}context ctx + ON ra.contextid = ctx.id + JOIN {$CFG->prefix}user u + ON ra.userid = u.id + JOIN {$CFG->prefix}role r + ON ra.roleid = r.id + LEFT OUTER JOIN {$CFG->prefix}course c + ON (ctx.instanceid=c.id AND ctx.contextlevel=".CONTEXT_COURSE.") + WHERE ( c.id IS NOT NULL"; + // under certain conditions, $catctxids is NULL + if($catctxids == NULL){ + $sql .= ") "; + }else{ + $sql .= " OR ra.contextid IN ($catctxids) )"; + } + + $sql .= "AND ra.roleid IN ({$CFG->coursemanager}) + $categoryclause + ORDER BY r.sortorder ASC, ctx.contextlevel ASC, ra.sortorder ASC"; + $rs = get_recordset_sql($sql); + + // This loop is fairly stupid as it stands - might get better + // results doing an initial pass clustering RAs by path. + while ($ra = rs_fetch_next_record($rs)) { + $user = new StdClass; + $user->id = $ra->userid; unset($ra->userid); + $user->firstname = $ra->firstname; unset($ra->firstname); + $user->lastname = $ra->lastname; unset($ra->lastname); + $ra->user = $user; + if ($ra->contextlevel == CONTEXT_SYSTEM) { + foreach ($courses as $k => $course) { + $courses[$k]->managers[] = $ra; + } + } elseif ($ra->contextlevel == CONTEXT_COURSECAT) { + if ($allcats === false) { + // It always applies + foreach ($courses as $k => $course) { + $courses[$k]->managers[] = $ra; + } + } else { + foreach ($courses as $k => $course) { + // Note that strpos() returns 0 as "matched at pos 0" + if (strpos($course->context->path, $ra->path.'/')===0) { + // Only add it to subpaths + $courses[$k]->managers[] = $ra; + } + } + } + } else { // course-level + if(!array_key_exists($ra->instanceid, $courses)) { + //this course is not in a list, probably a frontpage course + continue; + } + $courses[$ra->instanceid]->managers[] = $ra; + } + } + rs_close($rs); + } + + return $courses; +} + +/** + * Convenience function - lists courses that a user has access to view. + * + * For admins and others with access to "every" course in the system, we should + * try to get courses with explicit RAs. + * + * NOTE: this function is heavily geared towards the perspective of the user + * passed in $userid. So it will hide courses that the user cannot see + * (for any reason) even if called from cron or from another $USER's + * perspective. + * + * If you really want to know what courses are assigned to the user, + * without any hiding or scheming, call the lower-level + * get_user_courses_bycap(). + * + * + * Notes inherited from get_user_courses_bycap(): + * + * - $fields is an array of fieldnames to ADD + * so name the fields you really need, which will + * be added and uniq'd + * + * - the course records have $c->context which is a fully + * valid context object. Saves you a query per course! + * + * @uses $CFG,$USER + * @param int $userid The user of interest + * @param string $sort the sortorder in the course table + * @param array $fields - names of _additional_ fields to return (also accepts a string) + * @param bool $doanything True if using the doanything flag + * @param int $limit Maximum number of records to return, or 0 for unlimited + * @return array {@link $COURSE} of course objects + */ +function get_my_courses($userid, $sort='visible DESC,sortorder ASC', $fields=NULL, $doanything=false,$limit=0) { + + global $CFG,$USER; + + // Guest's do not have any courses + $sitecontext = get_context_instance(CONTEXT_SYSTEM); + if (has_capability('moodle/legacy:guest',$sitecontext,$userid,false)) { + return(array()); + } + + $basefields = array('id', 'category', 'sortorder', + 'shortname', 'fullname', 'idnumber', + 'teacher', 'teachers', 'student', 'students', + 'guest', 'startdate', 'visible', + 'newsitems', 'cost', 'enrol', + 'groupmode', 'groupmodeforce'); + + if (!is_null($fields) && is_string($fields)) { + if (empty($fields)) { + $fields = $basefields; + } else { + // turn the fields from a string to an array that + // get_user_courses_bycap() will like... + $fields = explode(',',$fields); + $fields = array_map('trim', $fields); + $fields = array_unique(array_merge($basefields, $fields)); + } + } elseif (is_array($fields)) { + $fields = array_unique(array_merge($basefields, $fields)); + } else { + $fields = $basefields; + } + + $orderby = ''; + $sort = trim($sort); + if (!empty($sort)) { + $rawsorts = explode(',', $sort); + $sorts = array(); + foreach ($rawsorts as $rawsort) { + $rawsort = trim($rawsort); + if (strpos($rawsort, 'c.') === 0) { + $rawsort = substr($rawsort, 2); + } + $sorts[] = trim($rawsort); + } + $sort = 'c.'.implode(',c.', $sorts); + $orderby = "ORDER BY $sort"; + } + + // + // Logged-in user - Check cached courses + // + // NOTE! it's a _string_ because + // - it's all we'll ever use + // - it serialises much more compact than an array + // this a big concern here - cost of serialise + // and unserialise gets huge as the session grows + // + // If the courses are too many - it won't be set + // for large numbers of courses, caching in the session + // has marginal benefits (costs too much, not + // worthwhile...) and we may hit SQL parser limits + // because we use IN() + // + if ($userid === $USER->id) { + if (isset($USER->loginascontext) + && $USER->loginascontext->contextlevel == CONTEXT_COURSE) { + // list _only_ this course + // anything else is asking for trouble... + $courseids = $USER->loginascontext->instanceid; + } elseif (isset($USER->mycourses) + && is_string($USER->mycourses)) { + if ($USER->mycourses === '') { + // empty str means: user has no courses + // ... so do the easy thing... + return array(); + } else { + $courseids = $USER->mycourses; + } + } + if (isset($courseids)) { + // The data massaging here MUST be kept in sync with + // get_user_courses_bycap() so we return + // the same... + // (but here we don't need to check has_cap) + $coursefields = 'c.' .join(',c.', $fields); + $sql = "SELECT $coursefields, + ctx.id AS ctxid, ctx.path AS ctxpath, + ctx.depth as ctxdepth, ctx.contextlevel AS ctxlevel, + cc.path AS categorypath + FROM {$CFG->prefix}course c + JOIN {$CFG->prefix}course_categories cc + ON c.category=cc.id + JOIN {$CFG->prefix}context ctx + ON (c.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.") + WHERE c.id IN ($courseids) + $orderby"; + $rs = get_recordset_sql($sql); + $courses = array(); + $cc = 0; // keep count + while ($c = rs_fetch_next_record($rs)) { + // build the context obj + $c = make_context_subobj($c); + + $courses[$c->id] = $c; + if ($limit > 0 && $cc++ > $limit) { + break; + } + } + rs_close($rs); + return $courses; + } + } + + // Non-cached - get accessinfo + if ($userid === $USER->id && isset($USER->access)) { + $accessinfo = $USER->access; + } else { + $accessinfo = get_user_access_sitewide($userid); + } + + + $courses = get_user_courses_bycap($userid, 'moodle/course:view', $accessinfo, + $doanything, $sort, $fields, + $limit); + + $cats = NULL; + // If we have to walk category visibility + // to eval course visibility, get the categories + if (empty($CFG->allowvisiblecoursesinhiddencategories)) { + $sql = "SELECT cc.id, cc.path, cc.visible, + ctx.id AS ctxid, ctx.path AS ctxpath, + ctx.depth as ctxdepth, ctx.contextlevel AS ctxlevel + FROM {$CFG->prefix}course_categories cc + JOIN {$CFG->prefix}context ctx ON (cc.id = ctx.instanceid) + WHERE ctx.contextlevel = ".CONTEXT_COURSECAT." + ORDER BY cc.id"; + $rs = get_recordset_sql($sql); + + // Using a temporary array instead of $cats here, to avoid a "true" result when isnull($cats) further down + $categories = array(); + while ($course_cat = rs_fetch_next_record($rs)) { + // build the context obj + $course_cat = make_context_subobj($course_cat); + $categories[$course_cat->id] = $course_cat; + } + rs_close($rs); + + if (!empty($categories)) { + $cats = $categories; + } + + unset($course_cat); + } + // + // Strangely, get_my_courses() is expected to return the + // array keyed on id, which messes up the sorting + // So do that, and also cache the ids in the session if appropriate + // + $kcourses = array(); + $courses_count = count($courses); + $cacheids = NULL; + $vcatpaths = array(); + if ($userid === $USER->id && $courses_count < 500) { + $cacheids = array(); + } + for ($n=0; $n<$courses_count; $n++) { + + // + // Check whether $USER (not $userid) can _actually_ see them + // Easy if $CFG->allowvisiblecoursesinhiddencategories + // is set, and we don't have to care about categories. + // Lots of work otherwise... (all in mem though!) + // + $cansee = false; + if (is_null($cats)) { // easy rules! + if ($courses[$n]->visible == true) { + $cansee = true; + } elseif (has_capability('moodle/course:viewhiddencourses', + $courses[$n]->context, $USER->id)) { + $cansee = true; + } + } else { + // + // Is the cat visible? + // we have to assume it _is_ visible + // so we can shortcut when we find a hidden one + // + $viscat = true; + $cpath = $courses[$n]->categorypath; + if (isset($vcatpaths[$cpath])) { + $viscat = $vcatpaths[$cpath]; + } else { + $cpath = substr($cpath,1); // kill leading slash + $cpath = explode('/',$cpath); + $ccct = count($cpath); + for ($m=0;$m<$ccct;$m++) { + $ccid = $cpath[$m]; + if ($cats[$ccid]->visible==false) { + $viscat = false; + break; + } + } + $vcatpaths[$courses[$n]->categorypath] = $viscat; + } + + // + // Perhaps it's actually visible to $USER + // check moodle/category:visibility + // + // The name isn't obvious, but the description says + // "See hidden categories" so the user shall see... + // But also check if the allowvisiblecoursesinhiddencategories setting is true, and check for course visibility + if ($viscat === false) { + $catctx = $cats[$courses[$n]->category]->context; + if (has_capability('moodle/category:visibility', $catctx, $USER->id)) { + $vcatpaths[$courses[$n]->categorypath] = true; + $viscat = true; + } elseif ($CFG->allowvisiblecoursesinhiddencategories && $courses[$n]->visible == true) { + $viscat = true; + } + } + + // + // Decision matrix + // + if ($viscat === true) { + if ($courses[$n]->visible == true) { + $cansee = true; + } elseif (has_capability('moodle/course:viewhiddencourses', + $courses[$n]->context, $USER->id)) { + $cansee = true; + } + } + } + if ($cansee === true) { + $kcourses[$courses[$n]->id] = $courses[$n]; + if (is_array($cacheids)) { + $cacheids[] = $courses[$n]->id; + } + } + } + if (is_array($cacheids)) { + // Only happens + // - for the logged in user + // - below the threshold (500) + // empty string is _valid_ + $USER->mycourses = join(',',$cacheids); + } elseif ($userid === $USER->id && isset($USER->mycourses)) { + // cheap sanity check + unset($USER->mycourses); + } + + return $kcourses; +} + +/** + * A list of courses that match a search + * + * @uses $CFG + * @param array $searchterms ? + * @param string $sort ? + * @param int $page ? + * @param int $recordsperpage ? + * @param int $totalcount Passed in by reference. ? + * @return object {@link $COURSE} records + */ +function get_courses_search($searchterms, $sort='fullname ASC', $page=0, $recordsperpage=50, &$totalcount) { + + global $CFG; + + //to allow case-insensitive search for postgesql + if ($CFG->dbfamily == 'postgres') { + $LIKE = 'ILIKE'; + $NOTLIKE = 'NOT ILIKE'; // case-insensitive + $REGEXP = '~*'; + $NOTREGEXP = '!~*'; + } else { + $LIKE = 'LIKE'; + $NOTLIKE = 'NOT LIKE'; + $REGEXP = 'REGEXP'; + $NOTREGEXP = 'NOT REGEXP'; + } + + $fullnamesearch = ''; + $summarysearch = ''; + + foreach ($searchterms as $searchterm) { + + $NOT = ''; /// Initially we aren't going to perform NOT LIKE searches, only MSSQL and Oracle + /// will use it to simulate the "-" operator with LIKE clause + + /// Under Oracle and MSSQL, trim the + and - operators and perform + /// simpler LIKE (or NOT LIKE) queries + if ($CFG->dbfamily == 'oracle' || $CFG->dbfamily == 'mssql') { + if (substr($searchterm, 0, 1) == '-') { + $NOT = ' NOT '; + } + $searchterm = trim($searchterm, '+-'); + } + + if ($fullnamesearch) { + $fullnamesearch .= ' AND '; + } + if ($summarysearch) { + $summarysearch .= ' AND '; + } + + if (substr($searchterm,0,1) == '+') { + $searchterm = substr($searchterm,1); + $summarysearch .= " c.summary $REGEXP '(^|[^a-zA-Z0-9])$searchterm([^a-zA-Z0-9]|$)' "; + $fullnamesearch .= " c.fullname $REGEXP '(^|[^a-zA-Z0-9])$searchterm([^a-zA-Z0-9]|$)' "; + } else if (substr($searchterm,0,1) == "-") { + $searchterm = substr($searchterm,1); + $summarysearch .= " c.summary $NOTREGEXP '(^|[^a-zA-Z0-9])$searchterm([^a-zA-Z0-9]|$)' "; + $fullnamesearch .= " c.fullname $NOTREGEXP '(^|[^a-zA-Z0-9])$searchterm([^a-zA-Z0-9]|$)' "; + } else { + $summarysearch .= ' summary '. $NOT . $LIKE .' \'%'. $searchterm .'%\' '; + $fullnamesearch .= ' fullname '. $NOT . $LIKE .' \'%'. $searchterm .'%\' '; + } + + } + + $sql = "SELECT c.*, + ctx.id AS ctxid, ctx.path AS ctxpath, + ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel + FROM {$CFG->prefix}course c + JOIN {$CFG->prefix}context ctx + ON (c.id = ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.") + WHERE (( $fullnamesearch ) OR ( $summarysearch )) + AND category > 0 + ORDER BY " . $sort; + + $courses = array(); + + if ($rs = get_recordset_sql($sql)) { + + + // Tiki pagination + $limitfrom = $page * $recordsperpage; + $limitto = $limitfrom + $recordsperpage; + $c = 0; // counts how many visible courses we've seen + + while ($course = rs_fetch_next_record($rs)) { + $course = make_context_subobj($course); + if ($course->visible || has_capability('moodle/course:viewhiddencourses', $course->context)) { + // Don't exit this loop till the end + // we need to count all the visible courses + // to update $totalcount + if ($c >= $limitfrom && $c < $limitto) { + $courses[] = $course; + } + $c++; + } + } + } + + // our caller expects 2 bits of data - our return + // array, and an updated $totalcount + $totalcount = $c; + return $courses; +} + + +/** + * Returns a sorted list of categories. Each category object has a context + * property that is a context object. + * + * When asking for $parent='none' it will return all the categories, regardless + * of depth. Wheen asking for a specific parent, the default is to return + * a "shallow" resultset. Pass false to $shallow and it will return all + * the child categories as well. + * + * + * @param string $parent The parent category if any + * @param string $sort the sortorder + * @param bool $shallow - set to false to get the children too + * @return array of categories + */ +function get_categories($parent='none', $sort=NULL, $shallow=true) { + global $CFG; + + if ($sort === NULL) { + $sort = 'ORDER BY cc.sortorder ASC'; + } elseif ($sort ==='') { + // leave it as empty + } else { + $sort = "ORDER BY $sort"; + } + + if ($parent === 'none') { + $sql = "SELECT cc.*, + ctx.id AS ctxid, ctx.path AS ctxpath, + ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel + FROM {$CFG->prefix}course_categories cc + JOIN {$CFG->prefix}context ctx + ON cc.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSECAT." + $sort"; + } elseif ($shallow) { + $parent = (int)$parent; + $sql = "SELECT cc.*, + ctx.id AS ctxid, ctx.path AS ctxpath, + ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel + FROM {$CFG->prefix}course_categories cc + JOIN {$CFG->prefix}context ctx + ON cc.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSECAT." + WHERE cc.parent=$parent + $sort"; + } else { + $parent = (int)$parent; + $sql = "SELECT cc.*, + ctx.id AS ctxid, ctx.path AS ctxpath, + ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel + FROM {$CFG->prefix}course_categories cc + JOIN {$CFG->prefix}context ctx + ON cc.id=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSECAT." + JOIN {$CFG->prefix}course_categories ccp + ON (cc.path LIKE ".sql_concat('ccp.path',"'%'").") + WHERE ccp.id=$parent + $sort"; + } + $categories = array(); + + if( $rs = get_recordset_sql($sql) ){ + while ($cat = rs_fetch_next_record($rs)) { + $cat = make_context_subobj($cat); + if ($cat->visible || has_capability('moodle/category:visibility',$cat->context)) { + $categories[$cat->id] = $cat; + } + } + } + return $categories; +} + + +/** + * Returns an array of category ids of all the subcategories for a given + * category. + * @param $catid - The id of the category whose subcategories we want to find. + * @return array of category ids. + */ +function get_all_subcategories($catid) { + + $subcats = array(); + + if ($categories = get_records('course_categories', 'parent', $catid)) { + foreach ($categories as $cat) { + array_push($subcats, $cat->id); + $subcats = array_merge($subcats, get_all_subcategories($cat->id)); + } + } + return $subcats; +} + + +/** +* This recursive function makes sure that the courseorder is consecutive +* +* @param type description +* +* $n is the starting point, offered only for compatilibity -- will be ignored! +* $safe (bool) prevents it from assuming category-sortorder is unique, used to upgrade +* safely from 1.4 to 1.5 +*/ +function fix_course_sortorder($categoryid=0, $n=0, $safe=0, $depth=0, $path='') { + + global $CFG; + + $count = 0; + + $catgap = 1000; // "standard" category gap + $tolerance = 200; // how "close" categories can get + + if ($categoryid > 0){ + // update depth and path + $cat = get_record('course_categories', 'id', $categoryid); + if ($cat->parent == 0) { + $depth = 0; + $path = ''; + } else if ($depth == 0 ) { // doesn't make sense; get from DB + // this is only called if the $depth parameter looks dodgy + $parent = get_record('course_categories', 'id', $cat->parent); + $path = $parent->path; + $depth = $parent->depth; + } + $path = $path . '/' . $categoryid; + $depth = $depth + 1; + + if ($cat->path !== $path) { + set_field('course_categories', 'path', addslashes($path), 'id', $categoryid); + } + if ($cat->depth != $depth) { + set_field('course_categories', 'depth', $depth, 'id', $categoryid); + } + } + + // get some basic info about courses in the category + $info = get_record_sql('SELECT MIN(sortorder) AS min, + MAX(sortorder) AS max, + COUNT(sortorder) AS count + FROM ' . $CFG->prefix . 'course + WHERE category=' . $categoryid); + if (is_object($info)) { // no courses? + $max = $info->max; + $count = $info->count; + $min = $info->min; + unset($info); + } + + if ($categoryid > 0 && $n==0) { // only passed category so don't shift it + $n = $min; + } + + // $hasgap flag indicates whether there's a gap in the sequence + $hasgap = false; + if ($max-$min+1 != $count) { + $hasgap = true; + } + + // $mustshift indicates whether the sequence must be shifted to + // meet its range + $mustshift = false; + if ($min < $n+$tolerance || $min > $n+$tolerance+$catgap ) { + $mustshift = true; + } + + // actually sort only if there are courses, + // and we meet one ofthe triggers: + // - safe flag + // - they are not in a continuos block + // - they are too close to the 'bottom' + if ($count && ( $safe || $hasgap || $mustshift ) ) { + // special, optimized case where all we need is to shift + if ( $mustshift && !$safe && !$hasgap) { + $shift = $n + $catgap - $min; + if ($shift < $count) { + $shift = $count + $catgap; + } + // UPDATE course SET sortorder=sortorder+$shift + execute_sql("UPDATE {$CFG->prefix}course + SET sortorder=sortorder+$shift + WHERE category=$categoryid", 0); + $n = $n + $catgap + $count; + + } else { // do it slowly + $n = $n + $catgap; + // if the new sequence overlaps the current sequence, lack of transactions + // will stop us -- shift things aside for a moment... + if ($safe || ($n >= $min && $n+$count+1 < $min && $CFG->dbfamily==='mysql')) { + $shift = $max + $n + 1000; + execute_sql("UPDATE {$CFG->prefix}course + SET sortorder=sortorder+$shift + WHERE category=$categoryid", 0); + } + + $courses = get_courses($categoryid, 'c.sortorder ASC', 'c.id,c.sortorder'); + begin_sql(); + $tx = true; // transaction sanity + foreach ($courses as $course) { + if ($tx && $course->sortorder != $n ) { // save db traffic + $tx = $tx && set_field('course', 'sortorder', $n, + 'id', $course->id); + } + $n++; + } + if ($tx) { + commit_sql(); + } else { + rollback_sql(); + if (!$safe) { + // if we failed when called with !safe, try + // to recover calling self with safe=true + return fix_course_sortorder($categoryid, $n, true, $depth, $path); + } + } + } + } + set_field('course_categories', 'coursecount', $count, 'id', $categoryid); + + // $n could need updating + $max = get_field_sql("SELECT MAX(sortorder) from {$CFG->prefix}course WHERE category=$categoryid"); + if ($max > $n) { + $n = $max; + } + + if ($categories = get_categories($categoryid)) { + foreach ($categories as $category) { + $n = fix_course_sortorder($category->id, $n, $safe, $depth, $path); + } + } + + return $n+1; +} + +/** + * Ensure all courses have a valid course category + * useful if a category has been removed manually + **/ +function fix_coursecategory_orphans() { + + global $CFG; + + // Note: the handling of sortorder here is arguably + // open to race conditions. Hard to fix here, unlikely + // to hit anyone in production. + + $sql = "SELECT c.id, c.category, c.shortname + FROM {$CFG->prefix}course c + LEFT OUTER JOIN {$CFG->prefix}course_categories cc ON c.category=cc.id + WHERE cc.id IS NULL AND c.id != " . SITEID; + + $rs = get_recordset_sql($sql); + + if (!rs_EOF($rs)) { // we have some orphans + + // the "default" category is the lowest numbered... + $default = get_field_sql("SELECT MIN(id) + FROM {$CFG->prefix}course_categories"); + $sortorder = get_field_sql("SELECT MAX(sortorder) + FROM {$CFG->prefix}course + WHERE category=$default"); + + + begin_sql(); + $tx = true; + while ($tx && $course = rs_fetch_next_record($rs)) { + $tx = $tx && set_field('course', 'category', $default, 'id', $course->id); + $tx = $tx && set_field('course', 'sortorder', ++$sortorder, 'id', $course->id); + } + if ($tx) { + commit_sql(); + } else { + rollback_sql(); + } + } + rs_close($rs); +} + +/** + * List of remote courses that a user has access to via MNET. + * Works only on the IDP + * + * @uses $CFG, $USER + * @return array {@link $COURSE} of course objects + */ +function get_my_remotecourses($userid=0) { + global $CFG, $USER; + + if (empty($userid)) { + $userid = $USER->id; + } + + $sql = "SELECT c.remoteid, c.shortname, c.fullname, + c.hostid, c.summary, c.cat_name, + h.name AS hostname + FROM {$CFG->prefix}mnet_enrol_course c + JOIN {$CFG->prefix}mnet_enrol_assignments a ON c.id=a.courseid + JOIN {$CFG->prefix}mnet_host h ON c.hostid=h.id + WHERE a.userid={$userid}"; + + return get_records_sql($sql); +} + +/** + * List of remote hosts that a user has access to via MNET. + * Works on the SP + * + * @uses $CFG, $USER + * @return array of host objects + */ +function get_my_remotehosts() { + global $CFG, $USER; + + if ($USER->mnethostid == $CFG->mnet_localhost_id) { + return false; // Return nothing on the IDP + } + if (!empty($USER->mnet_foreign_host_array) && is_array($USER->mnet_foreign_host_array)) { + return $USER->mnet_foreign_host_array; + } + return false; +} + +/** + * This function creates a default separated/connected scale + * + * This function creates a default separated/connected scale + * so there's something in the database. The locations of + * strings and files is a bit odd, but this is because we + * need to maintain backward compatibility with many different + * existing language translations and older sites. + * + * @uses $CFG + */ +function make_default_scale() { + + global $CFG; + + $defaultscale = NULL; + $defaultscale->courseid = 0; + $defaultscale->userid = 0; + $defaultscale->name = get_string('separateandconnected'); + $defaultscale->scale = get_string('postrating1', 'forum').','. + get_string('postrating2', 'forum').','. + get_string('postrating3', 'forum'); + $defaultscale->timemodified = time(); + + /// Read in the big description from the file. Note this is not + /// HTML (despite the file extension) but Moodle format text. + $parentlang = get_string('parentlanguage'); + if ($parentlang[0] == '[') { + $parentlang = ''; + } + if (is_readable($CFG->dataroot .'/lang/'. $CFG->lang .'/help/forum/ratings.html')) { + $file = file($CFG->dataroot .'/lang/'. $CFG->lang .'/help/forum/ratings.html'); + } else if (is_readable($CFG->dirroot .'/lang/'. $CFG->lang .'/help/forum/ratings.html')) { + $file = file($CFG->dirroot .'/lang/'. $CFG->lang .'/help/forum/ratings.html'); + } else if ($parentlang and is_readable($CFG->dataroot .'/lang/'. $parentlang .'/help/forum/ratings.html')) { + $file = file($CFG->dataroot .'/lang/'. $parentlang .'/help/forum/ratings.html'); + } else if ($parentlang and is_readable($CFG->dirroot .'/lang/'. $parentlang .'/help/forum/ratings.html')) { + $file = file($CFG->dirroot .'/lang/'. $parentlang .'/help/forum/ratings.html'); + } else if (is_readable($CFG->dirroot .'/lang/en_utf8/help/forum/ratings.html')) { + $file = file($CFG->dirroot .'/lang/en_utf8/help/forum/ratings.html'); + } else { + $file = ''; + } + + $defaultscale->description = addslashes(implode('', $file)); + + if ($defaultscale->id = insert_record('scale', $defaultscale)) { + execute_sql('UPDATE '. $CFG->prefix .'forum SET scale = \''. $defaultscale->id .'\'', false); + } +} + + +/** + * Returns a menu of all available scales from the site as well as the given course + * + * @uses $CFG + * @param int $courseid The id of the course as found in the 'course' table. + * @return object + */ +function get_scales_menu($courseid=0) { + + global $CFG; + + $sql = "SELECT id, name FROM {$CFG->prefix}scale + WHERE courseid = '0' or courseid = '$courseid' + ORDER BY courseid ASC, name ASC"; + + if ($scales = get_records_sql_menu($sql)) { + return $scales; + } + + make_default_scale(); + + return get_records_sql_menu($sql); +} + + + +/** + * Given a set of timezone records, put them in the database, replacing what is there + * + * @uses $CFG + * @param array $timezones An array of timezone records + */ +function update_timezone_records($timezones) { +/// Given a set of timezone records, put them in the database + + global $CFG; + +/// Clear out all the old stuff + execute_sql('TRUNCATE TABLE '.$CFG->prefix.'timezone', false); + +/// Insert all the new stuff + foreach ($timezones as $timezone) { + if (is_array($timezone)) { + $timezone = (object)$timezone; + } + insert_record('timezone', $timezone); + } +} + + +/// MODULE FUNCTIONS ///////////////////////////////////////////////// + +/** + * Just gets a raw list of all modules in a course + * + * @uses $CFG + * @param int $courseid The id of the course as found in the 'course' table. + * @return object + */ +function get_course_mods($courseid) { + global $CFG; + + if (empty($courseid)) { + return false; // avoid warnings + } + + return get_records_sql("SELECT cm.*, m.name as modname + FROM {$CFG->prefix}modules m, + {$CFG->prefix}course_modules cm + WHERE cm.course = ".intval($courseid)." + AND cm.module = m.id AND m.visible = 1"); // no disabled mods +} + + +/** + * Given an id of a course module, finds the coursemodule description + * + * @param string $modulename name of module type, eg. resource, assignment,... + * @param int $cmid course module id (id in course_modules table) + * @param int $courseid optional course id for extra validation + * @return object course module instance with instance and module name + */ +function get_coursemodule_from_id($modulename, $cmid, $courseid=0) { + + global $CFG; + + $courseselect = ($courseid) ? 'cm.course = '.intval($courseid).' AND ' : ''; + + return get_record_sql("SELECT cm.*, m.name, md.name as modname + FROM {$CFG->prefix}course_modules cm, + {$CFG->prefix}modules md, + {$CFG->prefix}$modulename m + WHERE $courseselect + cm.id = ".intval($cmid)." AND + cm.instance = m.id AND + md.name = '$modulename' AND + md.id = cm.module"); +} + +/** + * Given an instance number of a module, finds the coursemodule description + * + * @param string $modulename name of module type, eg. resource, assignment,... + * @param int $instance module instance number (id in resource, assignment etc. table) + * @param int $courseid optional course id for extra validation + * @return object course module instance with instance and module name + */ +function get_coursemodule_from_instance($modulename, $instance, $courseid=0) { + + global $CFG; + + $courseselect = ($courseid) ? 'cm.course = '.intval($courseid).' AND ' : ''; + + return get_record_sql("SELECT cm.*, m.name, md.name as modname + FROM {$CFG->prefix}course_modules cm, + {$CFG->prefix}modules md, + {$CFG->prefix}$modulename m + WHERE $courseselect + cm.instance = m.id AND + md.name = '$modulename' AND + md.id = cm.module AND + m.id = ".intval($instance)); + +} + +/** + * Returns all course modules of given activity in course + * @param string $modulename (forum, quiz, etc.) + * @param int $courseid + * @param string $extrafields extra fields starting with m. + * @return array of cm objects, false if not found or error + */ +function get_coursemodules_in_course($modulename, $courseid, $extrafields='') { + global $CFG; + + if (!empty($extrafields)) { + $extrafields = ", $extrafields"; + } + return get_records_sql("SELECT cm.*, m.name, md.name as modname $extrafields + FROM {$CFG->prefix}course_modules cm, + {$CFG->prefix}modules md, + {$CFG->prefix}$modulename m + WHERE cm.course = $courseid AND + cm.instance = m.id AND + md.name = '$modulename' AND + md.id = cm.module"); +} + +/** + * Returns an array of all the active instances of a particular module in given courses, sorted in the order they are defined + * + * Returns an array of all the active instances of a particular + * module in given courses, sorted in the order they are defined + * in the course. Returns an empty array on any errors. + * + * The returned objects includle the columns cw.section, cm.visible, + * cm.groupmode and cm.groupingid, cm.groupmembersonly, and are indexed by cm.id. + * + * @param string $modulename The name of the module to get instances for + * @param array $courses an array of course objects. + * @return array of module instance objects, including some extra fields from the course_modules + * and course_sections tables, or an empty array if an error occurred. + */ +function get_all_instances_in_courses($modulename, $courses, $userid=NULL, $includeinvisible=false) { + global $CFG; + + $outputarray = array(); + + if (empty($courses) || !is_array($courses) || count($courses) == 0) { + return $outputarray; + } + + if (!$rawmods = get_records_sql("SELECT cm.id AS coursemodule, m.*, cw.section, cm.visible AS visible, + cm.groupmode, cm.groupingid, cm.groupmembersonly + FROM {$CFG->prefix}course_modules cm, + {$CFG->prefix}course_sections cw, + {$CFG->prefix}modules md, + {$CFG->prefix}$modulename m + WHERE cm.course IN (".implode(',',array_keys($courses)).") AND + cm.instance = m.id AND + cm.section = cw.id AND + md.name = '$modulename' AND + md.id = cm.module")) { + return $outputarray; + } + + require_once($CFG->dirroot.'/course/lib.php'); + + foreach ($courses as $course) { + $modinfo = get_fast_modinfo($course, $userid); + + if (empty($modinfo->instances[$modulename])) { + continue; + } + + foreach ($modinfo->instances[$modulename] as $cm) { + if (!$includeinvisible and !$cm->uservisible) { + continue; + } + if (!isset($rawmods[$cm->id])) { + continue; + } + $instance = $rawmods[$cm->id]; + if (!empty($cm->extra)) { + $instance->extra = urlencode($cm->extra); // bc compatibility + } + $outputarray[] = $instance; + } + } + + return $outputarray; +} + +/** + * Returns an array of all the active instances of a particular module in a given course, + * sorted in the order they are defined. + * + * Returns an array of all the active instances of a particular + * module in a given course, sorted in the order they are defined + * in the course. Returns an empty array on any errors. + * + * The returned objects includle the columns cw.section, cm.visible, + * cm.groupmode and cm.groupingid, cm.groupmembersonly, and are indexed by cm.id. + * + * @param string $modulename The name of the module to get instances for + * @param object $course The course obect. + * @return array of module instance objects, including some extra fields from the course_modules + * and course_sections tables, or an empty array if an error occurred. + */ +function get_all_instances_in_course($modulename, $course, $userid=NULL, $includeinvisible=false) { + return get_all_instances_in_courses($modulename, array($course->id => $course), $userid, $includeinvisible); +} + + +/** + * Determine whether a module instance is visible within a course + * + * Given a valid module object with info about the id and course, + * and the module's type (eg "forum") returns whether the object + * is visible or not, groupmembersonly visibility not tested + * + * @uses $CFG + * @param $moduletype Name of the module eg 'forum' + * @param $module Object which is the instance of the module + * @return bool + */ +function instance_is_visible($moduletype, $module) { + + global $CFG; + + if (!empty($module->id)) { + if ($records = get_records_sql("SELECT cm.instance, cm.visible, cm.groupingid, cm.id, cm.groupmembersonly, cm.course + FROM {$CFG->prefix}course_modules cm, + {$CFG->prefix}modules m + WHERE cm.course = '$module->course' AND + cm.module = m.id AND + m.name = '$moduletype' AND + cm.instance = '$module->id'")) { + + foreach ($records as $record) { // there should only be one - use the first one + return $record->visible; + } + } + } + return true; // visible by default! +} + +/** + * Determine whether a course module is visible within a course, + * this is different from instance_is_visible() - faster and visibility for user + * + * @param object $cm object + * @param int $userid empty means current user + * @return bool + */ +function coursemodule_visible_for_user($cm, $userid=0) { + global $USER; + + if (empty($cm->id)) { + debugging("Incorrect course module parameter!", DEBUG_DEVELOPER); + return false; + } + if (empty($userid)) { + $userid = $USER->id; + } + if (!$cm->visible and !has_capability('moodle/course:viewhiddenactivities', get_context_instance(CONTEXT_MODULE, $cm->id), $userid)) { + return false; + } + return groups_course_module_visible($cm, $userid); +} + + + + +/// LOG FUNCTIONS ///////////////////////////////////////////////////// + + +/** + * Add an entry to the log table. + * + * Add an entry to the log table. These are "action" focussed rather + * than web server hits, and provide a way to easily reconstruct what + * any particular student has been doing. + * + * @uses $CFG + * @uses $USER + * @uses $db + * @uses $REMOTE_ADDR + * @uses SITEID + * @param int $courseid The course id + * @param string $module The module name - e.g. forum, journal, resource, course, user etc + * @param string $action 'view', 'update', 'add' or 'delete', possibly followed by another word to clarify. + * @param string $url The file and parameters used to see the results of the action + * @param string $info Additional description information + * @param string $cm The course_module->id if there is one + * @param string $user If log regards $user other than $USER + */ +function add_to_log($courseid, $module, $action, $url='', $info='', $cm=0, $user=0) { + // Note that this function intentionally does not follow the normal Moodle DB access idioms. + // This is for a good reason: it is the most frequently used DB update function, + // so it has been optimised for speed. + global $db, $CFG, $USER; + + if ($cm === '' || is_null($cm)) { // postgres won't translate empty string to its default + $cm = 0; + } + + if ($user) { + $userid = $user; + } else { + if (!empty($USER->realuser)) { // Don't log + return; + } + $userid = empty($USER->id) ? '0' : $USER->id; + } + + $REMOTE_ADDR = getremoteaddr(); + + $timenow = time(); + $info = addslashes($info); + if (!empty($url)) { // could break doing html_entity_decode on an empty var. + $url = html_entity_decode($url); // for php < 4.3.0 this is defined in moodlelib.php + } + + // Restrict length of log lines to the space actually available in the + // database so that it doesn't cause a DB error. Log a warning so that + // developers can avoid doing things which are likely to cause this on a + // routine basis. + $tl=textlib_get_instance(); + if(!empty($info) && $tl->strlen($info)>255) { + $info=$tl->substr($info,0,252).'...'; + debugging('Warning: logged very long info',DEBUG_DEVELOPER); + } + // Note: Unlike $info, URL appears to be already slashed before this function + // is called. Since database limits are for the data before slashes, we need + // to remove them... + $url=stripslashes($url); + // If the 100 field size is changed, also need to alter print_log in course/lib.php + if(!empty($url) && $tl->strlen($url)>100) { + $url=$tl->substr($url,0,97).'...'; + debugging('Warning: logged very long URL',DEBUG_DEVELOPER); + } + $url=addslashes($url); + + if (defined('MDL_PERFDB')) { global $PERF ; $PERF->dbqueries++; $PERF->logwrites++;}; + + if ($CFG->type = 'oci8po') { + if (empty($info)) { + $info = ' '; + } + } + $sql ='INSERT INTO '. $CFG->prefix .'log (time, userid, course, ip, module, cmid, action, url, info) + VALUES (' . "'$timenow', '$userid', '$courseid', '$REMOTE_ADDR', '$module', '$cm', '$action', '$url', '$info')"; + + $result = $db->Execute($sql); + + // MDL-11893, alert $CFG->supportemail if insert into log failed + if (!$result && $CFG->supportemail) { + $site = get_site(); + $subject = 'Insert into log failed at your moodle site '.$site->fullname; + $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"; + $message .= "The failed SQL is:\n\n" . $sql; + + // email_to_user is not usable because email_to_user tries to write to the logs table, + // and this will get caught in an infinite loop, if disk is full + if (empty($CFG->noemailever)) { + $lasttime = get_config('admin', 'lastloginserterrormail'); + if(empty($lasttime) || time() - $lasttime > 60*60*24) { // limit to 1 email per day + mail($CFG->supportemail, $subject, $message); + set_config('lastloginserterrormail', time(), 'admin'); + } + } + } + + if (!$result) { + debugging('Error: Could not insert a new entry to the Moodle log', DEBUG_ALL); + } + +} + +/** + * Store user last access times - called when use enters a course or site + * + * Note: we use ADOdb code directly in this function to save some CPU + * cycles here and there. They are simple operations not needing any + * of the postprocessing performed by dmllib.php + * + * @param int $courseid, empty means site + * @return void + */ +function user_accesstime_log($courseid=0) { + + global $USER, $CFG, $PERF, $db; + + if (!isloggedin() or !empty($USER->realuser)) { + // no access tracking + return; + } + + if (empty($courseid)) { + $courseid = SITEID; + } + + $timenow = time(); + +/// Store site lastaccess time for the current user + if ($timenow - $USER->lastaccess > LASTACCESS_UPDATE_SECS) { + /// Update $USER->lastaccess for next checks + $USER->lastaccess = $timenow; + if (defined('MDL_PERFDB')) { global $PERF ; $PERF->dbqueries++;}; + + $remoteaddr = getremoteaddr(); + if ($db->Execute("UPDATE {$CFG->prefix}user + SET lastip = '$remoteaddr', lastaccess = $timenow + WHERE id = $USER->id")) { + } else { + debugging('Error: Could not update global user lastaccess information'); // Don't throw an error + } + /// Remove this record from record cache since it will change + if (!empty($CFG->rcache)) { + rcache_unset('user', $USER->id); + } + } + + if ($courseid == SITEID) { + /// no user_lastaccess for frontpage + return; + } + +/// Store course lastaccess times for the current user + if (empty($USER->currentcourseaccess[$courseid]) or ($timenow - $USER->currentcourseaccess[$courseid] > LASTACCESS_UPDATE_SECS)) { + if (defined('MDL_PERFDB')) { global $PERF ; $PERF->dbqueries++; }; + + $exists = false; // To detect if the user_lastaccess record exists or no + if ($rs = $db->Execute("SELECT timeaccess + FROM {$CFG->prefix}user_lastaccess + WHERE userid = $USER->id AND courseid = $courseid")) { + if (!$rs->EOF) { + $exists = true; + $lastaccess = reset($rs->fields); + if ($timenow - $lastaccess < LASTACCESS_UPDATE_SECS) { + /// no need to update now, it was updated recently in concurrent login ;-) + $rs->Close(); + return; + } + } + $rs->Close(); + } + + /// Update course lastaccess for next checks + $USER->currentcourseaccess[$courseid] = $timenow; + if (defined('MDL_PERFDB')) { global $PERF ; $PERF->dbqueries++; }; + + if ($exists) { // user_lastaccess record exists, update it + if ($db->Execute("UPDATE {$CFG->prefix}user_lastaccess + SET timeaccess = $timenow + WHERE userid = $USER->id AND courseid = $courseid")) { + } else { + debugging('Error: Could not update course user lastacess information'); // Don't throw an error + } + + } else { // user lastaccess record doesn't exist, insert it + if ($db->Execute("INSERT INTO {$CFG->prefix}user_lastaccess + (userid, courseid, timeaccess) + VALUES ($USER->id, $courseid, $timenow)")) { + } else { + debugging('Error: Could not insert course user lastaccess information'); // Don't throw an error + } + } + } +} + +/** + * Select all log records based on SQL criteria + * + * @uses $CFG + * @param string $select SQL select criteria + * @param string $order SQL order by clause to sort the records returned + * @param string $limitfrom ? + * @param int $limitnum ? + * @param int $totalcount Passed in by reference. + * @return object + * @todo Finish documenting this function + */ +function get_logs($select, $order='l.time DESC', $limitfrom='', $limitnum='', &$totalcount) { + global $CFG; + + if ($order) { + $order = 'ORDER BY '. $order; + } + + $selectsql = $CFG->prefix .'log l LEFT JOIN '. $CFG->prefix .'user u ON l.userid = u.id '. ((strlen($select) > 0) ? 'WHERE '. $select : ''); + $countsql = $CFG->prefix.'log l '.((strlen($select) > 0) ? ' WHERE '. $select : ''); + + $totalcount = count_records_sql("SELECT COUNT(*) FROM $countsql"); + + return get_records_sql('SELECT l.*, u.firstname, u.lastname, u.picture + FROM '. $selectsql .' '. $order, $limitfrom, $limitnum) ; +} + + +/** + * Select all log records for a given course and user + * + * @uses $CFG + * @uses DAYSECS + * @param int $userid The id of the user as found in the 'user' table. + * @param int $courseid The id of the course as found in the 'course' table. + * @param string $coursestart ? + * @todo Finish documenting this function + */ +function get_logs_usercourse($userid, $courseid, $coursestart) { + global $CFG; + + if ($courseid) { + $courseselect = ' AND course = \''. $courseid .'\' '; + } else { + $courseselect = ''; + } + + return get_records_sql("SELECT floor((time - $coursestart)/". DAYSECS .") as day, count(*) as num + FROM {$CFG->prefix}log + WHERE userid = '$userid' + AND time > '$coursestart' $courseselect + GROUP BY floor((time - $coursestart)/". DAYSECS .") "); +} + +/** + * Select all log records for a given course, user, and day + * + * @uses $CFG + * @uses HOURSECS + * @param int $userid The id of the user as found in the 'user' table. + * @param int $courseid The id of the course as found in the 'course' table. + * @param string $daystart ? + * @return object + * @todo Finish documenting this function + */ +function get_logs_userday($userid, $courseid, $daystart) { + global $CFG; + + if ($courseid) { + $courseselect = ' AND course = \''. $courseid .'\' '; + } else { + $courseselect = ''; + } + + return get_records_sql("SELECT floor((time - $daystart)/". HOURSECS .") as hour, count(*) as num + FROM {$CFG->prefix}log + WHERE userid = '$userid' + AND time > '$daystart' $courseselect + GROUP BY floor((time - $daystart)/". HOURSECS .") "); +} + +/** + * Returns an object with counts of failed login attempts + * + * Returns information about failed login attempts. If the current user is + * an admin, then two numbers are returned: the number of attempts and the + * number of accounts. For non-admins, only the attempts on the given user + * are shown. + * + * @param string $mode Either 'admin', 'teacher' or 'everybody' + * @param string $username The username we are searching for + * @param string $lastlogin The date from which we are searching + * @return int + */ +function count_login_failures($mode, $username, $lastlogin) { + + $select = 'module=\'login\' AND action=\'error\' AND time > '. $lastlogin; + + if (has_capability('moodle/site:config', get_context_instance(CONTEXT_SYSTEM))) { // Return information about all accounts + if ($count->attempts = count_records_select('log', $select)) { + $count->accounts = count_records_select('log', $select, 'COUNT(DISTINCT info)'); + return $count; + } + } else if ($mode == 'everybody' or ($mode == 'teacher' and isteacherinanycourse())) { + if ($count->attempts = count_records_select('log', $select .' AND info = \''. $username .'\'')) { + return $count; + } + } + return NULL; +} + + +/// GENERAL HELPFUL THINGS /////////////////////////////////// + +/** + * Dump a given object's information in a PRE block. + * + * Mostly just used for debugging. + * + * @param mixed $object The data to be printed + */ +function print_object($object) { + echo '
' . htmlspecialchars(print_r($object,true)) . '
'; +} + +/* + * Check whether a course is visible through its parents + * path. + * + * Notes: + * + * - All we need from the course is ->category. _However_ + * if the course object has a categorypath property, + * we'll save a dbquery + * + * - If we return false, you'll still need to check if + * the user can has the 'moodle/category:visibility' + * capability... + * + * - Will generate 2 DB calls. + * + * - It does have a small local cache, however... + * + * - Do NOT call this over many courses as it'll generate + * DB traffic. Instead, see what get_my_courses() does. + * + * @param mixed $object A course object + * @return bool + */ +function course_parent_visible($course = null) { + global $CFG; + //return true; + static $mycache; + + if (!is_object($course)) { + return true; + } + if (!empty($CFG->allowvisiblecoursesinhiddencategories)) { + return true; + } + + if (!isset($mycache)) { + $mycache = array(); + } else { + // cast to force assoc array + $k = (string)$course->category; + if (isset($mycache[$k])) { + return $mycache[$k]; + } + } + + if (isset($course->categorypath)) { + $path = $course->categorypath; + } else { + $path = get_field('course_categories', 'path', + 'id', $course->category); + } + $catids = substr($path,1); // strip leading slash + $catids = str_replace('/',',',$catids); + + $sql = "SELECT MIN(visible) + FROM {$CFG->prefix}course_categories + WHERE id IN ($catids)"; + $vis = get_field_sql($sql); + + // cast to force assoc array + $k = (string)$course->category; + $mycache[$k] = $vis; + + return $vis; +} + +/** + * This function is the official hook inside XMLDB stuff to delegate its debug to one + * external function. + * + * Any script can avoid calls to this function by defining XMLDB_SKIP_DEBUG_HOOK before + * using XMLDB classes. Obviously, also, if this function doesn't exist, it isn't invoked ;-) + * + * @param $message string contains the error message + * @param $object object XMLDB object that fired the debug + */ +function xmldb_debug($message, $object) { + + debugging($message, DEBUG_DEVELOPER); +} + +/** + * true or false function to see if user can create any courses at all + * @return bool + */ +function user_can_create_courses() { + global $USER; + // if user has course creation capability at any site or course cat, then return true; + + if (has_capability('moodle/course:create', get_context_instance(CONTEXT_SYSTEM))) { + return true; + } else { + return (bool) count(get_creatable_categories()); + } + +} + +/** + * get the list of categories the current user can create courses in + * @return array + */ +function get_creatable_categories() { + + $creatablecats = array(); + if ($cats = get_records('course_categories')) { + foreach ($cats as $cat) { + if (has_capability('moodle/course:create', get_context_instance(CONTEXT_COURSECAT, $cat->id))) { + $creatablecats[$cat->id] = $cat->name; + } + } + } + return $creatablecats; +} + +// vim:autoindent:expandtab:shiftwidth=4:tabstop=4:tw=140: +?> Index: temp_moodle_dev/moodle/mod/forum/db/install.xml =================================================================== diff -u --- temp_moodle_dev/moodle/mod/forum/db/install.xml (revision 0) +++ temp_moodle_dev/moodle/mod/forum/db/install.xml (revision 3163c863d928a9e392249661266bb38a09936312) @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + +
\ No newline at end of file Index: temp_moodle_dev/moodle/mod/forum/index.php =================================================================== diff -u --- temp_moodle_dev/moodle/mod/forum/index.php (revision 0) +++ temp_moodle_dev/moodle/mod/forum/index.php (revision 3163c863d928a9e392249661266bb38a09936312) @@ -0,0 +1,408 @@ +libdir/rsslib.php"); + + $id = optional_param('id', 0, PARAM_INT); // Course id + $subscribe = optional_param('subscribe', null, PARAM_INT); // Subscribe/Unsubscribe all forums + + if ($id) { + if (! $course = get_record('course', 'id', $id)) { + error("Course ID is incorrect"); + } + } else { + if (! $course = get_site()) { + error("Could not find a top-level course!"); + } + } + + require_course_login($course); + $coursecontext = get_context_instance(CONTEXT_COURSE, $course->id); + + + unset($SESSION->fromdiscussion); + + add_to_log($course->id, 'forum', 'view forums', "index.php?id=$course->id"); + + $strforums = get_string('forums', 'forum'); + $strforum = get_string('forum', 'forum'); + $strdescription = get_string('description'); + $strdiscussions = get_string('discussions', 'forum'); + $strsubscribed = get_string('subscribed', 'forum'); + $strunreadposts = get_string('unreadposts', 'forum'); + $strtracking = get_string('tracking', 'forum'); + $strmarkallread = get_string('markallread', 'forum'); + $strtrackforum = get_string('trackforum', 'forum'); + $strnotrackforum = get_string('notrackforum', 'forum'); + $strsubscribe = get_string('subscribe', 'forum'); + $strunsubscribe = get_string('unsubscribe', 'forum'); + $stryes = get_string('yes'); + $strno = get_string('no'); + $strrss = get_string('rss'); + $strweek = get_string('week'); + $strsection = get_string('section'); + + $searchform = forum_search_form($course); + + + // Start of the table for General Forums + + $generaltable->head = array ($strforum, $strdescription, $strdiscussions); + $generaltable->align = array ('left', 'left', 'center'); + + if ($usetracking = forum_tp_can_track_forums()) { + $untracked = forum_tp_get_untracked_forums($USER->id, $course->id); + + $generaltable->head[] = $strunreadposts; + $generaltable->align[] = 'center'; + + $generaltable->head[] = $strtracking; + $generaltable->align[] = 'center'; + } + + $subscribed_forums = forum_get_subscribed_forums($course); + + if ($can_subscribe = (!isguestuser() && has_capability('moodle/course:view', $coursecontext))) { + $generaltable->head[] = $strsubscribed; + $generaltable->align[] = 'center'; + } + + if ($show_rss = (($can_subscribe || $course->id == SITEID) && + isset($CFG->enablerssfeeds) && isset($CFG->forum_enablerssfeeds) && + $CFG->enablerssfeeds && $CFG->forum_enablerssfeeds)) { + $generaltable->head[] = $strrss; + $generaltable->align[] = 'center'; + } + + + // Parse and organise all the forums. Most forums are course modules but + // some special ones are not. These get placed in the general forums + // category with the forums in section 0. + + $forums = get_records('forum', 'course', $course->id); + + $generalforums = array(); + $learningforums = array(); + $modinfo =& get_fast_modinfo($course); + + if (!isset($modinfo->instances['forum'])) { + $modinfo->instances['forum'] = array(); + } + + foreach ($modinfo->instances['forum'] as $forumid=>$cm) { + if (!$cm->uservisible or !isset($forums[$forumid])) { + continue; + } + + $forum = $forums[$forumid]; + + if (!$context = get_context_instance(CONTEXT_MODULE, $cm->id)) { + continue; // Shouldn't happen + } + + if (!has_capability('mod/forum:viewdiscussion', $context)) { + continue; + } + + // fill two type array - order in modinfo is the same as in course + if ($forum->type == 'news' or $forum->type == 'social') { + $generalforums[$forum->id] = $forum; + + } else if ($course->id == SITEID or empty($cm->sectionnum)) { + $generalforums[$forum->id] = $forum; + + } else { + $learningforums[$forum->id] = $forum; + } + } + + /// Do course wide subscribe/unsubscribe + if (!is_null($subscribe) and !isguestuser() and !isguest()) { + foreach ($modinfo->instances['forum'] as $forumid=>$cm) { + $forum = $forums[$forumid]; + $cansub = false; + if (has_capability('mod/forum:viewdiscussion', $cm)) { + $cansub = true; + } + if ($cansub && $cm->visible == 0 && + !has_capability('mod/forum:managesubscriptions', $cm)) + { + $cansub = false; + } + if (!forum_is_forcesubscribed($forum)) { + $subscribed = forum_is_subscribed($USER->id, $forum); + if ($subscribe && !$subscribed && $cansub) { + forum_subscribe($USER->id, $forumid); + } else if (!$subscribe && $subscribed) { + forum_unsubscribe($USER->id, $forumid); + } + } + } + $returnto = forum_go_back_to("index.php?id=$course->id"); + if ($subscribe) { + add_to_log($course->id, 'forum', 'subscribeall', "index.php?id=$course->id", $course->id); + redirect($returnto, get_string('nowallsubscribed', 'forum', format_string($course->shortname)), 1); + } else { + add_to_log($course->id, 'forum', 'unsubscribeall', "index.php?id=$course->id", $course->id); + redirect($returnto, get_string('nowallunsubscribed', 'forum', format_string($course->shortname)), 1); + } + } + + /// First, let's process the general forums and build up a display + + $introoptions = new object(); + $introoptions->para = false; + + if ($generalforums) { + foreach ($generalforums as $forum) { + $cm = $modinfo->instances['forum'][$forum->id]; + $context = get_context_instance(CONTEXT_MODULE, $cm->id); + + $count = forum_count_discussions($forum, $cm, $course); + + if ($usetracking) { + if ($forum->trackingtype == FORUM_TRACKING_OFF) { + $unreadlink = '-'; + $trackedlink = '-'; + + } else { + if (isset($untracked[$forum->id])) { + $unreadlink = '-'; + } else if ($unread = forum_tp_count_forum_unread_posts($cm, $course)) { + $unreadlink = ''.$unread.''; + $unreadlink .= ''.$strmarkallread.''; + } else { + $unreadlink = '0'; + } + + if ($forum->trackingtype == FORUM_TRACKING_ON) { + $trackedlink = $stryes; + + } else { + $options = array('id'=>$forum->id); + if (!isset($untracked[$forum->id])) { + $trackedlink = print_single_button($CFG->wwwroot.'/mod/forum/settracking.php', $options, $stryes, 'post', '_self', true, $strnotrackforum); + } else { + $trackedlink = print_single_button($CFG->wwwroot.'/mod/forum/settracking.php', $options, $strno, 'post', '_self', true, $strtrackforum); + } + } + } + } + + $forum->intro = shorten_text(trim(format_text($forum->intro, FORMAT_HTML, $introoptions)), $CFG->forum_shortpost); + $forumname = format_string($forum->name, true);; + + if ($cm->visible) { + $style = ''; + } else { + $style = 'class="dimmed"'; + } + $forumlink = "id\" $style>".format_string($forum->name,true).""; + $discussionlink = "id\" $style>".$count.""; + + $row = array ($forumlink, $forum->intro, $discussionlink); + if ($usetracking) { + $row[] = $unreadlink; + $row[] = $trackedlink; // Tracking. + } + + if ($can_subscribe) { + if ($forum->forcesubscribe != FORUM_DISALLOWSUBSCRIBE) { + $row[] = forum_get_subscribe_link($forum, $context, array('subscribed' => $stryes, + 'unsubscribed' => $strno, 'forcesubscribed' => $stryes, + 'cantsubscribe' => '-'), false, false, true, $subscribed_forums); + } else { + $row[] = '-'; + } + } + + //If this forum has RSS activated, calculate it + if ($show_rss) { + if ($forum->rsstype and $forum->rssarticles) { + //Calculate the tolltip text + if ($forum->rsstype == 1) { + $tooltiptext = get_string('rsssubscriberssdiscussions', 'forum', format_string($forum->name)); + } else { + $tooltiptext = get_string('rsssubscriberssposts', 'forum', format_string($forum->name)); + } + //Get html code for RSS link + $row[] = rss_get_link($course->id, $USER->id, 'forum', $forum->id, $tooltiptext); + } else { + $row[] = ' '; + } + } + + $generaltable->data[] = $row; + } + } + + + // Start of the table for Learning Forums + $learningtable->head = array ($strforum, $strdescription, $strdiscussions); + $learningtable->align = array ('left', 'left', 'center'); + + if ($usetracking) { + $learningtable->head[] = $strunreadposts; + $learningtable->align[] = 'center'; + + $learningtable->head[] = $strtracking; + $learningtable->align[] = 'center'; + } + + if ($can_subscribe) { + $learningtable->head[] = $strsubscribed; + $learningtable->align[] = 'center'; + } + + if ($show_rss = (($can_subscribe || $course->id == SITEID) && + isset($CFG->enablerssfeeds) && isset($CFG->forum_enablerssfeeds) && + $CFG->enablerssfeeds && $CFG->forum_enablerssfeeds)) { + $learningtable->head[] = $strrss; + $learningtable->align[] = 'center'; + } + + /// Now let's process the learning forums + + if ($course->id != SITEID) { // Only real courses have learning forums + // Add extra field for section number, at the front + if ($course->format == 'weeks' or $course->format == 'weekscss') { + array_unshift($learningtable->head, $strweek); + } else { + array_unshift($learningtable->head, $strsection); + } + array_unshift($learningtable->align, 'center'); + + + if ($learningforums) { + $currentsection = ''; + foreach ($learningforums as $forum) { + $cm = $modinfo->instances['forum'][$forum->id]; + $context = get_context_instance(CONTEXT_MODULE, $cm->id); + + $count = forum_count_discussions($forum, $cm, $course); + + if ($usetracking) { + if ($forum->trackingtype == FORUM_TRACKING_OFF) { + $unreadlink = '-'; + $trackedlink = '-'; + + } else { + if (isset($untracked[$forum->id])) { + $unreadlink = '-'; + } else if ($unread = forum_tp_count_forum_unread_posts($cm, $course)) { + $unreadlink = ''.$unread.''; + $unreadlink .= ''.$strmarkallread.''; + } else { + $unreadlink = '0'; + } + + if ($forum->trackingtype == FORUM_TRACKING_ON) { + $trackedlink = $stryes; + + } else { + $options = array('id'=>$forum->id); + if (!isset($untracked[$forum->id])) { + $trackedlink = print_single_button($CFG->wwwroot.'/mod/forum/settracking.php', $options, $stryes, 'post', '_self', true, $strnotrackforum); + } else { + $trackedlink = print_single_button($CFG->wwwroot.'/mod/forum/settracking.php', $options, $strno, 'post', '_self', true, $strtrackforum); + } + } + } + } + + $introoptions->para=false; + $forum->intro = shorten_text(trim(format_text($forum->intro, FORMAT_HTML, $introoptions)), $CFG->forum_shortpost); + + if ($cm->sectionnum != $currentsection) { + $printsection = $cm->sectionnum; + if ($currentsection) { + $learningtable->data[] = 'hr'; + } + $currentsection = $cm->sectionnum; + } else { + $printsection = ''; + } + + $forumname = format_string($forum->name,true);; + + if ($cm->visible) { + $style = ''; + } else { + $style = 'class="dimmed"'; + } + $forumlink = "id\" $style>".format_string($forum->name,true).""; + $discussionlink = "id\" $style>".$count.""; + + $row = array ($printsection, $forumlink, $forum->intro, $discussionlink); + if ($usetracking) { + $row[] = $unreadlink; + $row[] = $trackedlink; // Tracking. + } + + if ($can_subscribe) { + if ($forum->forcesubscribe != FORUM_DISALLOWSUBSCRIBE) { + $row[] = forum_get_subscribe_link($forum, $context, array('subscribed' => $stryes, + 'unsubscribed' => $strno, 'forcesubscribed' => $stryes, + 'cantsubscribe' => '-'), false, false, true, $subscribed_forums); + } else { + $row[] = '-'; + } + } + + //If this forum has RSS activated, calculate it + if ($show_rss) { + if ($forum->rsstype and $forum->rssarticles) { + //Calculate the tolltip text + if ($forum->rsstype == 1) { + $tooltiptext = get_string('rsssubscriberssdiscussions', 'forum', format_string($forum->name)); + } else { + $tooltiptext = get_string('rsssubscriberssposts', 'forum', format_string($forum->name)); + } + //Get html code for RSS link + $row[] = rss_get_link($course->id, $USER->id, 'forum', $forum->id, $tooltiptext); + } else { + $row[] = ' '; + } + } + + $learningtable->data[] = $row; + } + } + } + + + /// Output the page + $navlinks = array(); + $navlinks[] = array('name' => $strforums, 'link' => '', 'type' => 'activity'); + + print_header("$course->shortname: $strforums", $course->fullname, + build_navigation($navlinks), + "", "", true, $searchform, navmenu($course)); + + if (!isguest()) { + print_box_start('subscription'); + echo ''; + echo ''.get_string('allsubscribe', 'forum').''; + echo '
'; + echo ''.get_string('allunsubscribe', 'forum').''; + echo ''; + print_box_end(); + print_box(' ', 'clearer'); + } + + if ($generalforums) { + print_heading(get_string('generalforums', 'forum')); + print_table($generaltable); + } + + if ($learningforums) { + print_heading(get_string('learningforums', 'forum')); + print_table($learningtable); + } + + print_footer($course); + +?> Index: temp_moodle_dev/moodle/mod/forum/lib.php =================================================================== diff -u --- temp_moodle_dev/moodle/mod/forum/lib.php (revision 0) +++ temp_moodle_dev/moodle/mod/forum/lib.php (revision 3163c863d928a9e392249661266bb38a09936312) @@ -0,0 +1,6700 @@ +libdir.'/filelib.php'); + +/// CONSTANTS /////////////////////////////////////////////////////////// + +define('FORUM_MODE_FLATOLDEST', 1); +define('FORUM_MODE_FLATNEWEST', -1); +define('FORUM_MODE_THREADED', 2); +define('FORUM_MODE_NESTED', 3); + +define('FORUM_FORCESUBSCRIBE', 1); +define('FORUM_INITIALSUBSCRIBE', 2); +define('FORUM_DISALLOWSUBSCRIBE',3); + +define('FORUM_TRACKING_OFF', 0); +define('FORUM_TRACKING_OPTIONAL', 1); +define('FORUM_TRACKING_ON', 2); + +define('FORUM_UNSET_POST_RATING', -999); + +define ('FORUM_AGGREGATE_NONE', 0); //no ratings +define ('FORUM_AGGREGATE_AVG', 1); +define ('FORUM_AGGREGATE_COUNT', 2); +define ('FORUM_AGGREGATE_MAX', 3); +define ('FORUM_AGGREGATE_MIN', 4); +define ('FORUM_AGGREGATE_SUM', 5); + +/// STANDARD FUNCTIONS /////////////////////////////////////////////////////////// + +/** + * Given an object containing all the necessary data, + * (defined by the form in mod.html) this function + * will create a new instance and return the id number + * of the new instance. + * @param object $forum add forum instance (with magic quotes) + * @return int intance id + */ +function forum_add_instance($forum) { + global $CFG; + + $forum->timemodified = time(); + + if (empty($forum->assessed)) { + $forum->assessed = 0; + } + + if (empty($forum->ratingtime) or empty($forum->assessed)) { + $forum->assesstimestart = 0; + $forum->assesstimefinish = 0; + } + + if (!$forum->id = insert_record('forum', $forum)) { + return false; + } + + if ($forum->type == 'single') { // Create related discussion. + $discussion = new object(); + $discussion->course = $forum->course; + $discussion->forum = $forum->id; + $discussion->name = $forum->name; + $discussion->intro = $forum->intro; + $discussion->assessed = $forum->assessed; + $discussion->format = $forum->type; + $discussion->mailnow = false; + $discussion->groupid = -1; + + if (! forum_add_discussion($discussion, $discussion->intro)) { + error('Could not add the discussion for this forum'); + } + } + + if ($forum->forcesubscribe == FORUM_INITIALSUBSCRIBE) { // all users should be subscribed initially + $users = get_course_users($forum->course); + foreach ($users as $user) { + forum_subscribe($user->id, $forum->id); + } + } + + $forum = stripslashes_recursive($forum); + forum_grade_item_update($forum); + + return $forum->id; +} + + +/** + * Given an object containing all the necessary data, + * (defined by the form in mod.html) this function + * will update an existing instance with new data. + * @param object $forum forum instance (with magic quotes) + * @return bool success + */ +function forum_update_instance($forum) { + $forum->timemodified = time(); + $forum->id = $forum->instance; + + if (empty($forum->assessed)) { + $forum->assessed = 0; + } + + if (empty($forum->ratingtime) or empty($forum->assessed)) { + $forum->assesstimestart = 0; + $forum->assesstimefinish = 0; + } + + $oldforum = get_record('forum', 'id', $forum->id); + + // MDL-3942 - if the aggregation type or scale (i.e. max grade) changes then recalculate the grades for the entire forum + // if scale changes - do we need to recheck the ratings, if ratings higher than scale how do we want to respond? + // for count and sum aggregation types the grade we check to make sure they do not exceed the scale (i.e. max score) when calculating the grade + if (($oldforum->assessed<>$forum->assessed) or ($oldforum->scale<>$forum->scale)) { + forum_update_grades($forum); // recalculate grades for the forum + } + + if ($forum->type == 'single') { // Update related discussion and post. + if (! $discussion = get_record('forum_discussions', 'forum', $forum->id)) { + if ($discussions = get_records('forum_discussions', 'forum', $forum->id, 'timemodified ASC')) { + notify('Warning! There is more than one discussion in this forum - using the most recent'); + $discussion = array_pop($discussions); + } else { + error('Could not find the discussion in this forum'); + } + } + if (! $post = get_record('forum_posts', 'id', $discussion->firstpost)) { + error('Could not find the first post in this forum discussion'); + } + + $post->subject = $forum->name; + $post->message = $forum->intro; + $post->modified = $forum->timemodified; + + if (! update_record('forum_posts', ($post))) { + error('Could not update the first post'); + } + + $discussion->name = $forum->name; + + if (! update_record('forum_discussions', ($discussion))) { + error('Could not update the discussion'); + } + } + + if (!update_record('forum', $forum)) { + error('Can not update forum'); + } + + $forum = stripslashes_recursive($forum); + forum_grade_item_update($forum); + + return true; +} + + +/** + * Given an ID of an instance of this module, + * this function will permanently delete the instance + * and any data that depends on it. + * @param int forum instance id + * @return bool success + */ +function forum_delete_instance($id) { + + if (!$forum = get_record('forum', 'id', $id)) { + return false; + } + + $result = true; + + if ($discussions = get_records('forum_discussions', 'forum', $forum->id)) { + foreach ($discussions as $discussion) { + if (!forum_delete_discussion($discussion, true)) { + $result = false; + } + } + } + + if (!delete_records('forum_subscriptions', 'forum', $forum->id)) { + $result = false; + } + + forum_tp_delete_read_records(-1, -1, -1, $forum->id); + + if (!delete_records('forum', 'id', $forum->id)) { + $result = false; + } + + forum_grade_item_delete($forum); + + return $result; +} + + +/** + * Function to be run periodically according to the moodle cron + * Finds all posts that have yet to be mailed out, and mails them + * out to all subscribers + * @return void + */ +function forum_cron() { + global $CFG, $USER; + + $cronuser = clone($USER); + $site = get_site(); + + // all users that are subscribed to any post that needs sending + $users = array(); + + // status arrays + $mailcount = array(); + $errorcount = array(); + + // caches + $discussions = array(); + $forums = array(); + $courses = array(); + $coursemodules = array(); + $subscribedusers = array(); + + + // Posts older than 2 days will not be mailed. This is to avoid the problem where + // cron has not been running for a long time, and then suddenly people are flooded + // with mail from the past few weeks or months + $timenow = time(); + $endtime = $timenow - $CFG->maxeditingtime; + $starttime = $endtime - 48 * 3600; // Two days earlier + + if ($posts = forum_get_unmailed_posts($starttime, $endtime, $timenow)) { + // Mark them all now as being mailed. It's unlikely but possible there + // might be an error later so that a post is NOT actually mailed out, + // but since mail isn't crucial, we can accept this risk. Doing it now + // prevents the risk of duplicated mails, which is a worse problem. + + if (!forum_mark_old_posts_as_mailed($endtime)) { + mtrace('Errors occurred while trying to mark some posts as being mailed.'); + return false; // Don't continue trying to mail them, in case we are in a cron loop + } + + // checking post validity, and adding users to loop through later + foreach ($posts as $pid => $post) { + + $discussionid = $post->discussion; + if (!isset($discussions[$discussionid])) { + if ($discussion = get_record('forum_discussions', 'id', $post->discussion)) { + $discussions[$discussionid] = $discussion; + } else { + mtrace('Could not find discussion '.$discussionid); + unset($posts[$pid]); + continue; + } + } + $forumid = $discussions[$discussionid]->forum; + if (!isset($forums[$forumid])) { + if ($forum = get_record('forum', 'id', $forumid)) { + $forums[$forumid] = $forum; + } else { + mtrace('Could not find forum '.$forumid); + unset($posts[$pid]); + continue; + } + } + $courseid = $forums[$forumid]->course; + if (!isset($courses[$courseid])) { + if ($course = get_record('course', 'id', $courseid)) { + $courses[$courseid] = $course; + } else { + mtrace('Could not find course '.$courseid); + unset($posts[$pid]); + continue; + } + } + if (!isset($coursemodules[$forumid])) { + if ($cm = get_coursemodule_from_instance('forum', $forumid, $courseid)) { + $coursemodules[$forumid] = $cm; + } else { + mtrace('Could not course module for forum '.$forumid); + unset($posts[$pid]); + continue; + } + } + + + // caching subscribed users of each forum + if (!isset($subscribedusers[$forumid])) { + if ($subusers = forum_subscribed_users($courses[$courseid], $forums[$forumid], 0, false)) { + foreach ($subusers as $postuser) { + // do not try to mail users with stopped email + if ($postuser->emailstop) { + if (!empty($CFG->forum_logblocked)) { + add_to_log(SITEID, 'forum', 'mail blocked', '', '', 0, $postuser->id); + } + continue; + } + // this user is subscribed to this forum + $subscribedusers[$forumid][$postuser->id] = $postuser->id; + // this user is a user we have to process later + $users[$postuser->id] = $postuser; + } + unset($subusers); // release memory + } + } + + $mailcount[$pid] = 0; + $errorcount[$pid] = 0; + } + } + + if ($users && $posts) { + + $urlinfo = parse_url($CFG->wwwroot); + $hostname = $urlinfo['host']; + + foreach ($users as $userto) { + + @set_time_limit(120); // terminate if processing of any account takes longer than 2 minutes + + // set this so that the capabilities are cached, and environment matches receiving user + $USER = $userto; + + mtrace('Processing user '.$userto->id); + + // init caches + $userto->viewfullnames = array(); + $userto->canpost = array(); + $userto->markposts = array(); + $userto->enrolledin = array(); + + // reset the caches + foreach ($coursemodules as $forumid=>$unused) { + $coursemodules[$forumid]->cache = new object(); + $coursemodules[$forumid]->cache->caps = array(); + unset($coursemodules[$forumid]->uservisible); + } + + foreach ($posts as $pid => $post) { + + // Set up the environment for the post, discussion, forum, course + $discussion = $discussions[$post->discussion]; + $forum = $forums[$discussion->forum]; + $course = $courses[$forum->course]; + $cm =& $coursemodules[$forum->id]; + + // Do some checks to see if we can bail out now + if (!isset($subscribedusers[$forum->id][$userto->id])) { + continue; // user does not subscribe to this forum + } + + // Verify user is enrollend in course - if not do not send any email + if (!isset($userto->enrolledin[$course->id])) { + $userto->enrolledin[$course->id] = has_capability('moodle/course:view', get_context_instance(CONTEXT_COURSE, $course->id)); + } + if (!$userto->enrolledin[$course->id]) { + // oops - this user should not receive anything from this course + continue; + } + + // Get info about the sending user + if (array_key_exists($post->userid, $users)) { // we might know him/her already + $userfrom = $users[$post->userid]; + } else if ($userfrom = get_record('user', 'id', $post->userid)) { + $users[$userfrom->id] = $userfrom; // fetch only once, we can add it to user list, it will be skipped anyway + } else { + mtrace('Could not find user '.$post->userid); + continue; + } + + // setup global $COURSE properly - needed for roles and languages + course_setup($course); // More environment + + // Fill caches + if (!isset($userto->viewfullnames[$forum->id])) { + $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id); + $userto->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $modcontext); + } + if (!isset($userto->canpost[$discussion->id])) { + $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id); + $userto->canpost[$discussion->id] = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext); + } + if (!isset($userfrom->groups[$forum->id])) { + if (!isset($userfrom->groups)) { + $userfrom->groups = array(); + $users[$userfrom->id]->groups = array(); + } + $userfrom->groups[$forum->id] = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid); + $users[$userfrom->id]->groups[$forum->id] = $userfrom->groups[$forum->id]; + } + + // Make sure groups allow this user to see this email + if ($discussion->groupid > 0 and $groupmode = groups_get_activity_groupmode($cm, $course)) { // Groups are being used + if (!groups_group_exists($discussion->groupid)) { // Can't find group + continue; // Be safe and don't send it to anyone + } + + if (!groups_is_member($discussion->groupid) and !has_capability('moodle/site:accessallgroups', $modcontext)) { + // do not send posts from other groups when in SEPARATEGROUPS or VISIBLEGROUPS + continue; + } + } + + // Make sure we're allowed to see it... + if (!forum_user_can_see_post($forum, $discussion, $post, NULL, $cm)) { + mtrace('user '.$userto->id. ' can not see '.$post->id); + continue; + } + + // OK so we need to send the email. + + // Does the user want this post in a digest? If so postpone it for now. + if ($userto->maildigest > 0) { + // This user wants the mails to be in digest form + $queue = new object(); + $queue->userid = $userto->id; + $queue->discussionid = $discussion->id; + $queue->postid = $post->id; + $queue->timemodified = $post->created; + if (!insert_record('forum_queue', $queue)) { + mtrace("Error: mod/forum/cron.php: Could not queue for digest mail for id $post->id to user $userto->id ($userto->email) .. not trying again."); + } + continue; + } + + + // Prepare to actually send the post now, and build up the content + + $cleanforumname = str_replace('"', "'", strip_tags(format_string($forum->name))); + + $userfrom->customheaders = array ( // Headers to make emails easier to track + 'Precedence: Bulk', + 'List-Id: "'.$cleanforumname.'" id.'@'.$hostname.'>', + 'List-Help: '.$CFG->wwwroot.'/mod/forum/view.php?f='.$forum->id, + 'Message-ID: id.'@'.$hostname.'>', + 'In-Reply-To: parent.'@'.$hostname.'>', + 'References: parent.'@'.$hostname.'>', + 'X-Course-Id: '.$course->id, + 'X-Course-Name: '.format_string($course->fullname, true) + ); + + + $postsubject = "$course->shortname: ".format_string($post->subject,true); + $posttext = forum_make_mail_text($course, $forum, $discussion, $post, $userfrom, $userto); + $posthtml = forum_make_mail_html($course, $forum, $discussion, $post, $userfrom, $userto); + + // Send the post now! + + mtrace('Sending ', ''); + + if (!$mailresult = email_to_user($userto, $userfrom, $postsubject, $posttext, + $posthtml, '', '', $CFG->forum_replytouser)) { + mtrace("Error: mod/forum/cron.php: Could not send out mail for id $post->id to user $userto->id". + " ($userto->email) .. not trying again."); + add_to_log($course->id, 'forum', 'mail error', "discuss.php?d=$discussion->id#p$post->id", + substr(format_string($post->subject,true),0,30), $cm->id, $userto->id); + $errorcount[$post->id]++; + } else if ($mailresult === 'emailstop') { + // should not be reached anymore - see check above + } else { + $mailcount[$post->id]++; + + // Mark post as read if forum_usermarksread is set off + if (!$CFG->forum_usermarksread) { + $userto->markposts[$post->id] = $post->id; + } + } + + mtrace('post '.$post->id. ': '.$post->subject); + } + + // mark processed posts as read + forum_tp_mark_posts_read($userto, $userto->markposts); + } + } + + if ($posts) { + foreach ($posts as $post) { + mtrace($mailcount[$post->id]." users were sent post $post->id, '$post->subject'"); + if ($errorcount[$post->id]) { + set_field("forum_posts", "mailed", "2", "id", "$post->id"); + } + } + } + + // release some memory + unset($subscribedusers); + unset($mailcount); + unset($errorcount); + + $USER = clone($cronuser); + course_setup(SITEID); + + $sitetimezone = $CFG->timezone; + + // Now see if there are any digest mails waiting to be sent, and if we should send them + + mtrace('Starting digest processing...'); + + @set_time_limit(300); // terminate if not able to fetch all digests in 5 minutes + + if (!isset($CFG->digestmailtimelast)) { // To catch the first time + set_config('digestmailtimelast', 0); + } + + $timenow = time(); + $digesttime = usergetmidnight($timenow, $sitetimezone) + ($CFG->digestmailtime * 3600); + + // Delete any really old ones (normally there shouldn't be any) + $weekago = $timenow - (7 * 24 * 3600); + delete_records_select('forum_queue', "timemodified < $weekago"); + mtrace ('Cleaned old digest records'); + + if ($CFG->digestmailtimelast < $digesttime and $timenow > $digesttime) { + + mtrace('Sending forum digests: '.userdate($timenow, '', $sitetimezone)); + + $digestposts_rs = get_recordset_select('forum_queue', "timemodified < $digesttime"); + + if (!rs_EOF($digestposts_rs)) { + + // We have work to do + $usermailcount = 0; + + //caches - reuse the those filled before too + $discussionposts = array(); + $userdiscussions = array(); + + while ($digestpost = rs_fetch_next_record($digestposts_rs)) { + if (!isset($users[$digestpost->userid])) { + if ($user = get_record('user', 'id', $digestpost->userid)) { + $users[$digestpost->userid] = $user; + } else { + continue; + } + } + $postuser = $users[$digestpost->userid]; + if ($postuser->emailstop) { + if (!empty($CFG->forum_logblocked)) { + add_to_log(SITEID, 'forum', 'mail blocked', '', '', 0, $postuser->id); + } + continue; + } + + if (!isset($posts[$digestpost->postid])) { + if ($post = get_record('forum_posts', 'id', $digestpost->postid)) { + $posts[$digestpost->postid] = $post; + } else { + continue; + } + } + $discussionid = $digestpost->discussionid; + if (!isset($discussions[$discussionid])) { + if ($discussion = get_record('forum_discussions', 'id', $discussionid)) { + $discussions[$discussionid] = $discussion; + } else { + continue; + } + } + $forumid = $discussions[$discussionid]->forum; + if (!isset($forums[$forumid])) { + if ($forum = get_record('forum', 'id', $forumid)) { + $forums[$forumid] = $forum; + } else { + continue; + } + } + + $courseid = $forums[$forumid]->course; + if (!isset($courses[$courseid])) { + if ($course = get_record('course', 'id', $courseid)) { + $courses[$courseid] = $course; + } else { + continue; + } + } + + if (!isset($coursemodules[$forumid])) { + if ($cm = get_coursemodule_from_instance('forum', $forumid, $courseid)) { + $coursemodules[$forumid] = $cm; + } else { + continue; + } + } + $userdiscussions[$digestpost->userid][$digestpost->discussionid] = $digestpost->discussionid; + $discussionposts[$digestpost->discussionid][$digestpost->postid] = $digestpost->postid; + } + rs_close($digestposts_rs); /// Finished iteration, let's close the resultset + + // Data collected, start sending out emails to each user + foreach ($userdiscussions as $userid => $thesediscussions) { + + @set_time_limit(120); // terminate if processing of any account takes longer than 2 minutes + + $USER = $cronuser; + course_setup(SITEID); // reset cron user language, theme and timezone settings + + mtrace(get_string('processingdigest', 'forum', $userid), '... '); + + // First of all delete all the queue entries for this user + delete_records_select('forum_queue', "userid = $userid AND timemodified < $digesttime"); + $userto = $users[$userid]; + + // Override the language and timezone of the "current" user, so that + // mail is customised for the receiver. + $USER = $userto; + course_setup(SITEID); + + // init caches + $userto->viewfullnames = array(); + $userto->canpost = array(); + $userto->markposts = array(); + + $postsubject = get_string('digestmailsubject', 'forum', format_string($site->shortname, true)); + + $headerdata = new object(); + $headerdata->sitename = format_string($site->fullname, true); + $headerdata->userprefs = $CFG->wwwroot.'/user/edit.php?id='.$userid.'&course='.$site->id; + + $posttext = get_string('digestmailheader', 'forum', $headerdata)."\n\n"; + $headerdata->userprefs = ''.get_string('digestmailprefs', 'forum').''; + + $posthtml = ""; + foreach ($CFG->stylesheets as $stylesheet) { + $posthtml .= ''."\n"; + } + $posthtml .= "\n\n"; + $posthtml .= '

'.get_string('digestmailheader', 'forum', $headerdata).'



'; + + foreach ($thesediscussions as $discussionid) { + + @set_time_limit(120); // to be reset for each post + + $discussion = $discussions[$discussionid]; + $forum = $forums[$discussion->forum]; + $course = $courses[$forum->course]; + $cm = $coursemodules[$forum->id]; + + //override language + course_setup($course); + + // Fill caches + if (!isset($userto->viewfullnames[$forum->id])) { + $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id); + $userto->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $modcontext); + } + if (!isset($userto->canpost[$discussion->id])) { + $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id); + $userto->canpost[$discussion->id] = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext); + } + + $strforums = get_string('forums', 'forum'); + $canunsubscribe = ! forum_is_forcesubscribed($forum); + $canreply = $userto->canpost[$discussion->id]; + + $posttext .= "\n \n"; + $posttext .= '====================================================================='; + $posttext .= "\n \n"; + $posttext .= "$course->shortname -> $strforums -> ".format_string($forum->name,true); + if ($discussion->name != $forum->name) { + $posttext .= " -> ".format_string($discussion->name,true); + } + $posttext .= "\n"; + + $posthtml .= "

". + "wwwroot/course/view.php?id=$course->id\">$course->shortname -> ". + "wwwroot/mod/forum/index.php?id=$course->id\">$strforums -> ". + "wwwroot/mod/forum/view.php?f=$forum->id\">".format_string($forum->name,true).""; + if ($discussion->name == $forum->name) { + $posthtml .= "

"; + } else { + $posthtml .= " -> wwwroot/mod/forum/discuss.php?d=$discussion->id\">".format_string($discussion->name,true)."

"; + } + $posthtml .= '

'; + + $postsarray = $discussionposts[$discussionid]; + sort($postsarray); + + foreach ($postsarray as $postid) { + $post = $posts[$postid]; + + if (array_key_exists($post->userid, $users)) { // we might know him/her already + $userfrom = $users[$post->userid]; + } else if ($userfrom = get_record('user', 'id', $post->userid)) { + $users[$userfrom->id] = $userfrom; // fetch only once, we can add it to user list, it will be skipped anyway + } else { + mtrace('Could not find user '.$post->userid); + continue; + } + + if (!isset($userfrom->groups[$forum->id])) { + if (!isset($userfrom->groups)) { + $userfrom->groups = array(); + $users[$userfrom->id]->groups = array(); + } + $userfrom->groups[$forum->id] = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid); + $users[$userfrom->id]->groups[$forum->id] = $userfrom->groups[$forum->id]; + } + + $userfrom->customheaders = array ("Precedence: Bulk"); + + if ($userto->maildigest == 2) { + // Subjects only + $by = new object(); + $by->name = fullname($userfrom); + $by->date = userdate($post->modified); + $posttext .= "\n".format_string($post->subject,true).' '.get_string("bynameondate", "forum", $by); + $posttext .= "\n---------------------------------------------------------------------"; + + $by->name = "wwwroot/user/view.php?id=$userfrom->id&course=$course->id\">$by->name"; + $posthtml .= '

'.format_string($post->subject,true).' '.get_string("bynameondate", "forum", $by).'
'; + + } else { + // The full treatment + $posttext .= forum_make_mail_text($course, $forum, $discussion, $post, $userfrom, $userto, true); + $posthtml .= forum_make_mail_post($course, $forum, $discussion, $post, $userfrom, $userto, false, $canreply, true, false); + + // Create an array of postid's for this user to mark as read. + if (!$CFG->forum_usermarksread) { + $userto->markposts[$post->id] = $post->id; + } + } + } + if ($canunsubscribe) { + $posthtml .= "\n
wwwroot/mod/forum/subscribe.php?id=$forum->id\">".get_string("unsubscribe", "forum")."
"; + } else { + $posthtml .= "\n
".get_string("everyoneissubscribed", "forum")."
"; + } + $posthtml .= '

'; + } + $posthtml .= ''; + + if ($userto->mailformat != 1) { + // This user DOESN'T want to receive HTML + $posthtml = ''; + } + + if (!$mailresult = email_to_user($userto, $site->shortname, $postsubject, $posttext, $posthtml, + '', '', $CFG->forum_replytouser)) { + mtrace("ERROR!"); + echo "Error: mod/forum/cron.php: Could not send out digest mail to user $userto->id ($userto->email)... not trying again.\n"; + add_to_log($course->id, 'forum', 'mail digest error', '', '', $cm->id, $userto->id); + } else if ($mailresult === 'emailstop') { + // should not happen anymore - see check above + } else { + mtrace("success."); + $usermailcount++; + + // Mark post as read if forum_usermarksread is set off + forum_tp_mark_posts_read($userto, $userto->markposts); + } + } + } + /// We have finishied all digest emails, update $CFG->digestmailtimelast + set_config('digestmailtimelast', $timenow); + } + + $USER = $cronuser; + course_setup(SITEID); // reset cron user language, theme and timezone settings + + if (!empty($usermailcount)) { + mtrace(get_string('digestsentusers', 'forum', $usermailcount)); + } + + if (!empty($CFG->forum_lastreadclean)) { + $timenow = time(); + if ($CFG->forum_lastreadclean + (24*3600) < $timenow) { + set_config('forum_lastreadclean', $timenow); + mtrace('Removing old forum read tracking info...'); + forum_tp_clean_read_records(); + } + } else { + set_config('forum_lastreadclean', time()); + } + + + return true; +} + +/** + * Builds and returns the body of the email notification in plain text. + * + * @param object $course + * @param object $forum + * @param object $discussion + * @param object $post + * @param object $userfrom + * @param object $userto + * @param boolean $bare + * @return string The email body in plain text format. + */ +function forum_make_mail_text($course, $forum, $discussion, $post, $userfrom, $userto, $bare = false) { + global $CFG, $USER; + + if (!isset($userto->viewfullnames[$forum->id])) { + if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $course->id)) { + error('Course Module ID was incorrect'); + } + $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id); + $viewfullnames = has_capability('moodle/site:viewfullnames', $modcontext, $userto->id); + } else { + $viewfullnames = $userto->viewfullnames[$forum->id]; + } + + if (!isset($userto->canpost[$discussion->id])) { + $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id); + $canreply = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext); + } else { + $canreply = $userto->canpost[$discussion->id]; + } + + $by = New stdClass; + $by->name = fullname($userfrom, $viewfullnames); + $by->date = userdate($post->modified, "", $userto->timezone); + + $strbynameondate = get_string('bynameondate', 'forum', $by); + + $strforums = get_string('forums', 'forum'); + + $canunsubscribe = ! forum_is_forcesubscribed($forum); + + $posttext = ''; + + if (!$bare) { + $posttext = "$course->shortname -> $strforums -> ".format_string($forum->name,true); + + if ($discussion->name != $forum->name) { + $posttext .= " -> ".format_string($discussion->name,true); + } + } + + $posttext .= "\n---------------------------------------------------------------------\n"; + $posttext .= format_string($post->subject,true); + if ($bare) { + $posttext .= " ($CFG->wwwroot/mod/forum/discuss.php?d=$discussion->id#p$post->id)"; + } + $posttext .= "\n".$strbynameondate."\n"; + $posttext .= "---------------------------------------------------------------------\n"; + $posttext .= format_text_email(trusttext_strip($post->message), $post->format); + $posttext .= "\n\n"; + if ($post->attachment) { + $post->course = $course->id; + $post->forum = $forum->id; + $posttext .= forum_print_attachments($post, "text"); + } + if (!$bare && $canreply) { + $posttext .= "---------------------------------------------------------------------\n"; + $posttext .= get_string("postmailinfo", "forum", $course->shortname)."\n"; + $posttext .= "$CFG->wwwroot/mod/forum/post.php?reply=$post->id\n"; + } + if (!$bare && $canunsubscribe) { + $posttext .= "\n---------------------------------------------------------------------\n"; + $posttext .= get_string("unsubscribe", "forum"); + $posttext .= ": $CFG->wwwroot/mod/forum/subscribe.php?id=$forum->id\n"; + } + + return $posttext; +} + +/** + * Builds and returns the body of the email notification in html format. + * + * @param object $course + * @param object $forum + * @param object $discussion + * @param object $post + * @param object $userfrom + * @param object $userto + * @return string The email text in HTML format + */ +function forum_make_mail_html($course, $forum, $discussion, $post, $userfrom, $userto) { + global $CFG; + + if ($userto->mailformat != 1) { // Needs to be HTML + return ''; + } + + if (!isset($userto->canpost[$discussion->id])) { + $canreply = forum_user_can_post($forum, $discussion, $userto); + } else { + $canreply = $userto->canpost[$discussion->id]; + } + + $strforums = get_string('forums', 'forum'); + $canunsubscribe = ! forum_is_forcesubscribed($forum); + + $posthtml = ''; + foreach ($CFG->stylesheets as $stylesheet) { + $posthtml .= ''."\n"; + } + $posthtml .= ''; + $posthtml .= "\n\n\n"; + + $posthtml .= ''; + } else { + $posthtml .= ' » '. + format_string($discussion->name,true).''; + } + $posthtml .= forum_make_mail_post($course, $forum, $discussion, $post, $userfrom, $userto, false, $canreply, true, false); + + if ($canunsubscribe) { + $posthtml .= '
'; + } + + $posthtml .= ''; + + return $posthtml; +} + + +/** + * + * @param object $course + * @param object $user + * @param object $mod TODO this is not used in this function, refactor + * @param object $forum + * @return object A standard object with 2 variables: info (number of posts for this user) and time (last modified) + */ +function forum_user_outline($course, $user, $mod, $forum) { + if ($count = forum_count_user_posts($forum->id, $user->id)) { + if ($count->postcount > 0) { + $result = new object(); + $result->info = get_string("numposts", "forum", $count->postcount); + $result->time = $count->lastpost; + return $result; + } + } + return NULL; +} + + +/** + * + */ +function forum_user_complete($course, $user, $mod, $forum) { + global $CFG; + + if ($posts = forum_get_user_posts($forum->id, $user->id)) { + + if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $course->id)) { + error('Course Module ID was incorrect'); + } + $discussions = forum_get_user_involved_discussions($forum->id, $user->id); + + foreach ($posts as $post) { + if (!isset($discussions[$forum->discussion])) { + continue; + } + $discussion = $discussions[$forum->discussion]; + forum_print_post($post, $discussion, $forum, $cm, $course, false, false, false, false); + } + + } else { + echo "

".get_string("noposts", "forum")."

"; + } +} + +/** + * + */ +function forum_print_overview($courses,&$htmlarray) { + global $USER, $CFG; + $LIKE = sql_ilike(); + + if (empty($courses) || !is_array($courses) || count($courses) == 0) { + return array(); + } + + if (!$forums = get_all_instances_in_courses('forum',$courses)) { + return; + } + + + // get all forum logs in ONE query (much better!) + $sql = "SELECT instance,cmid,l.course,COUNT(l.id) as count FROM {$CFG->prefix}log l " + ." JOIN {$CFG->prefix}course_modules cm ON cm.id = cmid " + ." WHERE ("; + foreach ($courses as $course) { + $sql .= '(l.course = '.$course->id.' AND l.time > '.$course->lastaccess.') OR '; + } + $sql = substr($sql,0,-3); // take off the last OR + + $sql .= ") AND l.module = 'forum' AND action $LIKE 'add post%' " + ." AND userid != ".$USER->id." GROUP BY cmid,l.course,instance"; + + if (!$new = get_records_sql($sql)) { + $new = array(); // avoid warnings + } + + // also get all forum tracking stuff ONCE. + $trackingforums = array(); + foreach ($forums as $forum) { + if (forum_tp_can_track_forums($forum)) { + $trackingforums[$forum->id] = $forum; + } + } + + if (count($trackingforums) > 0) { + $cutoffdate = isset($CFG->forum_oldpostdays) ? (time() - ($CFG->forum_oldpostdays*24*60*60)) : 0; + $sql = 'SELECT d.forum,d.course,COUNT(p.id) AS count '. + ' FROM '.$CFG->prefix.'forum_posts p '. + ' JOIN '.$CFG->prefix.'forum_discussions d ON p.discussion = d.id '. + ' LEFT JOIN '.$CFG->prefix.'forum_read r ON r.postid = p.id AND r.userid = '.$USER->id.' WHERE ('; + foreach ($trackingforums as $track) { + $sql .= '(d.forum = '.$track->id.' AND (d.groupid = -1 OR d.groupid = 0 OR d.groupid = '.get_current_group($track->course).')) OR '; + } + $sql = substr($sql,0,-3); // take off the last OR + $sql .= ') AND p.modified >= '.$cutoffdate.' AND r.id is NULL GROUP BY d.forum,d.course'; + + if (!$unread = get_records_sql($sql)) { + $unread = array(); + } + } else { + $unread = array(); + } + + if (empty($unread) and empty($new)) { + return; + } + + $strforum = get_string('modulename','forum'); + $strnumunread = get_string('overviewnumunread','forum'); + $strnumpostssince = get_string('overviewnumpostssince','forum'); + + foreach ($forums as $forum) { + $str = ''; + $count = 0; + $thisunread = 0; + $showunread = false; + // either we have something from logs, or trackposts, or nothing. + if (array_key_exists($forum->id, $new) && !empty($new[$forum->id])) { + $count = $new[$forum->id]->count; + } + if (array_key_exists($forum->id,$unread)) { + $thisunread = $unread[$forum->id]->count; + $showunread = true; + } + if ($count > 0 || $thisunread > 0) { + $str .= '
'.$strforum.': '. + $forum->name.'
'; + $str .= '
'; + $str .= $count.' '.$strnumpostssince; + if (!empty($showunread)) { + $str .= '
'.$thisunread .' '.$strnumunread; + } + $str .= '
'; + } + if (!empty($str)) { + if (!array_key_exists($forum->course,$htmlarray)) { + $htmlarray[$forum->course] = array(); + } + if (!array_key_exists('forum',$htmlarray[$forum->course])) { + $htmlarray[$forum->course]['forum'] = ''; // initialize, avoid warnings + } + $htmlarray[$forum->course]['forum'] .= $str; + } + } +} + +/** + * Given a course and a date, prints a summary of all the new + * messages posted in the course since that date + * @param object $course + * @param bool $viewfullnames capability + * @param int $timestart + * @return bool success + */ +function forum_print_recent_activity($course, $viewfullnames, $timestart) { + global $CFG, $USER; + + // do not use log table if possible, it may be huge and is expensive to join with other tables + + if (!$posts = get_records_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid, + d.timestart, d.timeend, d.userid AS duserid, + u.firstname, u.lastname, u.email, u.picture + FROM {$CFG->prefix}forum_posts p + JOIN {$CFG->prefix}forum_discussions d ON d.id = p.discussion + JOIN {$CFG->prefix}forum f ON f.id = d.forum + JOIN {$CFG->prefix}user u ON u.id = p.userid + WHERE p.created > $timestart AND f.course = {$course->id} + ORDER BY p.id ASC")) { // order by initial posting date + return false; + } + + $modinfo =& get_fast_modinfo($course); + + $groupmodes = array(); + $cms = array(); + + $strftimerecent = get_string('strftimerecent'); + + $printposts = array(); + foreach ($posts as $post) { + if (!isset($modinfo->instances['forum'][$post->forum])) { + // not visible + continue; + } + $cm = $modinfo->instances['forum'][$post->forum]; + if (!$cm->uservisible) { + continue; + } + $context = get_context_instance(CONTEXT_MODULE, $cm->id); + + if (!has_capability('mod/forum:viewdiscussion', $context)) { + continue; + } + + if (!empty($CFG->forum_enabletimedposts) and $USER->id != $post->duserid + and (($post->timestart > 0 and $post->timestart > time()) or ($post->timeend > 0 and $post->timeend < time()))) { + if (!has_capability('mod/forum:viewhiddentimedposts', $context)) { + continue; + } + } + + $groupmode = groups_get_activity_groupmode($cm, $course); + + if ($groupmode) { + if ($post->groupid == -1 or $groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $context)) { + // oki (Open discussions have groupid -1) + } else { + // separate mode + if (isguestuser()) { + // shortcut + continue; + } + + if (is_null($modinfo->groups)) { + $modinfo->groups = groups_get_user_groups($course->id); // load all my groups and cache it in modinfo + } + + if (!array_key_exists($post->groupid, $modinfo->groups[0])) { + continue; + } + } + } + + $printposts[] = $post; + } + unset($posts); + + if (!$printposts) { + return false; + } + + print_headline(get_string('newforumposts', 'forum').':', 3); + echo "\n\n"; + + return true; +} + +/** + * Return grade for given user or all users. + * + * @param int $forumid id of forum + * @param int $userid optional user id, 0 means all users + * @return array array of grades, false if none + */ +function forum_get_user_grades($forum, $userid=0) { + global $CFG; + + $user = $userid ? "AND u.id = $userid" : ""; + + $aggtype = $forum->assessed; + switch ($aggtype) { + case FORUM_AGGREGATE_COUNT : + $sql = "SELECT u.id, u.id AS userid, COUNT(fr.rating) AS rawgrade + FROM {$CFG->prefix}user u, {$CFG->prefix}forum_posts fp, + {$CFG->prefix}forum_ratings fr, {$CFG->prefix}forum_discussions fd + WHERE u.id = fp.userid AND fp.discussion = fd.id AND fr.post = fp.id + AND fr.userid != u.id AND fd.forum = $forum->id + $user + GROUP BY u.id"; + break; + case FORUM_AGGREGATE_MAX : + $sql = "SELECT u.id, u.id AS userid, MAX(fr.rating) AS rawgrade + FROM {$CFG->prefix}user u, {$CFG->prefix}forum_posts fp, + {$CFG->prefix}forum_ratings fr, {$CFG->prefix}forum_discussions fd + WHERE u.id = fp.userid AND fp.discussion = fd.id AND fr.post = fp.id + AND fr.userid != u.id AND fd.forum = $forum->id + $user + GROUP BY u.id"; + break; + case FORUM_AGGREGATE_MIN : + $sql = "SELECT u.id, u.id AS userid, MIN(fr.rating) AS rawgrade + FROM {$CFG->prefix}user u, {$CFG->prefix}forum_posts fp, + {$CFG->prefix}forum_ratings fr, {$CFG->prefix}forum_discussions fd + WHERE u.id = fp.userid AND fp.discussion = fd.id AND fr.post = fp.id + AND fr.userid != u.id AND fd.forum = $forum->id + $user + GROUP BY u.id"; + break; + case FORUM_AGGREGATE_SUM : + $sql = "SELECT u.id, u.id AS userid, SUM(fr.rating) AS rawgrade + FROM {$CFG->prefix}user u, {$CFG->prefix}forum_posts fp, + {$CFG->prefix}forum_ratings fr, {$CFG->prefix}forum_discussions fd + WHERE u.id = fp.userid AND fp.discussion = fd.id AND fr.post = fp.id + AND fr.userid != u.id AND fd.forum = $forum->id + $user + GROUP BY u.id"; + break; + default : //avg + $sql = "SELECT u.id, u.id AS userid, AVG(fr.rating) AS rawgrade + FROM {$CFG->prefix}user u, {$CFG->prefix}forum_posts fp, + {$CFG->prefix}forum_ratings fr, {$CFG->prefix}forum_discussions fd + WHERE u.id = fp.userid AND fp.discussion = fd.id AND fr.post = fp.id + AND fr.userid != u.id AND fd.forum = $forum->id + $user + GROUP BY u.id"; + break; + } + + if ($results = get_records_sql($sql)) { + // it could throw off the grading if count and sum returned a rawgrade higher than scale + // so to prevent it we review the results and ensure that rawgrade does not exceed the scale, if it does we set rawgrade = scale (i.e. full credit) + foreach ($results as $rid=>$result) { + if ($forum->scale >= 0) { + //numeric + if ($result->rawgrade > $forum->scale) { + $results[$rid]->rawgrade = $forum->scale; + } + } else { + //scales + if ($scale = get_record('scale', 'id', -$forum->scale)) { + $scale = explode(',', $scale->scale); + $max = count($scale); + if ($result->rawgrade > $max) { + $results[$rid]->rawgrade = $max; + } + } + } + } + } + + return $results; +} + +/** + * Update grades by firing grade_updated event + * + * @param object $forum null means all forums + * @param int $userid specific user only, 0 mean all + * @param boolean $nullifnone return null if grade does not exist + * @return void + */ +function forum_update_grades($forum=null, $userid=0, $nullifnone=true) { + global $CFG; + + if ($forum != null) { + require_once($CFG->libdir.'/gradelib.php'); + if ($grades = forum_get_user_grades($forum, $userid)) { + forum_grade_item_update($forum, $grades); + + } else if ($userid and $nullifnone) { + $grade = new object(); + $grade->userid = $userid; + $grade->rawgrade = NULL; + forum_grade_item_update($forum, $grade); + + } else { + forum_grade_item_update($forum); + } + + } else { + $sql = "SELECT f.*, cm.idnumber as cmidnumber + FROM {$CFG->prefix}forum f, {$CFG->prefix}course_modules cm, {$CFG->prefix}modules m + WHERE m.name='forum' AND m.id=cm.module AND cm.instance=f.id"; + if ($rs = get_recordset_sql($sql)) { + while ($forum = rs_fetch_next_record($rs)) { + if ($forum->assessed) { + forum_update_grades($forum, 0, false); + } else { + forum_grade_item_update($forum); + } + } + rs_close($rs); + } + } +} + +/** + * Create/update grade item for given forum + * + * @param object $forum object with extra cmidnumber + * @param mixed optional array/object of grade(s); 'reset' means reset grades in gradebook + * @return int 0 if ok + */ +function forum_grade_item_update($forum, $grades=NULL) { + global $CFG; + if (!function_exists('grade_update')) { //workaround for buggy PHP versions + require_once($CFG->libdir.'/gradelib.php'); + } + + $params = array('itemname'=>$forum->name, 'idnumber'=>$forum->cmidnumber); + + if (!$forum->assessed or $forum->scale == 0) { + $params['gradetype'] = GRADE_TYPE_NONE; + + } else if ($forum->scale > 0) { + $params['gradetype'] = GRADE_TYPE_VALUE; + $params['grademax'] = $forum->scale; + $params['grademin'] = 0; + + } else if ($forum->scale < 0) { + $params['gradetype'] = GRADE_TYPE_SCALE; + $params['scaleid'] = -$forum->scale; + } + + if ($grades === 'reset') { + $params['reset'] = true; + $grades = NULL; + } + + return grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, $grades, $params); +} + +/** + * Delete grade item for given forum + * + * @param object $forum object + * @return object grade_item + */ +function forum_grade_item_delete($forum) { + global $CFG; + require_once($CFG->libdir.'/gradelib.php'); + + return grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, NULL, array('deleted'=>1)); +} + + +/** + * Returns the users with data in one forum + * (users with records in forum_subscriptions, forum_posts and forum_ratings, students) + * @param int $forumid + * @return mixed array or false if none + */ +function forum_get_participants($forumid) { + + global $CFG; + + //Get students from forum_subscriptions + $st_subscriptions = get_records_sql("SELECT DISTINCT u.id, u.id + FROM {$CFG->prefix}user u, + {$CFG->prefix}forum_subscriptions s + WHERE s.forum = '$forumid' and + u.id = s.userid"); + //Get students from forum_posts + $st_posts = get_records_sql("SELECT DISTINCT u.id, u.id + FROM {$CFG->prefix}user u, + {$CFG->prefix}forum_discussions d, + {$CFG->prefix}forum_posts p + WHERE d.forum = '$forumid' and + p.discussion = d.id and + u.id = p.userid"); + + //Get students from forum_ratings + $st_ratings = get_records_sql("SELECT DISTINCT u.id, u.id + FROM {$CFG->prefix}user u, + {$CFG->prefix}forum_discussions d, + {$CFG->prefix}forum_posts p, + {$CFG->prefix}forum_ratings r + WHERE d.forum = '$forumid' and + p.discussion = d.id and + r.post = p.id and + u.id = r.userid"); + + //Add st_posts to st_subscriptions + if ($st_posts) { + foreach ($st_posts as $st_post) { + $st_subscriptions[$st_post->id] = $st_post; + } + } + //Add st_ratings to st_subscriptions + if ($st_ratings) { + foreach ($st_ratings as $st_rating) { + $st_subscriptions[$st_rating->id] = $st_rating; + } + } + //Return st_subscriptions array (it contains an array of unique users) + return ($st_subscriptions); +} + +/** + * This function returns if a scale is being used by one forum + * @param int $forumid + * @param int $scaleid negative number + * @return bool + */ +function forum_scale_used ($forumid,$scaleid) { + + $return = false; + + $rec = get_record("forum","id","$forumid","scale","-$scaleid"); + + if (!empty($rec) && !empty($scaleid)) { + $return = true; + } + + return $return; +} + +/** + * Checks if scale is being used by any instance of forum + * + * This is used to find out if scale used anywhere + * @param $scaleid int + * @return boolean True if the scale is used by any forum + */ +function forum_scale_used_anywhere($scaleid) { + if ($scaleid and record_exists('forum', 'scale', -$scaleid)) { + return true; + } else { + return false; + } +} + +// SQL FUNCTIONS /////////////////////////////////////////////////////////// + +/** + * Gets a post with all info ready for forum_print_post + * Most of these joins are just to get the forum id + * @param int $postid + * @return mixed array of posts or false + */ +function forum_get_post_full($postid) { + global $CFG; + + return get_record_sql("SELECT p.*, d.forum, u.firstname, u.lastname, u.email, u.picture, u.imagealt + FROM {$CFG->prefix}forum_posts p + JOIN {$CFG->prefix}forum_discussions d ON p.discussion = d.id + LEFT JOIN {$CFG->prefix}user u ON p.userid = u.id + WHERE p.id = '$postid'"); +} + +/** + * Gets posts with all info ready for forum_print_post + * We pass forumid in because we always know it so no need to make a + * complicated join to find it out. + * @return mixed array of posts or false + */ +function forum_get_discussion_posts($discussion, $sort, $forumid) { + global $CFG; + + return get_records_sql("SELECT p.*, $forumid AS forum, u.firstname, u.lastname, u.email, u.picture, u.imagealt + FROM {$CFG->prefix}forum_posts p + LEFT JOIN {$CFG->prefix}user u ON p.userid = u.id + WHERE p.discussion = $discussion + AND p.parent > 0 $sort"); +} + +/** + * Gets all posts in discussion including top parent. + * @param int $discussionid + * @param string $sort + * @param bool $tracking does user track the forum? + * @return array of posts + */ +function forum_get_all_discussion_posts($discussionid, $sort, $tracking=false) { + global $CFG, $USER; + + $tr_sel = ""; + $tr_join = ""; + + if ($tracking) { + $now = time(); + $cutoffdate = $now - ($CFG->forum_oldpostdays * 24 * 3600); + $tr_sel = ", fr.id AS postread"; + $tr_join = "LEFT JOIN {$CFG->prefix}forum_read fr ON (fr.postid = p.id AND fr.userid = $USER->id)"; + } + + if (!$posts = get_records_sql("SELECT p.*, u.firstname, u.lastname, u.email, u.picture, u.imagealt $tr_sel + FROM {$CFG->prefix}forum_posts p + LEFT JOIN {$CFG->prefix}user u ON p.userid = u.id + $tr_join + WHERE p.discussion = $discussionid + ORDER BY $sort")) { + return array(); + } + + foreach ($posts as $pid=>$p) { + if ($tracking) { + if (forum_tp_is_post_old($p)) { + $posts[$pid]->postread = true; + } + } + if (!$p->parent) { + continue; + } + if (!isset($posts[$p->parent])) { + continue; // parent does not exist?? + } + if (!isset($posts[$p->parent]->children)) { + $posts[$p->parent]->children = array(); + } + $posts[$p->parent]->children[$pid] =& $posts[$pid]; + } + + return $posts; +} + +/** + * Gets posts with all info ready for forum_print_post + * We pass forumid in because we always know it so no need to make a + * complicated join to find it out. + */ +function forum_get_child_posts($parent, $forumid) { + global $CFG; + + return get_records_sql("SELECT p.*, $forumid AS forum, u.firstname, u.lastname, u.email, u.picture, u.imagealt + FROM {$CFG->prefix}forum_posts p + LEFT JOIN {$CFG->prefix}user u ON p.userid = u.id + WHERE p.parent = '$parent' + ORDER BY p.created ASC"); +} + +/** + * An array of forum objects that the user is allowed to read/search through. + * @param $userid + * @param $courseid - if 0, we look for forums throughout the whole site. + * @return array of forum objects, or false if no matches + * Forum objects have the following attributes: + * id, type, course, cmid, cmvisible, cmgroupmode, accessallgroups, + * viewhiddentimedposts + */ +function forum_get_readable_forums($userid, $courseid=0) { + + global $CFG, $USER; + require_once($CFG->dirroot.'/course/lib.php'); + + if (!$forummod = get_record('modules', 'name', 'forum')) { + error('The forum module is not installed'); + } + + if ($courseid) { + $courses = get_records('course', 'id', $courseid); + } else { + // If no course is specified, then the user can see SITE + his courses. + // And admins can see all courses, so pass the $doanything flag enabled + $courses1 = get_records('course', 'id', SITEID); + $courses2 = get_my_courses($userid, null, null, true); + $courses = array_merge($courses1, $courses2); + } + if (!$courses) { + return array(); + } + + $readableforums = array(); + + foreach ($courses as $course) { + + $modinfo =& get_fast_modinfo($course); + if (is_null($modinfo->groups)) { + $modinfo->groups = groups_get_user_groups($course->id, $userid); + } + + if (empty($modinfo->instances['forum'])) { + // hmm, no forums? + continue; + } + + $courseforums = get_records('forum', 'course', $course->id); + + foreach ($modinfo->instances['forum'] as $forumid => $cm) { + if (!$cm->uservisible or !isset($courseforums[$forumid])) { + continue; + } + $context = get_context_instance(CONTEXT_MODULE, $cm->id); + $forum = $courseforums[$forumid]; + + if (!has_capability('mod/forum:viewdiscussion', $context)) { + continue; + } + + /// group access + if (groups_get_activity_groupmode($cm, $course) == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) { + if (is_null($modinfo->groups)) { + $modinfo->groups = groups_get_user_groups($course->id, $USER->id); + } + if (empty($CFG->enablegroupings)) { + $forum->onlygroups = $modinfo->groups[0]; + $forum->onlygroups[] = -1; + } else if (isset($modinfo->groups[$cm->groupingid])) { + $forum->onlygroups = $modinfo->groups[$cm->groupingid]; + $forum->onlygroups[] = -1; + } else { + $forum->onlygroups = array(-1); + } + } + + /// hidden timed discussions + $forum->viewhiddentimedposts = true; + if (!empty($CFG->forum_enabletimedposts)) { + if (!has_capability('mod/forum:viewhiddentimedposts', $context)) { + $forum->viewhiddentimedposts = false; + } + } + + /// qanda access + if ($forum->type == 'qanda' + && !has_capability('mod/forum:viewqandawithoutposting', $context)) { + + // We need to check whether the user has posted in the qanda forum. + $forum->onlydiscussions = array(); // Holds discussion ids for the discussions + // the user is allowed to see in this forum. + if ($discussionspostedin = forum_discussions_user_has_posted_in($forum->id, $USER->id)) { + foreach ($discussionspostedin as $d) { + $forum->onlydiscussions[] = $d->id; + } + } + } + + $readableforums[$forum->id] = $forum; + } + + unset($modinfo); + + } // End foreach $courses + + //print_object($courses); + //print_object($readableforums); + + return $readableforums; +} + +/** + * Returns a list of posts found using an array of search terms. + * @param $searchterms - array of search terms, e.g. word +word -word + * @param $courseid - if 0, we search through the whole site + * @param $page + * @param $recordsperpage=50 + * @param &$totalcount + * @param $extrasql + * @return array of posts found + */ +function forum_search_posts($searchterms, $courseid=0, $limitfrom=0, $limitnum=50, + &$totalcount, $extrasql='') { + global $CFG, $USER; + require_once($CFG->libdir.'/searchlib.php'); + + $forums = forum_get_readable_forums($USER->id, $courseid); + + if (count($forums) == 0) { + $totalcount = 0; + return false; + } + + $now = round(time(), -2); // db friendly + + $fullaccess = array(); + $where = array(); + + foreach ($forums as $forumid => $forum) { + $select = array(); + + if (!$forum->viewhiddentimedposts) { + $select[] = "(d.userid = {$USER->id} OR (d.timestart < $now AND (d.timeend = 0 OR d.timeend > $now)))"; + } + + if ($forum->type == 'qanda') { + if (!empty($forum->onlydiscussions)) { + $discussionsids = implode(',', $forum->onlydiscussions); + $select[] = "(d.id IN ($discussionsids) OR p.parent = 0)"; + } else { + $select[] = "p.parent = 0"; + } + } + + if (!empty($forum->onlygroups)) { + $groupids = implode(',', $forum->onlygroups); + $select[] = "d.groupid IN ($groupids)"; + } + + if ($select) { + $selects = implode(" AND ", $select); + $where[] = "(d.forum = $forumid AND $selects)"; + } else { + $fullaccess[] = $forumid; + } + } + + if ($fullaccess) { + $fullids = implode(',', $fullaccess); + $where[] = "(d.forum IN ($fullids))"; + } + + $selectdiscussion = "(".implode(" OR ", $where).")"; + + // Some differences SQL + $LIKE = sql_ilike(); + $NOTLIKE = 'NOT ' . $LIKE; + if ($CFG->dbfamily == 'postgres') { + $REGEXP = '~*'; + $NOTREGEXP = '!~*'; + } else { + $REGEXP = 'REGEXP'; + $NOTREGEXP = 'NOT REGEXP'; + } + + $messagesearch = ''; + $searchstring = ''; + + // Need to concat these back together for parser to work. + foreach($searchterms as $searchterm){ + if ($searchstring != '') { + $searchstring .= ' '; + } + $searchstring .= $searchterm; + } + + // We need to allow quoted strings for the search. The quotes *should* be stripped + // by the parser, but this should be examined carefully for security implications. + $searchstring = str_replace("\\\"","\"",$searchstring); + $parser = new search_parser(); + $lexer = new search_lexer($parser); + + if ($lexer->parse($searchstring)) { + $parsearray = $parser->get_parsed_array(); + // Experimental feature under 1.8! MDL-8830 + // Use alternative text searches if defined + // This feature only works under mysql until properly implemented for other DBs + // Requires manual creation of text index for forum_posts before enabling it: + // CREATE FULLTEXT INDEX foru_post_tix ON [prefix]forum_posts (subject, message) + // Experimental feature under 1.8! MDL-8830 + if (!empty($CFG->forum_usetextsearches)) { + $messagesearch = search_generate_text_SQL($parsearray, 'p.message', 'p.subject', + 'p.userid', 'u.id', 'u.firstname', + 'u.lastname', 'p.modified', 'd.forum'); + } else { + $messagesearch = search_generate_SQL($parsearray, 'p.message', 'p.subject', + 'p.userid', 'u.id', 'u.firstname', + 'u.lastname', 'p.modified', 'd.forum'); + } + } + + $fromsql = "{$CFG->prefix}forum_posts p, + {$CFG->prefix}forum_discussions d, + {$CFG->prefix}user u"; + + $selectsql = " $messagesearch + AND p.discussion = d.id + AND p.userid = u.id + AND $selectdiscussion + $extrasql"; + + $countsql = "SELECT COUNT(*) + FROM $fromsql + WHERE $selectsql"; + + $searchsql = "SELECT p.*, + d.forum, + u.firstname, + u.lastname, + u.email, + u.picture, + u.imagealt + FROM $fromsql + WHERE $selectsql + ORDER BY p.modified DESC"; + + $totalcount = count_records_sql($countsql); + + return get_records_sql($searchsql, $limitfrom, $limitnum); +} + +/** + * Returns a list of ratings for all posts in discussion + * @param object $discussion + * @return array of ratings or false + */ +function forum_get_all_discussion_ratings($discussion) { + global $CFG; + return get_records_sql("SELECT r.id, r.userid, p.id AS postid, r.rating + FROM {$CFG->prefix}forum_ratings r, + {$CFG->prefix}forum_posts p + WHERE r.post = p.id AND p.discussion = $discussion->id + ORDER BY p.id ASC"); +} + +/** + * Returns a list of ratings for a particular post - sorted. + * @param int $postid + * @param string $sort + * @return array of ratings or false + */ +function forum_get_ratings($postid, $sort="u.firstname ASC") { + global $CFG; + return get_records_sql("SELECT u.*, r.rating, r.time + FROM {$CFG->prefix}forum_ratings r, + {$CFG->prefix}user u + WHERE r.post = '$postid' + AND r.userid = u.id + ORDER BY $sort"); +} + +/** + * Returns a list of all new posts that have not been mailed yet + * @param int $starttime - posts created after this time + * @param int $endtime - posts created before this + * @param int $now - used for timed discussions only + */ +function forum_get_unmailed_posts($starttime, $endtime, $now=null) { + global $CFG; + + if (!empty($CFG->forum_enabletimedposts)) { + if (empty($now)) { + $now = time(); + } + $timedsql = "AND (d.timestart < $now AND (d.timeend = 0 OR d.timeend > $now))"; + } else { + $timedsql = ""; + } + + return get_records_sql("SELECT p.*, d.course, d.forum + FROM {$CFG->prefix}forum_posts p + JOIN {$CFG->prefix}forum_discussions d ON d.id = p.discussion + WHERE p.mailed = 0 + AND p.created >= $starttime + AND (p.created < $endtime OR p.mailnow = 1) + $timedsql + ORDER BY p.modified ASC"); +} + +/** + * Marks posts before a certain time as being mailed already + */ +function forum_mark_old_posts_as_mailed($endtime, $now=null) { + global $CFG; + if (empty($now)) { + $now = time(); + } + + if (empty($CFG->forum_enabletimedposts)) { + return execute_sql("UPDATE {$CFG->prefix}forum_posts + SET mailed = '1' + WHERE (created < $endtime OR mailnow = 1) + AND mailed = 0", false); + + } else { + return execute_sql("UPDATE {$CFG->prefix}forum_posts + SET mailed = '1' + WHERE discussion NOT IN (SELECT d.id + FROM {$CFG->prefix}forum_discussions d + WHERE d.timestart > $now) + AND (created < $endtime OR mailnow = 1) + AND mailed = 0", false); + } +} + +/** + * Get all the posts for a user in a forum suitable for forum_print_post + */ +function forum_get_user_posts($forumid, $userid) { + global $CFG; + + $timedsql = ""; + if (!empty($CFG->forum_enabletimedposts)) { + $cm = get_coursemodule_from_instance('forum', $forumid); + if (!has_capability('mod/forum:viewhiddentimedposts' , get_context_instance(CONTEXT_MODULE, $cm->id))) { + $now = time(); + $timedsql = "AND (d.timestart < $now AND (d.timeend = 0 OR d.timeend > $now))"; + } + } + + return get_records_sql("SELECT p.*, d.forum, u.firstname, u.lastname, u.email, u.picture, u.imagealt + FROM {$CFG->prefix}forum f + JOIN {$CFG->prefix}forum_discussions d ON d.forum = f.id + JOIN {$CFG->prefix}forum_posts p ON p.discussion = d.id + JOIN {$CFG->prefix}user u ON u.id = p.userid + WHERE f.id = $forumid + AND p.userid = $userid + $timedsql + ORDER BY p.modified ASC"); +} + +/** + * Get all the discussions user participated in + * @param int $forumid + * @param int $userid + * @return array or false + */ +function forum_get_user_involved_discussions($forumid, $userid) { + global $CFG; + + $timedsql = ""; + if (!empty($CFG->forum_enabletimedposts)) { + $cm = get_coursemodule_from_instance('forum', $forumid); + if (!has_capability('mod/forum:viewhiddentimedposts' , get_context_instance(CONTEXT_MODULE, $cm->id))) { + $now = time(); + $timedsql = "AND (d.timestart < $now AND (d.timeend = 0 OR d.timeend > $now))"; + } + } + + return get_records_sql("SELECT DISTINCT d.* + FROM {$CFG->prefix}forum f + JOIN {$CFG->prefix}forum_discussions d ON d.forum = f.id + JOIN {$CFG->prefix}forum_posts p ON p.discussion = d.id + WHERE f.id = $forumid + AND p.userid = $userid + $timedsql"); +} + +/** + * Get all the posts for a user in a forum suitable for forum_print_post + * @param int $forumid + * @param int $userid + * @return array of counts or false + */ +function forum_count_user_posts($forumid, $userid) { + global $CFG; + + $timedsql = ""; + if (!empty($CFG->forum_enabletimedposts)) { + $cm = get_coursemodule_from_instance('forum', $forumid); + if (!has_capability('mod/forum:viewhiddentimedposts' , get_context_instance(CONTEXT_MODULE, $cm->id))) { + $now = time(); + $timedsql = "AND (d.timestart < $now AND (d.timeend = 0 OR d.timeend > $now))"; + } + } + + return get_record_sql("SELECT COUNT(p.id) AS postcount, MAX(p.modified) AS lastpost + FROM {$CFG->prefix}forum f + JOIN {$CFG->prefix}forum_discussions d ON d.forum = f.id + JOIN {$CFG->prefix}forum_posts p ON p.discussion = d.id + JOIN {$CFG->prefix}user u ON u.id = p.userid + WHERE f.id = $forumid + AND p.userid = $userid + $timedsql"); +} + +/** + * Given a log entry, return the forum post details for it. + */ +function forum_get_post_from_log($log) { + global $CFG; + + if ($log->action == "add post") { + + return get_record_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid, + u.firstname, u.lastname, u.email, u.picture + FROM {$CFG->prefix}forum_discussions d, + {$CFG->prefix}forum_posts p, + {$CFG->prefix}forum f, + {$CFG->prefix}user u + WHERE p.id = '$log->info' + AND d.id = p.discussion + AND p.userid = u.id + AND u.deleted <> '1' + AND f.id = d.forum"); + + + } else if ($log->action == "add discussion") { + + return get_record_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid, + u.firstname, u.lastname, u.email, u.picture + FROM {$CFG->prefix}forum_discussions d, + {$CFG->prefix}forum_posts p, + {$CFG->prefix}forum f, + {$CFG->prefix}user u + WHERE d.id = '$log->info' + AND d.firstpost = p.id + AND p.userid = u.id + AND u.deleted <> '1' + AND f.id = d.forum"); + } + return NULL; +} + +/** + * Given a discussion id, return the first post from the discussion + */ +function forum_get_firstpost_from_discussion($discussionid) { + global $CFG; + + return get_record_sql("SELECT p.* + FROM {$CFG->prefix}forum_discussions d, + {$CFG->prefix}forum_posts p + WHERE d.id = '$discussionid' + AND d.firstpost = p.id "); +} + +/** + * Returns an array of counts of replies to each discussion + */ +function forum_count_discussion_replies($forumid, $forumsort="", $limit=-1, $page=-1, $perpage=0) { + global $CFG; + + if ($limit > 0) { + $limitfrom = 0; + $limitnum = $limit; + } else if ($page != -1) { + $limitfrom = $page*$perpage; + $limitnum = $perpage; + } else { + $limitfrom = 0; + $limitnum = 0; + } + + if ($forumsort == "") { + $orderby = ""; + $groupby = ""; + + } else { + $orderby = "ORDER BY $forumsort"; + $groupby = ", ".strtolower($forumsort); + $groupby = str_replace('desc', '', $groupby); + $groupby = str_replace('asc', '', $groupby); + } + + if (($limitfrom == 0 and $limitnum == 0) or $forumsort == "") { + $sql = "SELECT p.discussion, COUNT(p.id) AS replies, MAX(p.id) AS lastpostid + FROM {$CFG->prefix}forum_posts p + JOIN {$CFG->prefix}forum_discussions d ON p.discussion = d.id + WHERE p.parent > 0 AND d.forum = $forumid + GROUP BY p.discussion"; + return get_records_sql($sql); + + } else { + $sql = "SELECT p.discussion, (COUNT(p.id) - 1) AS replies, MAX(p.id) AS lastpostid + FROM {$CFG->prefix}forum_posts p + JOIN {$CFG->prefix}forum_discussions d ON p.discussion = d.id + WHERE d.forum = $forumid + GROUP BY p.discussion $groupby + $orderby"; + return get_records_sql("SELECT * FROM ($sql) sq", $limitfrom, $limitnum); + } +} + +function forum_count_discussions($forum, $cm, $course) { + global $CFG, $USER; + + static $cache = array(); + + $now = round(time(), -2); // db cache friendliness + + if (!isset($cache[$course->id])) { + if (!empty($CFG->forum_enabletimedposts)) { + $timedsql = "AND d.timestart < $now AND (d.timeend = 0 OR d.timeend > $now)"; + } else { + $timedsql = ""; + } + + $sql = "SELECT f.id, COUNT(d.id) as dcount + FROM {$CFG->prefix}forum f + JOIN {$CFG->prefix}forum_discussions d ON d.forum = f.id + WHERE f.course = $course->id + $timedsql + GROUP BY f.id"; + + if ($counts = get_records_sql($sql)) { + foreach ($counts as $count) { + $counts[$count->id] = $count->dcount; + } + $cache[$course->id] = $counts; + } else { + $cache[$course->id] = array(); + } + } + + if (empty($cache[$course->id][$forum->id])) { + return 0; + } + + $groupmode = groups_get_activity_groupmode($cm, $course); + + if ($groupmode != SEPARATEGROUPS) { + return $cache[$course->id][$forum->id]; + } + + if (has_capability('moodle/site:accessallgroups', get_context_instance(CONTEXT_MODULE, $cm->id))) { + return $cache[$course->id][$forum->id]; + } + + require_once($CFG->dirroot.'/course/lib.php'); + + $modinfo =& get_fast_modinfo($course); + if (is_null($modinfo->groups)) { + $modinfo->groups = groups_get_user_groups($course->id, $USER->id); + } + + if (empty($CFG->enablegroupings)) { + $mygroups = $modinfo->groups[0]; + } else { + $mygroups = $modinfo->groups[$cm->groupingid]; + } + + // add all groups posts + if (empty($mygroups)) { + $mygroups = array(-1=>-1); + } else { + $mygroups[-1] = -1; + } + $mygroups = implode(',', $mygroups); + + if (!empty($CFG->forum_enabletimedposts)) { + $timedsql = "AND d.timestart < $now AND (d.timeend = 0 OR d.timeend > $now)"; + } else { + $timedsql = ""; + } + + $sql = "SELECT COUNT(d.id) + FROM {$CFG->prefix}forum_discussions d + WHERE d.forum = $forum->id AND d.groupid IN ($mygroups) + $timedsql"; + + return get_field_sql($sql); +} + +/** + * How many unrated posts are in the given discussion for a given user? + */ +function forum_count_unrated_posts($discussionid, $userid) { + global $CFG; + if ($posts = get_record_sql("SELECT count(*) as num + FROM {$CFG->prefix}forum_posts + WHERE parent > 0 + AND discussion = '$discussionid' + AND userid <> '$userid' ")) { + + if ($rated = get_record_sql("SELECT count(*) as num + FROM {$CFG->prefix}forum_posts p, + {$CFG->prefix}forum_ratings r + WHERE p.discussion = '$discussionid' + AND p.id = r.post + AND r.userid = '$userid'")) { + $difference = $posts->num - $rated->num; + if ($difference > 0) { + return $difference; + } else { + return 0; // Just in case there was a counting error + } + } else { + return $posts->num; + } + } else { + return 0; + } +} + +/** + * Get all discussions in a forum + */ +function forum_get_discussions($cm, $forumsort="d.timemodified DESC", $fullpost=true, $unused=-1, $limit=-1, $userlastmodified=false, $page=-1, $perpage=0) { + global $CFG, $USER; + + $timelimit = ''; + + $modcontext = null; + + $now = round(time(), -2); + + if (!empty($CFG->forum_enabletimedposts)) { + + $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id); + + if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) { + $timelimit = " AND ((d.timestart <= $now AND (d.timeend = 0 OR d.timeend > $now))"; + if (isloggedin()) { + $timelimit .= " OR d.userid = $USER->id"; + } + $timelimit .= ")"; + } + } + + if ($limit > 0) { + $limitfrom = 0; + $limitnum = $limit; + } else if ($page != -1) { + $limitfrom = $page*$perpage; + $limitnum = $perpage; + } else { + $limitfrom = 0; + $limitnum = 0; + } + + $groupmode = groups_get_activity_groupmode($cm); + $currentgroup = groups_get_activity_group($cm); + + if ($groupmode) { + if (empty($modcontext)) { + $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id); + } + + if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) { + if ($currentgroup) { + $groupselect = "AND (d.groupid = $currentgroup OR d.groupid = -1)"; + } else { + $groupselect = ""; + } + + } else { + //seprate groups without access all + if ($currentgroup) { + $groupselect = "AND (d.groupid = $currentgroup OR d.groupid = -1)"; + } else { + $groupselect = "AND d.groupid = -1"; + } + } + } else { + $groupselect = ""; + } + + + if (empty($forumsort)) { + $forumsort = "d.timemodified DESC"; + } + if (empty($fullpost)) { + $postdata = "p.id,p.subject,p.modified,p.discussion,p.userid"; + } else { + $postdata = "p.*"; + } + + if (empty($userlastmodified)) { // We don't need to know this + $umfields = ""; + $umtable = ""; + } else { + $umfields = ", um.firstname AS umfirstname, um.lastname AS umlastname"; + $umtable = " LEFT JOIN {$CFG->prefix}user um ON (d.usermodified = um.id)"; + } + + $sql = "SELECT $postdata, d.name, d.timemodified, d.usermodified, d.groupid, d.timestart, d.timeend, + u.firstname, u.lastname, u.email, u.picture, u.imagealt $umfields + FROM {$CFG->prefix}forum_discussions d + JOIN {$CFG->prefix}forum_posts p ON p.discussion = d.id + JOIN {$CFG->prefix}user u ON p.userid = u.id + $umtable + WHERE d.forum = {$cm->instance} AND p.parent = 0 + $timelimit $groupselect + ORDER BY $forumsort"; + return get_records_sql($sql, $limitfrom, $limitnum); +} + +function forum_get_discussions_unread($cm) { + global $CFG, $USER; + + $now = round(time(), -2); + + $groupmode = groups_get_activity_groupmode($cm); + $currentgroup = groups_get_activity_group($cm); + + if ($groupmode) { + $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id); + + if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) { + if ($currentgroup) { + $groupselect = "AND (d.groupid = $currentgroup OR d.groupid = -1)"; + } else { + $groupselect = ""; + } + + } else { + //seprate groups without access all + if ($currentgroup) { + $groupselect = "AND (d.groupid = $currentgroup OR d.groupid = -1)"; + } else { + $groupselect = "AND d.groupid = -1"; + } + } + } else { + $groupselect = ""; + } + + $cutoffdate = $now - ($CFG->forum_oldpostdays*24*60*60); + + if (!empty($CFG->forum_enabletimedposts)) { + $timedsql = "AND d.timestart < $now AND (d.timeend = 0 OR d.timeend > $now)"; + } else { + $timedsql = ""; + } + + $sql = "SELECT d.id, COUNT(p.id) AS unread + FROM {$CFG->prefix}forum_discussions d + JOIN {$CFG->prefix}forum_posts p ON p.discussion = d.id + LEFT JOIN {$CFG->prefix}forum_read r ON (r.postid = p.id AND r.userid = $USER->id) + WHERE d.forum = {$cm->instance} + AND p.modified >= $cutoffdate AND r.id is NULL + $timedsql + $groupselect + GROUP BY d.id"; + if ($unreads = get_records_sql($sql)) { + foreach ($unreads as $unread) { + $unreads[$unread->id] = $unread->unread; + } + return $unreads; + } else { + return array(); + } +} + +function forum_get_discussions_count($cm) { + global $CFG, $USER; + + $now = round(time(), -2); + + $groupmode = groups_get_activity_groupmode($cm); + $currentgroup = groups_get_activity_group($cm); + + if ($groupmode) { + $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id); + + if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) { + if ($currentgroup) { + $groupselect = "AND (d.groupid = $currentgroup OR d.groupid = -1)"; + } else { + $groupselect = ""; + } + + } else { + //seprate groups without access all + if ($currentgroup) { + $groupselect = "AND (d.groupid = $currentgroup OR d.groupid = -1)"; + } else { + $groupselect = "AND d.groupid = -1"; + } + } + } else { + $groupselect = ""; + } + + $cutoffdate = $now - ($CFG->forum_oldpostdays*24*60*60); + + $timelimit = ""; + + if (!empty($CFG->forum_enabletimedposts)) { + + $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id); + + if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) { + $timelimit = " AND ((d.timestart <= $now AND (d.timeend = 0 OR d.timeend > $now))"; + if (isloggedin()) { + $timelimit .= " OR d.userid = $USER->id"; + } + $timelimit .= ")"; + } + } + + $sql = "SELECT COUNT(d.id) + FROM {$CFG->prefix}forum_discussions d + JOIN {$CFG->prefix}forum_posts p ON p.discussion = d.id + WHERE d.forum = {$cm->instance} AND p.parent = 0 + $timelimit $groupselect"; + + return get_field_sql($sql); +} + + +/** + * Get all discussions started by a particular user in a course (or group) + * This function no longer used ... + */ +function forum_get_user_discussions($courseid, $userid, $groupid=0) { + global $CFG; + + if ($groupid) { + $groupselect = " AND d.groupid = '$groupid' "; + } else { + $groupselect = ""; + } + + return get_records_sql("SELECT p.*, d.groupid, u.firstname, u.lastname, u.email, u.picture, u.imagealt, + f.type as forumtype, f.name as forumname, f.id as forumid + FROM {$CFG->prefix}forum_discussions d, + {$CFG->prefix}forum_posts p, + {$CFG->prefix}user u, + {$CFG->prefix}forum f + WHERE d.course = '$courseid' + AND p.discussion = d.id + AND p.parent = 0 + AND p.userid = u.id + AND u.id = '$userid' + AND d.forum = f.id $groupselect + ORDER BY p.created DESC"); +} + +/** + * Returns list of user objects that are subscribed to this forum + */ +function forum_subscribed_users($course, $forum, $groupid=0) { + + global $CFG; + + if ($groupid) { + $grouptables = ", {$CFG->prefix}groups_members gm "; + $groupselect = "AND gm.groupid = $groupid AND u.id = gm.userid"; + + } else { + $grouptables = ''; + $groupselect = ''; + } + + if (forum_is_forcesubscribed($forum)) { + $context = get_context_instance(CONTEXT_COURSE, $course->id); + $sort = "u.email ASC"; + $fields ="u.id, u.username, u.firstname, u.lastname, u.maildisplay, u.mailformat, u.maildigest, u.emailstop, u.imagealt, + u.email, u.city, u.country, u.lastaccess, u.lastlogin, u.picture, u.timezone, u.theme, u.lang, u.trackforums, u.mnethostid"; + $results = get_users_by_capability($context, 'mod/forum:initialsubscriptions', $fields, $sort, '','','','', false, true); + } else { + $results = get_records_sql("SELECT u.id, u.username, u.firstname, u.lastname, u.maildisplay, u.mailformat, u.maildigest, u.emailstop, u.imagealt, + u.email, u.city, u.country, u.lastaccess, u.lastlogin, u.picture, u.timezone, u.theme, u.lang, u.trackforums, u.mnethostid + FROM {$CFG->prefix}user u, + {$CFG->prefix}forum_subscriptions s $grouptables + WHERE s.forum = '$forum->id' + AND s.userid = u.id + AND u.deleted = 0 $groupselect + ORDER BY u.email ASC"); + } + + static $guestid = null; + + if (is_null($guestid)) { + if ($guest = guest_user()) { + $guestid = $guest->id; + } else { + $guestid = 0; + } + } + + // Guest user should never be subscribed to a forum. + unset($results[$guestid]); + + return $results; +} + + + +// OTHER FUNCTIONS /////////////////////////////////////////////////////////// + + +function forum_get_course_forum($courseid, $type) { +// How to set up special 1-per-course forums + global $CFG; + + if ($forums = get_records_select("forum", "course = '$courseid' AND type = '$type'", "id ASC")) { + // There should always only be ONE, but with the right combination of + // errors there might be more. In this case, just return the oldest one (lowest ID). + foreach ($forums as $forum) { + return $forum; // ie the first one + } + } + + // Doesn't exist, so create one now. + $forum->course = $courseid; + $forum->type = "$type"; + switch ($forum->type) { + case "news": + $forum->name = addslashes(get_string("namenews", "forum")); + $forum->intro = addslashes(get_string("intronews", "forum")); + $forum->forcesubscribe = FORUM_FORCESUBSCRIBE; + $forum->assessed = 0; + if ($courseid == SITEID) { + $forum->name = get_string("sitenews"); + $forum->forcesubscribe = 0; + } + break; + case "social": + $forum->name = addslashes(get_string("namesocial", "forum")); + $forum->intro = addslashes(get_string("introsocial", "forum")); + $forum->assessed = 0; + $forum->forcesubscribe = 0; + break; + default: + notify("That forum type doesn't exist!"); + return false; + break; + } + + $forum->timemodified = time(); + $forum->id = insert_record("forum", $forum); + + if (! $module = get_record("modules", "name", "forum")) { + notify("Could not find forum module!!"); + return false; + } + $mod = new object(); + $mod->course = $courseid; + $mod->module = $module->id; + $mod->instance = $forum->id; + $mod->section = 0; + if (! $mod->coursemodule = add_course_module($mod) ) { // assumes course/lib.php is loaded + notify("Could not add a new course module to the course '" . format_string($course->fullname) . "'"); + return false; + } + if (! $sectionid = add_mod_to_section($mod) ) { // assumes course/lib.php is loaded + notify("Could not add the new course module to that section"); + return false; + } + if (! set_field("course_modules", "section", $sectionid, "id", $mod->coursemodule)) { + notify("Could not update the course module with the correct section"); + return false; + } + include_once("$CFG->dirroot/course/lib.php"); + rebuild_course_cache($courseid); + + return get_record("forum", "id", "$forum->id"); +} + + +/** +* Given the data about a posting, builds up the HTML to display it and +* returns the HTML in a string. This is designed for sending via HTML email. +*/ +function forum_make_mail_post($course, $forum, $discussion, $post, $userfrom, $userto, + $ownpost=false, $reply=false, $link=false, $rate=false, $footer="") { + + global $CFG; + + if (!isset($userto->viewfullnames[$forum->id])) { + if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $course->id)) { + error('Course Module ID was incorrect'); + } + $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id); + $viewfullnames = has_capability('moodle/site:viewfullnames', $modcontext, $userto->id); + } else { + $viewfullnames = $userto->viewfullnames[$forum->id]; + } + + // format the post body + $options = new object(); + $options->para = true; + $formattedtext = format_text(trusttext_strip($post->message), $post->format, $options, $course->id); + + $output = ''; + + $output .= ''; + + if ($post->parent) { + $output .= ''; + + $output .= '
'; + $output .= print_user_picture($userfrom, $course->id, $userfrom->picture, false, true); + $output .= ''; + } else { + $output .= ''; + } + $output .= '
'.format_string($post->subject).'
'; + + $fullname = fullname($userfrom, $viewfullnames); + $by = new object(); + $by->name = ''.$fullname.''; + $by->date = userdate($post->modified, '', $userto->timezone); + $output .= '
'.get_string('bynameondate', 'forum', $by).'
'; + + $output .= '
'; + + if (isset($userfrom->groups)) { + $groups = $userfrom->groups[$forum->id]; + } else { + if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $course->id)) { + error('Course Module ID was incorrect'); + } + $group = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid); + } + + if ($groups) { + $output .= print_group_picture($groups, $course->id, false, true, true); + } else { + $output .= ' '; + } + + $output .= ''; + + if ($post->attachment) { + $post->course = $course->id; + $output .= '
'; + $output .= forum_print_attachments($post, 'html'); + $output .= "
"; + } + + $output .= $formattedtext; + +// Commands + $commands = array(); + + if ($post->parent) { + $commands[] = ''.get_string('parent', 'forum').''; + } + + if ($reply) { + $commands[] = ''. + get_string('reply', 'forum').''; + } + + $output .= '
'; + $output .= implode(' | ', $commands); + $output .= '
'; + +// Context link to post if required + if ($link) { + $output .= ''; + } + + if ($footer) { + $output .= ''; + } + $output .= '
'."\n\n"; + + return $output; +} + +/** + * Print a forum post + * + * @param object $post The post to print. + * @param integer $courseid The course this post belongs to. + * @param boolean $ownpost Whether this post belongs to the current user. + * @param boolean $reply Whether to print a 'reply' link at the bottom of the message. + * @param boolean $link Just print a shortened version of the post as a link to the full post. + * @param object $ratings -- I don't really know -- + * @param string $footer Extra stuff to print after the message. + * @param string $highlight Space-separated list of terms to highlight. + * @param int $post_read true, false or -99. If we already know whether this user + * has read this post, pass that in, otherwise, pass in -99, and this + * function will work it out. + * @param boolean $dummyifcantsee When forum_user_can_see_post says that + * the current user can't see this post, if this argument is true + * (the default) then print a dummy 'you can't see this post' post. + * If false, don't output anything at all. + */ +function forum_print_post($post, $discussion, $forum, &$cm, $course, $ownpost=false, $reply=false, $link=false, + $ratings=NULL, $footer="", $highlight="", $post_read=null, $dummyifcantsee=true, $istracked=null) { + + global $USER, $CFG; + + static $stredit, $strdelete, $strreply, $strparent, $strprune; + static $strpruneheading, $displaymode; + static $strmarkread, $strmarkunread; + + $post->course = $course->id; + $post->forum = $forum->id; + + // caching + if (!isset($cm->cache)) { + $cm->cache = new object(); + } + + if (!isset($cm->cache->caps)) { + $cm->cache->caps = array(); + $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id); + $cm->cache->caps['mod/forum:viewdiscussion'] = has_capability('mod/forum:viewdiscussion', $modcontext); + $cm->cache->caps['moodle/site:viewfullnames'] = has_capability('moodle/site:viewfullnames', $modcontext); + $cm->cache->caps['mod/forum:editanypost'] = has_capability('mod/forum:editanypost', $modcontext); + $cm->cache->caps['mod/forum:splitdiscussions'] = has_capability('mod/forum:splitdiscussions', $modcontext); + $cm->cache->caps['mod/forum:deleteownpost'] = has_capability('mod/forum:deleteownpost', $modcontext); + $cm->cache->caps['mod/forum:deleteanypost'] = has_capability('mod/forum:deleteanypost', $modcontext); + $cm->cache->caps['mod/forum:viewanyrating'] = has_capability('mod/forum:viewanyrating', $modcontext); + } + + if (!isset($cm->uservisible)) { + $cm->uservisible = coursemodule_visible_for_user($cm); + } + + if (!forum_user_can_see_post($forum, $discussion, $post, NULL, $cm)) { + if (!$dummyifcantsee) { + return; + } + echo ''; + echo ''; + echo ''; + if ($post->parent) { + echo ''; + + echo '
'; + // print_user_picture($post->userid, $courseid, $post->picture); + echo ''; + } else { + echo ''; + } + echo '
'.get_string('forumsubjecthidden','forum').'
'; + echo '
'; + print_string('forumauthorhidden','forum'); + echo '
'; + echo ' '; + + // Actual content + + echo ''."\n"; + echo get_string('forumbodyhidden','forum'); + echo '
'; + return; + } + + if (empty($stredit)) { + $stredit = get_string('edit', 'forum'); + $strdelete = get_string('delete', 'forum'); + $strreply = get_string('reply', 'forum'); + $strparent = get_string('parent', 'forum'); + $strpruneheading = get_string('pruneheading', 'forum'); + $strprune = get_string('prune', 'forum'); + $displaymode = get_user_preferences('forum_displaymode', $CFG->forum_displaymode); + $strmarkread = get_string('markread', 'forum'); + $strmarkunread = get_string('markunread', 'forum'); + + } + + $read_style = ''; + // ignore trackign status if not tracked or tracked param missing + if ($istracked) { + if (is_null($post_read)) { + debugging('fetching post_read info'); + $post_read = forum_tp_is_post_read($USER->id, $post); + } + + if ($post_read) { + $read_style = ' read'; + } else { + $read_style = ' unread'; + echo ''; + } + } + + echo ''; + echo ''; + + // Picture + $postuser = new object(); + $postuser->id = $post->userid; + $postuser->firstname = $post->firstname; + $postuser->lastname = $post->lastname; + $postuser->imagealt = $post->imagealt; + $postuser->picture = $post->picture; + + echo ''; + + if ($post->parent) { + echo ''; + + echo '
'; + print_user_picture($postuser, $course->id); + echo ''; + } else { + echo ''; + } + + if (!empty($post->subjectnoformat)) { + echo '
'.$post->subject.'
'; + } else { + echo '
'.format_string($post->subject).'
'; + } + + echo '
'; + $fullname = fullname($postuser, $cm->cache->caps['moodle/site:viewfullnames']); + $by = new object(); + $by->name = ''.$fullname.''; + $by->date = userdate($post->modified); + print_string('bynameondate', 'forum', $by); + echo '
'; + if (isset($cm->cache->usersgroups)) { + $groups = array(); + if (isset($cm->cache->usersgroups[$post->userid])) { + foreach ($cm->cache->usersgroups[$post->userid] as $gid) { + $groups[$gid] = $cm->cache->groups[$gid]; + } + } + } else { + $groups = groups_get_all_groups($course->id, $post->userid, $cm->groupingid); + } + + if ($groups) { + print_group_picture($groups, $course->id, false, false, true); + } else { + echo ' '; + } + +// Actual content + + echo ''."\n"; + + if ($post->attachment) { + echo '
'; + $attachedimages = forum_print_attachments($post); + echo '
'; + } else { + $attachedimages = ''; + } + + + $options = new object(); + $options->para = false; + $options->trusttext = true; + if ($link and (strlen(strip_tags($post->message)) > $CFG->forum_longpost)) { + // Print shortened version + echo format_text(forum_shorten_post($post->message), $post->format, $options, $course->id); + $numwords = count_words(strip_tags($post->message)); + echo '

'; + echo get_string('readtherest', 'forum'); + echo ' ('.get_string('numwords', '', $numwords).')...

'; + } else { + // Print whole message + if ($highlight) { + echo highlight($highlight, format_text($post->message, $post->format, $options, $course->id)); + } else { + echo format_text($post->message, $post->format, $options, $course->id); + } + echo $attachedimages; + } + + +// Commands + + $commands = array(); + + if ($istracked) { + // SPECIAL CASE: The front page can display a news item post to non-logged in users. + // Don't display the mark read / unread controls in this case. + if ($CFG->forum_usermarksread and isloggedin()) { + if ($post_read) { + $mcmd = '&mark=unread&postid='.$post->id; + $mtxt = $strmarkunread; + } else { + $mcmd = '&mark=read&postid='.$post->id; + $mtxt = $strmarkread; + } + if ($displaymode == FORUM_MODE_THREADED) { + $commands[] = ''.$mtxt.''; + } else { + $commands[] = ''.$mtxt.''; + } + } + } + + if ($post->parent) { // Zoom in to the parent specifically + if ($displaymode == FORUM_MODE_THREADED) { + $commands[] = ''.$strparent.''; + } else { + $commands[] = ''.$strparent.''; + } + } + + $age = time() - $post->created; + // Hack for allow to edit news posts those are not displayed yet until they are displayed + if (!$post->parent and $forum->type == 'news' and $discussion->timestart > time()) { + $age = 0; + } + $editanypost = $cm->cache->caps['mod/forum:editanypost']; + + if ($ownpost or $editanypost) { + if (($age < $CFG->maxeditingtime) or $editanypost) { + $commands[] = ''.$stredit.''; + } + } + + if ($cm->cache->caps['mod/forum:splitdiscussions'] + && $post->parent && $forum->type != 'single') { + + $commands[] = ''.$strprune.''; + } + + if (($ownpost and $age < $CFG->maxeditingtime + and $cm->cache->caps['mod/forum:deleteownpost']) + or $cm->cache->caps['mod/forum:deleteanypost']) { + $commands[] = ''.$strdelete.''; + } + + if ($reply) { + $commands[] = ''.$strreply.''; + } + + echo '
'; + echo implode(' | ', $commands); + echo '
'; + + +// Ratings + + $ratingsmenuused = false; + if (!empty($ratings) and isloggedin()) { + echo '
'; + $useratings = true; + if ($ratings->assesstimestart and $ratings->assesstimefinish) { + if ($post->created < $ratings->assesstimestart or $post->created > $ratings->assesstimefinish) { + $useratings = false; + } + } + if ($useratings) { + $mypost = ($USER->id == $post->userid); + + $canviewallratings = $cm->cache->caps['mod/forum:viewanyrating']; + + if (isset($cm->cache->ratings)) { + if (isset($cm->cache->ratings[$post->id])) { + $allratings = $cm->cache->ratings[$post->id]; + } else { + $allratings = array(); // no reatings present yet + } + } else { + $allratings = NULL; // not preloaded + } + + if (isset($cm->cache->myratings)) { + if (isset($cm->cache->myratings[$post->id])) { + $myrating = $cm->cache->myratings[$post->id]; + } else { + $myrating = FORUM_UNSET_POST_RATING; // no reatings present yet + } + } else { + $myrating = NULL; // not preloaded + } + + if ($canviewallratings and !$mypost) { + forum_print_ratings($post->id, $ratings->scale, $forum->assessed, $canviewallratings, $allratings); + if (!empty($ratings->allow)) { + echo ' '; + forum_print_rating_menu($post->id, $USER->id, $ratings->scale, $myrating); + $ratingsmenuused = true; + } + + } else if ($mypost) { + forum_print_ratings($post->id, $ratings->scale, $forum->assessed, true, $allratings); + + } else if (!empty($ratings->allow) ) { + forum_print_rating_menu($post->id, $USER->id, $ratings->scale, $myrating); + $ratingsmenuused = true; + } + } + echo '
'; + } + +// Link to post if required + + if ($link) { + echo ''; + } + + if ($footer) { + echo ''; + } + echo '
'."\n\n"; + + if ($istracked && !$CFG->forum_usermarksread && !$post_read) { + forum_tp_mark_post_read($USER->id, $post, $forum->id); + } + + return $ratingsmenuused; +} + + +/** + * This function prints the overview of a discussion in the forum listing. + * It needs some discussion information and some post information, these + * happen to be combined for efficiency in the $post parameter by the function + * that calls this one: forum_print_latest_discussions() + * + * @param object $post The post object (passed by reference for speed). + * @param object $forum The forum object. + * @param int $group Current group. + * @param string $datestring Format to use for the dates. + * @param boolean $cantrack Is tracking enabled for this forum. + * @param boolean $forumtracked Is the user tracking this forum. + * @param boolean $canviewparticipants True if user has the viewparticipants permission for this course + */ +function forum_print_discussion_header(&$post, $forum, $group=-1, $datestring="", + $cantrack=true, $forumtracked=true, $canviewparticipants=true, $modcontext=NULL) { + + global $USER, $CFG; + + static $rowcount; + static $strmarkalldread; + + if (empty($modcontext)) { + if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $forum->course)) { + error('Course Module ID was incorrect'); + } + $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id); + } + + if (!isset($rowcount)) { + $rowcount = 0; + $strmarkalldread = get_string('markalldread', 'forum'); + } else { + $rowcount = ($rowcount + 1) % 2; + } + + $post->subject = format_string($post->subject,true); + + echo "\n\n"; + echo ''; + + // Topic + echo ''; + echo ''.$post->subject.''; + echo "\n"; + + // Picture + $postuser = new object; + $postuser->id = $post->userid; + $postuser->firstname = $post->firstname; + $postuser->lastname = $post->lastname; + $postuser->imagealt = $post->imagealt; + $postuser->picture = $post->picture; + + echo ''; + print_user_picture($postuser, $forum->course); + echo "\n"; + + // User name + $fullname = fullname($post, has_capability('moodle/site:viewfullnames', $modcontext)); + echo ''; + echo ''.$fullname.''; + echo "\n"; + + // Group picture + if ($group !== -1) { // Groups are active - group is a group data object or NULL + echo ''; + if (!empty($group->picture) and empty($group->hidepicture)) { + print_group_picture($group, $forum->course, false, false, true); + } else if (isset($group->id)) { + if($canviewparticipants) { + echo ''.$group->name.''; + } else { + echo $group->name; + } + } + echo "\n"; + } + + if (has_capability('mod/forum:viewdiscussion', $modcontext)) { // Show the column with replies + echo ''; + echo ''; + echo $post->replies.''; + echo "\n"; + + if ($cantrack) { + echo ''; + if ($forumtracked) { + if ($post->unread > 0) { + echo ''; + echo ''; + echo $post->unread; + echo ''; + echo '' . + ''.$strmarkalldread.''; + echo ''; + } else { + echo ''; + echo $post->unread; + echo ''; + } + } else { + echo ''; + echo '-'; + echo ''; + } + echo "\n"; + } + } + + echo ''; + $usedate = (empty($post->timemodified)) ? $post->modified : $post->timemodified; // Just in case + $parenturl = (empty($post->lastpostid)) ? '' : '&parent='.$post->lastpostid; + $usermodified = new object(); + $usermodified->id = $post->usermodified; + $usermodified->firstname = $post->umfirstname; + $usermodified->lastname = $post->umlastname; + echo ''. + fullname($usermodified).'
'; + echo ''. + userdate($usedate, $datestring).''; + echo "\n"; + + echo "\n\n"; + +} + + +/** + * Given a post object that we already know has a long message + * this function truncates the message nicely to the first + * sane place between $CFG->forum_longpost and $CFG->forum_shortpost + */ +function forum_shorten_post($message) { + + global $CFG; + + $i = 0; + $tag = false; + $length = strlen($message); + $count = 0; + $stopzone = false; + $truncate = 0; + + for ($i=0; $i<$length; $i++) { + $char = $message[$i]; + + switch ($char) { + case "<": + $tag = true; + break; + case ">": + $tag = false; + break; + default: + if (!$tag) { + if ($stopzone) { + if ($char == ".") { + $truncate = $i+1; + break 2; + } + } + $count++; + } + break; + } + if (!$stopzone) { + if ($count > $CFG->forum_shortpost) { + $stopzone = true; + } + } + } + + if (!$truncate) { + $truncate = $i; + } + + return substr($message, 0, $truncate); +} + + +/** + * Print the multiple ratings on a post given to the current user by others. + * Forumid prevents the double lookup of the forumid in discussion to determine the aggregate type + * Scale is an array of ratings + */ +function forum_print_ratings($postid, $scale, $aggregatetype, $link=true, $ratings=null) { + + $strratings = ''; + + switch ($aggregatetype) { + case FORUM_AGGREGATE_AVG : + $agg = forum_get_ratings_mean($postid, $scale, $ratings); + $strratings = get_string("aggregateavg", "forum"); + break; + case FORUM_AGGREGATE_COUNT : + $agg = forum_get_ratings_count($postid, $scale, $ratings); + $strratings = get_string("aggregatecount", "forum"); + break; + case FORUM_AGGREGATE_MAX : + $agg = forum_get_ratings_max($postid, $scale, $ratings); + $strratings = get_string("aggregatemax", "forum"); + break; + case FORUM_AGGREGATE_MIN : + $agg = forum_get_ratings_min($postid, $scale, $ratings); + $strratings = get_string("aggregatemin", "forum"); + break; + case FORUM_AGGREGATE_SUM : + $agg = forum_get_ratings_sum($postid, $scale, $ratings); + $strratings = get_string("aggregatesum", "forum"); + break; + } + + if ($agg !== "") { + + if (empty($strratings)) { + $strratings = get_string("ratings", "forum"); + } + + echo "$strratings: "; + if ($link) { + link_to_popup_window ("/mod/forum/report.php?id=$postid", "ratings", $agg, 400, 600); + } else { + echo "$agg "; + } + } +} + + +/** + * Return the mean rating of a post given to the current user by others. + * Scale is an array of possible ratings in the scale + * Ratings is an optional simple array of actual ratings (just integers) + * Forumid is the forum id field needed - passing it avoids a double query of lookup up the discusion and then the forum id to get the aggregate type + */ +function forum_get_ratings_mean($postid, $scale, $ratings=NULL) { + + if (is_null($ratings)) { + $ratings = array(); + if ($rates = get_records("forum_ratings", "post", $postid)) { + foreach ($rates as $rate) { + $ratings[] = $rate->rating; + } + } + } + + $count = count($ratings); + + if ($count == 0 ) { + return ""; + + } else if ($count == 1) { + $rating = reset($ratings); + return $scale[$rating]; + + } else { + $total = 0; + foreach ($ratings as $rating) { + $total += $rating; + } + $mean = round( ((float)$total/(float)$count) + 0.001); // Little fudge factor so that 0.5 goes UP + + if (isset($scale[$mean])) { + return $scale[$mean]." ($count)"; + } else { + return "$mean ($count)"; // Should never happen, hopefully + } + } +} + +/** + * Return the count of the ratings of a post given to the current user by others. + * Scale is an array of possible ratings in the scale - the end of the scale is the highest or max grade + * Ratings is an optional simple array of actual ratings (just integers) + */ +function forum_get_ratings_count($postid, $scale, $ratings=NULL) { + + if (is_null($ratings)) { + $ratings = array(); + if ($rates = get_records("forum_ratings", "post", $postid)) { + foreach ($rates as $rate) { + $ratings[] = $rate->rating; + } + } + } + + $count = count($ratings); + $scalecount = count($scale)-1; //this should give us the last element of the scale aka the max grade with $scale[$scalecount] + + if ($count > $scale[$scalecount]) { //if the count exceeds the forum scale (i.e. max grade then set the score to the max grade + $count = $scale[$scalecount]; + } + return $scale[$count]; +} + +/** + * Return the max rating of a post given to the current user by others. + * Scale is an array of possible ratings in the scale + * Ratings is an optional simple array of actual ratings (just integers) + */ +function forum_get_ratings_max($postid, $scale, $ratings=NULL) { + + if (is_null($ratings)) { + $ratings = array(); + if ($rates = get_records("forum_ratings", "post", $postid)) { + foreach ($rates as $rate) { + $ratings[] = $rate->rating; + } + } + } + + $count = count($ratings); + $max = max($ratings); + + if ($count == 0 ) { + return ""; + + } else if ($count == 1) { //this works for max + $rating = reset($ratings); + return $scale[$rating]; + + } else { + + if (isset($scale[$max])) { + return $scale[$max]." ($count)"; + } else { + return "$max ($count)"; // Should never happen, hopefully + } + } +} + +/** + * Return the min rating of a post given to the current user by others. + * Scale is an array of possible ratings in the scale + * Ratings is an optional simple array of actual ratings (just integers) + */ +function forum_get_ratings_min($postid, $scale, $ratings=NULL) { + + if (is_null($ratings)) { + $ratings = array(); + if ($rates = get_records("forum_ratings", "post", $postid)) { + foreach ($rates as $rate) { + $ratings[] = $rate->rating; + } + } + } + + $count = count($ratings); + $min = min($ratings); + + if ($count == 0 ) { + return ""; + + } else if ($count == 1) { + $rating = reset($ratings); + return $scale[$rating]; //this works for min + + } else { + + if (isset($scale[$min])) { + return $scale[$min]." ($count)"; + } else { + return "$min ($count)"; // Should never happen, hopefully + } + } +} + + +/** + * Return the sum or total of ratings of a post given to the current user by others. + * Scale is an array of possible ratings in the scale + * Ratings is an optional simple array of actual ratings (just integers) + */ +function forum_get_ratings_sum($postid, $scale, $ratings=NULL) { + + if (is_null($ratings)) { + $ratings = array(); + if ($rates = get_records("forum_ratings", "post", $postid)) { + foreach ($rates as $rate) { + $ratings[] = $rate->rating; + } + } + } + + $count = count($ratings); + $scalecount = count($scale)-1; //this should give us the last element of the scale aka the max grade with $scale[$scalecount] + + if ($count == 0 ) { + return ""; + + } else if ($count == 1) { //this works for max. + $rating = reset($ratings); + return $scale[$rating]; + + } else { + $total = 0; + foreach ($ratings as $rating) { + $total += $rating; + } + if ($total > $scale[$scalecount]) { //if the total exceeds the max grade then set it to the max grade + $total = $scale[$scalecount]; + } + if (isset($scale[$total])) { + return $scale[$total]." ($count)"; + } else { + return "$total ($count)"; // Should never happen, hopefully + } + } +} + +/** + * Return a summary of post ratings given to the current user by others. + * Scale is an array of possible ratings in the scale + * Ratings is an optional simple array of actual ratings (just integers) + */ +function forum_get_ratings_summary($postid, $scale, $ratings=NULL) { + + if (is_null($ratings)) { + $ratings = array(); + if ($rates = get_records("forum_ratings", "post", $postid)) { + foreach ($rates as $rate) { + $rating[] = $rate->rating; + } + } + } + + + if (!$count = count($ratings)) { + return ""; + } + + + foreach ($scale as $key => $scaleitem) { + $sumrating[$key] = 0; + } + + foreach ($ratings as $rating) { + $sumrating[$rating]++; + } + + $summary = ""; + foreach ($scale as $key => $scaleitem) { + $summary = $sumrating[$key].$summary; + if ($key > 1) { + $summary = "/$summary"; + } + } + return $summary; +} + +/** + * Print the menu of ratings as part of a larger form. + * If the post has already been - set that value. + * Scale is an array of ratings + */ +function forum_print_rating_menu($postid, $userid, $scale, $myrating=NULL) { + + static $strrate; + + if (is_null($myrating)) { + if (!$rating = get_record("forum_ratings", "userid", $userid, "post", $postid)) { + $myrating = FORUM_UNSET_POST_RATING; + } else { + $myrating = $rating->rating; + } + } + + if (empty($strrate)) { + $strrate = get_string("rate", "forum"); + } + $scale = array(FORUM_UNSET_POST_RATING => $strrate.'...') + $scale; + choose_from_menu($scale, $postid, $myrating, ''); +} + +/** + * Print the drop down that allows the user to select how they want to have + * the discussion displayed. + * @param $id - forum id if $forumtype is 'single', + * discussion id for any other forum type + * @param $mode - forum layout mode + * @param $forumtype - optional + */ +function forum_print_mode_form($id, $mode, $forumtype='') { + if ($forumtype == 'single') { + echo '
'; + popup_form("view.php?f=$id&mode=", forum_get_layout_modes(), "mode", $mode, ""); + echo '
'; + } else { + popup_form("discuss.php?d=$id&mode=", forum_get_layout_modes(), "mode", $mode, ""); + } +} + +/** + * + */ +function forum_search_form($course, $search='') { + global $CFG; + + $output = '
'; + $output .= '
'; + $output .= '
'; + $output .= helpbutton('search', get_string('search'), 'moodle', true, false, '', true); + $output .= ''; + $output .= ''; + $output .= ''; + $output .= '
'; + $output .= '
'; + $output .= '
'; + + return $output; +} + + +/** + * + */ +function forum_set_return() { + global $CFG, $SESSION; + + if (! isset($SESSION->fromdiscussion)) { + if (!empty($_SERVER['HTTP_REFERER'])) { + $referer = $_SERVER['HTTP_REFERER']; + } else { + $referer = ""; + } + // If the referer is NOT a login screen then save it. + if (! strncasecmp("$CFG->wwwroot/login", $referer, 300)) { + $SESSION->fromdiscussion = $_SERVER["HTTP_REFERER"]; + } + } +} + + +/** + * + */ +function forum_go_back_to($default) { + global $SESSION; + + if (!empty($SESSION->fromdiscussion)) { + $returnto = $SESSION->fromdiscussion; + unset($SESSION->fromdiscussion); + return $returnto; + } else { + return $default; + } +} + +/** + * Creates a directory file name, suitable for make_upload_directory() + */ +function forum_file_area_name($post) { + global $CFG; + + if (!isset($post->forum) or !isset($post->course)) { + debugging('missing forum or course', DEBUG_DEVELOPER); + if (!$discussion = get_record('forum_discussions', 'id', $post->discussion)) { + return false; + } + if (!$forum = get_record('forum', 'id', $discussion->forum)) { + return false; + } + $forumid = $forum->id; + $courseid = $forum->course; + } else { + $forumid = $post->forum; + $courseid = $post->course; + } + + return "$courseid/$CFG->moddata/forum/$forumid/$post->id"; +} + +/** + * + */ +function forum_file_area($post) { + return make_upload_directory( forum_file_area_name($post) ); +} + +/** + * + */ +function forum_delete_old_attachments($post, $exception="") { + +/** + * Deletes all the user files in the attachments area for a post + * EXCEPT for any file named $exception + */ + if ($basedir = forum_file_area($post)) { + if ($files = get_directory_list($basedir)) { + foreach ($files as $file) { + if ($file != $exception) { + unlink("$basedir/$file"); + notify("Existing file '$file' has been deleted!"); + } + } + } + if (!$exception) { // Delete directory as well, if empty + rmdir("$basedir"); + } + } +} + +/** + * Given a discussion object that is being moved to forumid, + * this function checks all posts in that discussion + * for attachments, and if any are found, these are + * moved to the new forum directory. + */ +function forum_move_attachments($discussion, $forumid) { + + global $CFG; + + require_once($CFG->dirroot.'/lib/uploadlib.php'); + + $return = true; + + if ($posts = get_records_select("forum_posts", "discussion = '$discussion->id' AND attachment <> ''")) { + foreach ($posts as $oldpost) { + $oldpost->course = $discussion->course; + $oldpost->forum = $discussion->forum; + $oldpostdir = "$CFG->dataroot/".forum_file_area_name($oldpost); + if (is_dir($oldpostdir)) { + $newpost = $oldpost; + $newpost->forum = $forumid; + $newpostdir = forum_file_area_name($newpost); + // take off the last directory because otherwise we're renaming to a directory that already exists + // and this is unhappy in certain situations, eg over an nfs mount and potentially on windows too. + make_upload_directory(substr($newpostdir,0,strrpos($newpostdir,'/'))); + $newpostdir = $CFG->dataroot.'/'.forum_file_area_name($newpost); + $files = get_directory_list($oldpostdir); // get it before we rename it. + if (! @rename($oldpostdir, $newpostdir)) { + $return = false; + } + foreach ($files as $file) { + clam_change_log($oldpostdir.'/'.$file,$newpostdir.'/'.$file); + } + } + } + } + return $return; +} + +/** + * if return=html, then return a html string. + * if return=text, then return a text-only string. + * otherwise, print HTML for non-images, and return image HTML + */ +function forum_print_attachments($post, $return=NULL) { + + global $CFG; + + $filearea = forum_file_area_name($post); + + $imagereturn = ""; + $output = ""; + + if ($basedir = forum_file_area($post)) { + if ($files = get_directory_list($basedir)) { + $strattachment = get_string("attachment", "forum"); + foreach ($files as $file) { + $icon = mimeinfo("icon", $file); + $type = mimeinfo("type", $file); + $ffurl = get_file_url("$filearea/$file"); + $image = "pixpath/f/$icon\" class=\"icon\" alt=\"\" />"; + + if ($return == "html") { + $output .= "$image "; + $output .= "$file
"; + + } else if ($return == "text") { + $output .= "$strattachment $file:\n$ffurl\n"; + + } else { + if (in_array($type, array('image/gif', 'image/jpeg', 'image/png'))) { // Image attachments don't get printed as links + $imagereturn .= "
\"\""; + } else { + echo "$image "; + echo filter_text("$file
"); + } + } + } + } + } + + if ($return) { + return $output; + } + + return $imagereturn; +} +/** + * If successful, this function returns the name of the file + * @param $post is a full post record, including course and forum + * @param $newfile is a full upload array from $_FILES + * @param $message is a string to hold the messages. + */ + +/** + * + */ +function forum_add_attachment($post, $inputname,&$message) { + + global $CFG; + + if (!$forum = get_record("forum", "id", $post->forum)) { + return ""; + } + + if (!$course = get_record("course", "id", $forum->course)) { + return ""; + } + + require_once($CFG->dirroot.'/lib/uploadlib.php'); + $um = new upload_manager($inputname,true,false,$course,false,$forum->maxbytes,true,true); + $dir = forum_file_area_name($post); + if ($um->process_file_uploads($dir)) { + $message .= $um->get_errors(); + return $um->get_new_filename(); + } + $message .= $um->get_errors(); + return null; +} + +/** + * + */ +function forum_add_new_post($post,&$message) { + + global $USER, $CFG; + + $discussion = get_record('forum_discussions', 'id', $post->discussion); + $forum = get_record('forum', 'id', $discussion->forum); + + $post->created = $post->modified = time(); + $post->mailed = "0"; + $post->userid = $USER->id; + $post->attachment = ""; + $post->forum = $forum->id; // speedup + $post->course = $forum->course; // speedup + + if (! $post->id = insert_record("forum_posts", $post)) { + return false; + } + + if ($post->attachment = forum_add_attachment($post, 'attachment',$message)) { + set_field("forum_posts", "attachment", $post->attachment, "id", $post->id); + } + + // Update discussion modified date + set_field("forum_discussions", "timemodified", $post->modified, "id", $post->discussion); + set_field("forum_discussions", "usermodified", $post->userid, "id", $post->discussion); + + if (forum_tp_can_track_forums($forum) && forum_tp_is_tracked($forum)) { + forum_tp_mark_post_read($post->userid, $post, $post->forum); + } + + return $post->id; +} + +/** + * + */ +function forum_update_post($post,&$message) { + + global $USER, $CFG; + + $forum = get_record('forum', 'id', $post->forum); + + $post->modified = time(); + + $updatediscussion = new object(); + $updatediscussion->id = $post->discussion; + $updatediscussion->timemodified = $post->modified; // last modified tracking + $updatediscussion->usermodified = $post->userid; // last modified tracking + + if (!$post->parent) { // Post is a discussion starter - update discussion title and times too + $updatediscussion->name = $post->subject; + $updatediscussion->timestart = $post->timestart; + $updatediscussion->timeend = $post->timeend; + } + + if (!update_record('forum_discussions', $updatediscussion)) { + return false; + } + + if ($newfilename = forum_add_attachment($post, 'attachment',$message)) { + $post->attachment = $newfilename; + } else { + unset($post->attachment); + } + + if (forum_tp_can_track_forums($forum) && forum_tp_is_tracked($forum)) { + forum_tp_mark_post_read($post->userid, $post, $post->forum); + } + + return update_record('forum_posts', $post); +} + +/** + * Given an object containing all the necessary data, + * create a new discussion and return the id + */ +function forum_add_discussion($discussion,&$message) { + + global $USER, $CFG; + + $timenow = time(); + + // The first post is stored as a real post, and linked + // to from the discuss entry. + + $forum = get_record('forum', 'id', $discussion->forum); + + $post = new object(); + $post->discussion = 0; + $post->parent = 0; + $post->userid = $USER->id; + $post->created = $timenow; + $post->modified = $timenow; + $post->mailed = 0; + $post->subject = $discussion->name; + $post->message = $discussion->intro; + $post->attachment = ""; + $post->forum = $forum->id; // speedup + $post->course = $forum->course; // speedup + $post->format = $discussion->format; + $post->mailnow = $discussion->mailnow; + + if (! $post->id = insert_record("forum_posts", $post) ) { + return 0; + } + + if ($post->attachment = forum_add_attachment($post, 'attachment',$message)) { + set_field("forum_posts", "attachment", $post->attachment, "id", $post->id); //ignore errors + } + + // Now do the main entry for the discussion, + // linking to this first post + + $discussion->firstpost = $post->id; + $discussion->timemodified = $timenow; + $discussion->usermodified = $post->userid; + $discussion->userid = $USER->id; + + if (! $post->discussion = insert_record("forum_discussions", $discussion) ) { + delete_records("forum_posts", "id", $post->id); + return 0; + } + + // Finally, set the pointer on the post. + if (! set_field("forum_posts", "discussion", $post->discussion, "id", $post->id)) { + delete_records("forum_posts", "id", $post->id); + delete_records("forum_discussions", "id", $post->discussion); + return 0; + } + + if (forum_tp_can_track_forums($forum) && forum_tp_is_tracked($forum)) { + forum_tp_mark_post_read($post->userid, $post, $post->forum); + } + + return $post->discussion; +} + + +/** + * + */ +function forum_delete_discussion($discussion, $fulldelete=false) { +// $discussion is a discussion record object + + $result = true; + + if ($posts = get_records("forum_posts", "discussion", $discussion->id)) { + foreach ($posts as $post) { + $post->course = $discussion->course; + $post->forum = $discussion->forum; + if (! delete_records("forum_ratings", "post", "$post->id")) { + $result = false; + } + if (! forum_delete_post($post, $fulldelete)) { + $result = false; + } + } + } + + forum_tp_delete_read_records(-1, -1, $discussion->id); + + if (! delete_records("forum_discussions", "id", "$discussion->id")) { + $result = false; + } + + return $result; +} + + +/** + * + */ +function forum_delete_post($post, $children=false) { + if ($childposts = get_records('forum_posts', 'parent', $post->id)) { + if ($children) { + foreach ($childposts as $childpost) { + forum_delete_post($childpost, true); + } + } else { + return false; + } + } + if (delete_records("forum_posts", "id", $post->id)) { + delete_records("forum_ratings", "post", $post->id); // Just in case + + forum_tp_delete_read_records(-1, $post->id); + + if ($post->attachment) { + $discussion = get_record("forum_discussions", "id", $post->discussion); + $post->course = $discussion->course; + $post->forum = $discussion->forum; + forum_delete_old_attachments($post); + } + + // Just in case we are deleting the last post + forum_discussion_update_last_post($post->discussion); + + return true; + } + return false; +} + +/** + * + */ +function forum_count_replies($post, $children=true) { + $count = 0; + + if ($children) { + if ($childposts = get_records('forum_posts', 'parent', $post->id)) { + foreach ($childposts as $childpost) { + $count ++; // For this child + $count += forum_count_replies($childpost, true); + } + } + } else { + $count += count_records('forum_posts', 'parent', $post->id); + } + + return $count; +} + + +/** + * + */ +function forum_forcesubscribe($forumid, $value=1) { + return set_field("forum", "forcesubscribe", $value, "id", $forumid); +} + +/** + * + */ +function forum_is_forcesubscribed($forum) { + if (isset($forum->forcesubscribe)) { // then we use that + return ($forum->forcesubscribe == FORUM_FORCESUBSCRIBE); + } else { // Check the database + return (get_field('forum', 'forcesubscribe', 'id', $forum) == FORUM_FORCESUBSCRIBE); + } +} + +/** + * + */ +function forum_is_subscribed($userid, $forum) { + if (is_numeric($forum)) { + $forum = get_record('forum', 'id', $forum); + } + if (forum_is_forcesubscribed($forum)) { + return true; + } + return record_exists("forum_subscriptions", "userid", $userid, "forum", $forum->id); +} + +function forum_get_subscribed_forums($course) { + global $USER, $CFG; + $sql = "SELECT f.id + FROM {$CFG->prefix}forum f + LEFT JOIN {$CFG->prefix}forum_subscriptions fs ON (fs.forum = f.id AND fs.userid = $USER->id) + WHERE f.forcesubscribe <> ".FORUM_DISALLOWSUBSCRIBE." + AND (f.forcesubscribe = ".FORUM_FORCESUBSCRIBE." OR fs.id IS NOT NULL)"; + if ($subscribed = get_records_sql($sql)) { + foreach ($subscribed as $s) { + $subscribed[$s->id] = $s->id; + } + return $subscribed; + } else { + return array(); + } +} + +/** + * Adds user to the subscriber list + */ +function forum_subscribe($userid, $forumid) { + + if (record_exists("forum_subscriptions", "userid", $userid, "forum", $forumid)) { + return true; + } + + $sub = new object(); + $sub->userid = $userid; + $sub->forum = $forumid; + + return insert_record("forum_subscriptions", $sub); +} + +/** + * Removes user from the subscriber list + */ +function forum_unsubscribe($userid, $forumid) { + return delete_records("forum_subscriptions", "userid", $userid, "forum", $forumid); +} + +/** + * Given a new post, subscribes or unsubscribes as appropriate. + * Returns some text which describes what happened. + */ +function forum_post_subscription($post) { + + global $USER; + + $subscribed=forum_is_subscribed($USER->id, $post->forum); + if ((isset($post->subscribe) && $post->subscribe && $subscribed) + || (!$post->subscribe && !$subscribed)) { + return ""; + } + + if (!$forum = get_record("forum", "id", $post->forum)) { + return ""; + } + + $info = new object(); + $info->name = fullname($USER); + $info->forum = $forum->name; + + if (!empty($post->subscribe)) { + forum_subscribe($USER->id, $post->forum); + return "

".get_string("nowsubscribed", "forum", $info)."

"; + } + + forum_unsubscribe($USER->id, $post->forum); + return "

".get_string("nownotsubscribed", "forum", $info)."

"; +} + +/** + * Generate and return the subscribe or unsubscribe link for a forum. + * @param object $forum the forum. Fields used are $forum->id and $forum->forcesubscribe. + * @param object $context the context object for this forum. + * @param array $messages text used for the link in its various states + * (subscribed, unsubscribed, forcesubscribed or cantsubscribe). + * Any strings not passed in are taken from the $defaultmessages array + * at the top of the function. + * @param + */ +function forum_get_subscribe_link($forum, $context, $messages = array(), $cantaccessagroup = false, $fakelink=true, $backtoindex=false, $subscribed_forums=null) { + global $CFG, $USER; + $defaultmessages = array( + 'subscribed' => get_string('unsubscribe', 'forum'), + 'unsubscribed' => get_string('subscribe', 'forum'), + 'cantaccessgroup' => get_string('no'), + 'forcesubscribed' => get_string('everyoneissubscribed', 'forum'), + 'cantsubscribe' => get_string('disallowsubscribe','forum') + ); + $messages = $messages + $defaultmessages; + + if (forum_is_forcesubscribed($forum)) { + return $messages['forcesubscribed']; + } else if ($forum->forcesubscribe == FORUM_DISALLOWSUBSCRIBE && !has_capability('mod/forum:managesubscriptions', $context)) { + return $messages['cantsubscribe']; + } else if ($cantaccessagroup) { + return $messages['cantaccessgroup']; + } else { + if (is_null($subscribed_forums)) { + $subscribed = forum_is_subscribed($USER->id, $forum); + } else { + $subscribed = !empty($subscribed_forums[$forum->id]); + } + if ($subscribed) { + $linktext = $messages['subscribed']; + $linktitle = get_string('subscribestop', 'forum'); + } else { + $linktext = $messages['unsubscribed']; + $linktitle = get_string('subscribestart', 'forum'); + } + + $options = array(); + if ($backtoindex) { + $backtoindexlink = '&backtoindex=1'; + $options['backtoindex'] = 1; + } else { + $backtoindexlink = ''; + } + $link = ''; + + if ($fakelink) { + $link .= ''; + // use