Merge branch 'MDL-26120' of https://github.com/jacks92/moodle
[moodle.git] / mod / forum / lib.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * @package    mod
19  * @subpackage forum
20  * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
21  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22  */
24 defined('MOODLE_INTERNAL') || die();
26 /** Include required files */
27 require_once($CFG->libdir.'/filelib.php');
28 require_once($CFG->libdir.'/eventslib.php');
29 require_once($CFG->dirroot.'/user/selector/lib.php');
30 require_once($CFG->dirroot.'/mod/forum/post_form.php');
32 /// CONSTANTS ///////////////////////////////////////////////////////////
34 define('FORUM_MODE_FLATOLDEST', 1);
35 define('FORUM_MODE_FLATNEWEST', -1);
36 define('FORUM_MODE_THREADED', 2);
37 define('FORUM_MODE_NESTED', 3);
39 define('FORUM_CHOOSESUBSCRIBE', 0);
40 define('FORUM_FORCESUBSCRIBE', 1);
41 define('FORUM_INITIALSUBSCRIBE', 2);
42 define('FORUM_DISALLOWSUBSCRIBE',3);
44 define('FORUM_TRACKING_OFF', 0);
45 define('FORUM_TRACKING_OPTIONAL', 1);
46 define('FORUM_TRACKING_ON', 2);
48 define('FORUM_MAILED_PENDING', 0);
49 define('FORUM_MAILED_SUCCESS', 1);
50 define('FORUM_MAILED_ERROR', 2);
52 if (!defined('FORUM_CRON_USER_CACHE')) {
53     /** Defines how many full user records are cached in forum cron. */
54     define('FORUM_CRON_USER_CACHE', 5000);
55 }
57 /// STANDARD FUNCTIONS ///////////////////////////////////////////////////////////
59 /**
60  * Given an object containing all the necessary data,
61  * (defined by the form in mod_form.php) this function
62  * will create a new instance and return the id number
63  * of the new instance.
64  *
65  * @param stdClass $forum add forum instance
66  * @param mod_forum_mod_form $mform
67  * @return int intance id
68  */
69 function forum_add_instance($forum, $mform = null) {
70     global $CFG, $DB;
72     $forum->timemodified = time();
74     if (empty($forum->assessed)) {
75         $forum->assessed = 0;
76     }
78     if (empty($forum->ratingtime) or empty($forum->assessed)) {
79         $forum->assesstimestart  = 0;
80         $forum->assesstimefinish = 0;
81     }
83     $forum->id = $DB->insert_record('forum', $forum);
84     $modcontext = context_module::instance($forum->coursemodule);
86     if ($forum->type == 'single') {  // Create related discussion.
87         $discussion = new stdClass();
88         $discussion->course        = $forum->course;
89         $discussion->forum         = $forum->id;
90         $discussion->name          = $forum->name;
91         $discussion->assessed      = $forum->assessed;
92         $discussion->message       = $forum->intro;
93         $discussion->messageformat = $forum->introformat;
94         $discussion->messagetrust  = trusttext_trusted(context_course::instance($forum->course));
95         $discussion->mailnow       = false;
96         $discussion->groupid       = -1;
98         $message = '';
100         $discussion->id = forum_add_discussion($discussion, null, $message);
102         if ($mform and $draftid = file_get_submitted_draft_itemid('introeditor')) {
103             // Ugly hack - we need to copy the files somehow.
104             $discussion = $DB->get_record('forum_discussions', array('id'=>$discussion->id), '*', MUST_EXIST);
105             $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost), '*', MUST_EXIST);
107             $options = array('subdirs'=>true); // Use the same options as intro field!
108             $post->message = file_save_draft_area_files($draftid, $modcontext->id, 'mod_forum', 'post', $post->id, $options, $post->message);
109             $DB->set_field('forum_posts', 'message', $post->message, array('id'=>$post->id));
110         }
111     }
113     if ($forum->forcesubscribe == FORUM_INITIALSUBSCRIBE) {
114         $users = forum_get_potential_subscribers($modcontext, 0, 'u.id, u.email');
115         foreach ($users as $user) {
116             forum_subscribe($user->id, $forum->id);
117         }
118     }
120     forum_grade_item_update($forum);
122     return $forum->id;
126 /**
127  * Given an object containing all the necessary data,
128  * (defined by the form in mod_form.php) this function
129  * will update an existing instance with new data.
130  *
131  * @global object
132  * @param object $forum forum instance (with magic quotes)
133  * @return bool success
134  */
135 function forum_update_instance($forum, $mform) {
136     global $DB, $OUTPUT, $USER;
138     $forum->timemodified = time();
139     $forum->id           = $forum->instance;
141     if (empty($forum->assessed)) {
142         $forum->assessed = 0;
143     }
145     if (empty($forum->ratingtime) or empty($forum->assessed)) {
146         $forum->assesstimestart  = 0;
147         $forum->assesstimefinish = 0;
148     }
150     $oldforum = $DB->get_record('forum', array('id'=>$forum->id));
152     // MDL-3942 - if the aggregation type or scale (i.e. max grade) changes then recalculate the grades for the entire forum
153     // if  scale changes - do we need to recheck the ratings, if ratings higher than scale how do we want to respond?
154     // 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
155     if (($oldforum->assessed<>$forum->assessed) or ($oldforum->scale<>$forum->scale)) {
156         forum_update_grades($forum); // recalculate grades for the forum
157     }
159     if ($forum->type == 'single') {  // Update related discussion and post.
160         $discussions = $DB->get_records('forum_discussions', array('forum'=>$forum->id), 'timemodified ASC');
161         if (!empty($discussions)) {
162             if (count($discussions) > 1) {
163                 echo $OUTPUT->notification(get_string('warnformorepost', 'forum'));
164             }
165             $discussion = array_pop($discussions);
166         } else {
167             // try to recover by creating initial discussion - MDL-16262
168             $discussion = new stdClass();
169             $discussion->course          = $forum->course;
170             $discussion->forum           = $forum->id;
171             $discussion->name            = $forum->name;
172             $discussion->assessed        = $forum->assessed;
173             $discussion->message         = $forum->intro;
174             $discussion->messageformat   = $forum->introformat;
175             $discussion->messagetrust    = true;
176             $discussion->mailnow         = false;
177             $discussion->groupid         = -1;
179             $message = '';
181             forum_add_discussion($discussion, null, $message);
183             if (! $discussion = $DB->get_record('forum_discussions', array('forum'=>$forum->id))) {
184                 print_error('cannotadd', 'forum');
185             }
186         }
187         if (! $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost))) {
188             print_error('cannotfindfirstpost', 'forum');
189         }
191         $cm         = get_coursemodule_from_instance('forum', $forum->id);
192         $modcontext = context_module::instance($cm->id, MUST_EXIST);
194         $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost), '*', MUST_EXIST);
195         $post->subject       = $forum->name;
196         $post->message       = $forum->intro;
197         $post->messageformat = $forum->introformat;
198         $post->messagetrust  = trusttext_trusted($modcontext);
199         $post->modified      = $forum->timemodified;
200         $post->userid        = $USER->id;    // MDL-18599, so that current teacher can take ownership of activities.
202         if ($mform and $draftid = file_get_submitted_draft_itemid('introeditor')) {
203             // Ugly hack - we need to copy the files somehow.
204             $options = array('subdirs'=>true); // Use the same options as intro field!
205             $post->message = file_save_draft_area_files($draftid, $modcontext->id, 'mod_forum', 'post', $post->id, $options, $post->message);
206         }
208         $DB->update_record('forum_posts', $post);
209         $discussion->name = $forum->name;
210         $DB->update_record('forum_discussions', $discussion);
211     }
213     $DB->update_record('forum', $forum);
215     $modcontext = context_module::instance($forum->coursemodule);
216     if (($forum->forcesubscribe == FORUM_INITIALSUBSCRIBE) && ($oldforum->forcesubscribe <> $forum->forcesubscribe)) {
217         $users = forum_get_potential_subscribers($modcontext, 0, 'u.id, u.email', '');
218         foreach ($users as $user) {
219             forum_subscribe($user->id, $forum->id);
220         }
221     }
223     forum_grade_item_update($forum);
225     return true;
229 /**
230  * Given an ID of an instance of this module,
231  * this function will permanently delete the instance
232  * and any data that depends on it.
233  *
234  * @global object
235  * @param int $id forum instance id
236  * @return bool success
237  */
238 function forum_delete_instance($id) {
239     global $DB;
241     if (!$forum = $DB->get_record('forum', array('id'=>$id))) {
242         return false;
243     }
244     if (!$cm = get_coursemodule_from_instance('forum', $forum->id)) {
245         return false;
246     }
247     if (!$course = $DB->get_record('course', array('id'=>$cm->course))) {
248         return false;
249     }
251     $context = context_module::instance($cm->id);
253     // now get rid of all files
254     $fs = get_file_storage();
255     $fs->delete_area_files($context->id);
257     $result = true;
259     if ($discussions = $DB->get_records('forum_discussions', array('forum'=>$forum->id))) {
260         foreach ($discussions as $discussion) {
261             if (!forum_delete_discussion($discussion, true, $course, $cm, $forum)) {
262                 $result = false;
263             }
264         }
265     }
267     if (!$DB->delete_records('forum_subscriptions', array('forum'=>$forum->id))) {
268         $result = false;
269     }
271     forum_tp_delete_read_records(-1, -1, -1, $forum->id);
273     if (!$DB->delete_records('forum', array('id'=>$forum->id))) {
274         $result = false;
275     }
277     forum_grade_item_delete($forum);
279     return $result;
283 /**
284  * Indicates API features that the forum supports.
285  *
286  * @uses FEATURE_GROUPS
287  * @uses FEATURE_GROUPINGS
288  * @uses FEATURE_GROUPMEMBERSONLY
289  * @uses FEATURE_MOD_INTRO
290  * @uses FEATURE_COMPLETION_TRACKS_VIEWS
291  * @uses FEATURE_COMPLETION_HAS_RULES
292  * @uses FEATURE_GRADE_HAS_GRADE
293  * @uses FEATURE_GRADE_OUTCOMES
294  * @param string $feature
295  * @return mixed True if yes (some features may use other values)
296  */
297 function forum_supports($feature) {
298     switch($feature) {
299         case FEATURE_GROUPS:                  return true;
300         case FEATURE_GROUPINGS:               return true;
301         case FEATURE_GROUPMEMBERSONLY:        return true;
302         case FEATURE_MOD_INTRO:               return true;
303         case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
304         case FEATURE_COMPLETION_HAS_RULES:    return true;
305         case FEATURE_GRADE_HAS_GRADE:         return true;
306         case FEATURE_GRADE_OUTCOMES:          return true;
307         case FEATURE_RATE:                    return true;
308         case FEATURE_BACKUP_MOODLE2:          return true;
309         case FEATURE_SHOW_DESCRIPTION:        return true;
310         case FEATURE_PLAGIARISM:              return true;
312         default: return null;
313     }
317 /**
318  * Obtains the automatic completion state for this forum based on any conditions
319  * in forum settings.
320  *
321  * @global object
322  * @global object
323  * @param object $course Course
324  * @param object $cm Course-module
325  * @param int $userid User ID
326  * @param bool $type Type of comparison (or/and; can be used as return value if no conditions)
327  * @return bool True if completed, false if not. (If no conditions, then return
328  *   value depends on comparison type)
329  */
330 function forum_get_completion_state($course,$cm,$userid,$type) {
331     global $CFG,$DB;
333     // Get forum details
334     if (!($forum=$DB->get_record('forum',array('id'=>$cm->instance)))) {
335         throw new Exception("Can't find forum {$cm->instance}");
336     }
338     $result=$type; // Default return value
340     $postcountparams=array('userid'=>$userid,'forumid'=>$forum->id);
341     $postcountsql="
342 SELECT
343     COUNT(1)
344 FROM
345     {forum_posts} fp
346     INNER JOIN {forum_discussions} fd ON fp.discussion=fd.id
347 WHERE
348     fp.userid=:userid AND fd.forum=:forumid";
350     if ($forum->completiondiscussions) {
351         $value = $forum->completiondiscussions <=
352                  $DB->count_records('forum_discussions',array('forum'=>$forum->id,'userid'=>$userid));
353         if ($type == COMPLETION_AND) {
354             $result = $result && $value;
355         } else {
356             $result = $result || $value;
357         }
358     }
359     if ($forum->completionreplies) {
360         $value = $forum->completionreplies <=
361                  $DB->get_field_sql( $postcountsql.' AND fp.parent<>0',$postcountparams);
362         if ($type==COMPLETION_AND) {
363             $result = $result && $value;
364         } else {
365             $result = $result || $value;
366         }
367     }
368     if ($forum->completionposts) {
369         $value = $forum->completionposts <= $DB->get_field_sql($postcountsql,$postcountparams);
370         if ($type == COMPLETION_AND) {
371             $result = $result && $value;
372         } else {
373             $result = $result || $value;
374         }
375     }
377     return $result;
380 /**
381  * Create a message-id string to use in the custom headers of forum notification emails
382  *
383  * message-id is used by email clients to identify emails and to nest conversations
384  *
385  * @param int $postid The ID of the forum post we are notifying the user about
386  * @param int $usertoid The ID of the user being notified
387  * @param string $hostname The server's hostname
388  * @return string A unique message-id
389  */
390 function forum_get_email_message_id($postid, $usertoid, $hostname) {
391     return '<'.hash('sha256',$postid.'to'.$usertoid).'@'.$hostname.'>';
394 /**
395  * Removes properties from user record that are not necessary
396  * for sending post notifications.
397  * @param stdClass $user
398  * @return void, $user parameter is modified
399  */
400 function forum_cron_minimise_user_record(stdClass $user) {
402     // We store large amount of users in one huge array,
403     // make sure we do not store info there we do not actually need
404     // in mail generation code or messaging.
406     unset($user->institution);
407     unset($user->department);
408     unset($user->address);
409     unset($user->city);
410     unset($user->url);
411     unset($user->currentlogin);
412     unset($user->description);
413     unset($user->descriptionformat);
416 /**
417  * Function to be run periodically according to the moodle cron
418  * Finds all posts that have yet to be mailed out, and mails them
419  * out to all subscribers
420  *
421  * @global object
422  * @global object
423  * @global object
424  * @uses CONTEXT_MODULE
425  * @uses CONTEXT_COURSE
426  * @uses SITEID
427  * @uses FORMAT_PLAIN
428  * @return void
429  */
430 function forum_cron() {
431     global $CFG, $USER, $DB;
433     $site = get_site();
435     // All users that are subscribed to any post that needs sending,
436     // please increase $CFG->extramemorylimit on large sites that
437     // send notifications to a large number of users.
438     $users = array();
439     $userscount = 0; // Cached user counter - count($users) in PHP is horribly slow!!!
441     // status arrays
442     $mailcount  = array();
443     $errorcount = array();
445     // caches
446     $discussions     = array();
447     $forums          = array();
448     $courses         = array();
449     $coursemodules   = array();
450     $subscribedusers = array();
453     // Posts older than 2 days will not be mailed.  This is to avoid the problem where
454     // cron has not been running for a long time, and then suddenly people are flooded
455     // with mail from the past few weeks or months
456     $timenow   = time();
457     $endtime   = $timenow - $CFG->maxeditingtime;
458     $starttime = $endtime - 48 * 3600;   // Two days earlier
460     if ($posts = forum_get_unmailed_posts($starttime, $endtime, $timenow)) {
461         // Mark them all now as being mailed.  It's unlikely but possible there
462         // might be an error later so that a post is NOT actually mailed out,
463         // but since mail isn't crucial, we can accept this risk.  Doing it now
464         // prevents the risk of duplicated mails, which is a worse problem.
466         if (!forum_mark_old_posts_as_mailed($endtime)) {
467             mtrace('Errors occurred while trying to mark some posts as being mailed.');
468             return false;  // Don't continue trying to mail them, in case we are in a cron loop
469         }
471         // checking post validity, and adding users to loop through later
472         foreach ($posts as $pid => $post) {
474             $discussionid = $post->discussion;
475             if (!isset($discussions[$discussionid])) {
476                 if ($discussion = $DB->get_record('forum_discussions', array('id'=> $post->discussion))) {
477                     $discussions[$discussionid] = $discussion;
478                 } else {
479                     mtrace('Could not find discussion '.$discussionid);
480                     unset($posts[$pid]);
481                     continue;
482                 }
483             }
484             $forumid = $discussions[$discussionid]->forum;
485             if (!isset($forums[$forumid])) {
486                 if ($forum = $DB->get_record('forum', array('id' => $forumid))) {
487                     $forums[$forumid] = $forum;
488                 } else {
489                     mtrace('Could not find forum '.$forumid);
490                     unset($posts[$pid]);
491                     continue;
492                 }
493             }
494             $courseid = $forums[$forumid]->course;
495             if (!isset($courses[$courseid])) {
496                 if ($course = $DB->get_record('course', array('id' => $courseid))) {
497                     $courses[$courseid] = $course;
498                 } else {
499                     mtrace('Could not find course '.$courseid);
500                     unset($posts[$pid]);
501                     continue;
502                 }
503             }
504             if (!isset($coursemodules[$forumid])) {
505                 if ($cm = get_coursemodule_from_instance('forum', $forumid, $courseid)) {
506                     $coursemodules[$forumid] = $cm;
507                 } else {
508                     mtrace('Could not find course module for forum '.$forumid);
509                     unset($posts[$pid]);
510                     continue;
511                 }
512             }
515             // caching subscribed users of each forum
516             if (!isset($subscribedusers[$forumid])) {
517                 $modcontext = context_module::instance($coursemodules[$forumid]->id);
518                 if ($subusers = forum_subscribed_users($courses[$courseid], $forums[$forumid], 0, $modcontext, "u.*")) {
519                     foreach ($subusers as $postuser) {
520                         // this user is subscribed to this forum
521                         $subscribedusers[$forumid][$postuser->id] = $postuser->id;
522                         $userscount++;
523                         if ($userscount > FORUM_CRON_USER_CACHE) {
524                             // Store minimal user info.
525                             $minuser = new stdClass();
526                             $minuser->id = $postuser->id;
527                             $users[$postuser->id] = $minuser;
528                         } else {
529                             // Cache full user record.
530                             forum_cron_minimise_user_record($postuser);
531                             $users[$postuser->id] = $postuser;
532                         }
533                     }
534                     // Release memory.
535                     unset($subusers);
536                     unset($postuser);
537                 }
538             }
540             $mailcount[$pid] = 0;
541             $errorcount[$pid] = 0;
542         }
543     }
545     if ($users && $posts) {
547         $urlinfo = parse_url($CFG->wwwroot);
548         $hostname = $urlinfo['host'];
550         foreach ($users as $userto) {
552             @set_time_limit(120); // terminate if processing of any account takes longer than 2 minutes
554             mtrace('Processing user '.$userto->id);
556             // Init user caches - we keep the cache for one cycle only,
557             // otherwise it could consume too much memory.
558             if (isset($userto->username)) {
559                 $userto = clone($userto);
560             } else {
561                 $userto = $DB->get_record('user', array('id' => $userto->id));
562                 forum_cron_minimise_user_record($userto);
563             }
564             $userto->viewfullnames = array();
565             $userto->canpost       = array();
566             $userto->markposts     = array();
568             // set this so that the capabilities are cached, and environment matches receiving user
569             cron_setup_user($userto);
571             // reset the caches
572             foreach ($coursemodules as $forumid=>$unused) {
573                 $coursemodules[$forumid]->cache       = new stdClass();
574                 $coursemodules[$forumid]->cache->caps = array();
575                 unset($coursemodules[$forumid]->uservisible);
576             }
578             foreach ($posts as $pid => $post) {
580                 // Set up the environment for the post, discussion, forum, course
581                 $discussion = $discussions[$post->discussion];
582                 $forum      = $forums[$discussion->forum];
583                 $course     = $courses[$forum->course];
584                 $cm         =& $coursemodules[$forum->id];
586                 // Do some checks  to see if we can bail out now
587                 // Only active enrolled users are in the list of subscribers
588                 if (!isset($subscribedusers[$forum->id][$userto->id])) {
589                     continue; // user does not subscribe to this forum
590                 }
592                 // Don't send email if the forum is Q&A and the user has not posted
593                 // Initial topics are still mailed
594                 if ($forum->type == 'qanda' && !forum_get_user_posted_time($discussion->id, $userto->id) && $pid != $discussion->firstpost) {
595                     mtrace('Did not email '.$userto->id.' because user has not posted in discussion');
596                     continue;
597                 }
599                 // Get info about the sending user
600                 if (array_key_exists($post->userid, $users)) { // we might know him/her already
601                     $userfrom = $users[$post->userid];
602                     if (!isset($userfrom->idnumber)) {
603                         // Minimalised user info, fetch full record.
604                         $userfrom = $DB->get_record('user', array('id' => $userfrom->id));
605                         forum_cron_minimise_user_record($userfrom);
606                     }
608                 } else if ($userfrom = $DB->get_record('user', array('id' => $post->userid))) {
609                     forum_cron_minimise_user_record($userfrom);
610                     // Fetch only once if possible, we can add it to user list, it will be skipped anyway.
611                     if ($userscount <= FORUM_CRON_USER_CACHE) {
612                         $userscount++;
613                         $users[$userfrom->id] = $userfrom;
614                     }
616                 } else {
617                     mtrace('Could not find user '.$post->userid);
618                     continue;
619                 }
621                 //if we want to check that userto and userfrom are not the same person this is probably the spot to do it
623                 // setup global $COURSE properly - needed for roles and languages
624                 cron_setup_user($userto, $course);
626                 // Fill caches
627                 if (!isset($userto->viewfullnames[$forum->id])) {
628                     $modcontext = context_module::instance($cm->id);
629                     $userto->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $modcontext);
630                 }
631                 if (!isset($userto->canpost[$discussion->id])) {
632                     $modcontext = context_module::instance($cm->id);
633                     $userto->canpost[$discussion->id] = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
634                 }
635                 if (!isset($userfrom->groups[$forum->id])) {
636                     if (!isset($userfrom->groups)) {
637                         $userfrom->groups = array();
638                         if (isset($users[$userfrom->id])) {
639                             $users[$userfrom->id]->groups = array();
640                         }
641                     }
642                     $userfrom->groups[$forum->id] = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid);
643                     if (isset($users[$userfrom->id])) {
644                         $users[$userfrom->id]->groups[$forum->id] = $userfrom->groups[$forum->id];
645                     }
646                 }
648                 // Make sure groups allow this user to see this email
649                 if ($discussion->groupid > 0 and $groupmode = groups_get_activity_groupmode($cm, $course)) {   // Groups are being used
650                     if (!groups_group_exists($discussion->groupid)) { // Can't find group
651                         continue;                           // Be safe and don't send it to anyone
652                     }
654                     if (!groups_is_member($discussion->groupid) and !has_capability('moodle/site:accessallgroups', $modcontext)) {
655                         // do not send posts from other groups when in SEPARATEGROUPS or VISIBLEGROUPS
656                         continue;
657                     }
658                 }
660                 // Make sure we're allowed to see it...
661                 if (!forum_user_can_see_post($forum, $discussion, $post, NULL, $cm)) {
662                     mtrace('user '.$userto->id. ' can not see '.$post->id);
663                     continue;
664                 }
666                 // OK so we need to send the email.
668                 // Does the user want this post in a digest?  If so postpone it for now.
669                 if ($userto->maildigest > 0) {
670                     // This user wants the mails to be in digest form
671                     $queue = new stdClass();
672                     $queue->userid       = $userto->id;
673                     $queue->discussionid = $discussion->id;
674                     $queue->postid       = $post->id;
675                     $queue->timemodified = $post->created;
676                     $DB->insert_record('forum_queue', $queue);
677                     continue;
678                 }
681                 // Prepare to actually send the post now, and build up the content
683                 $cleanforumname = str_replace('"', "'", strip_tags(format_string($forum->name)));
685                 $userfrom->customheaders = array (  // Headers to make emails easier to track
686                            'Precedence: Bulk',
687                            'List-Id: "'.$cleanforumname.'" <moodleforum'.$forum->id.'@'.$hostname.'>',
688                            'List-Help: '.$CFG->wwwroot.'/mod/forum/view.php?f='.$forum->id,
689                            'Message-ID: '.forum_get_email_message_id($post->id, $userto->id, $hostname),
690                            'X-Course-Id: '.$course->id,
691                            'X-Course-Name: '.format_string($course->fullname, true)
692                 );
694                 if ($post->parent) {  // This post is a reply, so add headers for threading (see MDL-22551)
695                     $userfrom->customheaders[] = 'In-Reply-To: '.forum_get_email_message_id($post->parent, $userto->id, $hostname);
696                     $userfrom->customheaders[] = 'References: '.forum_get_email_message_id($post->parent, $userto->id, $hostname);
697                 }
699                 $shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
701                 $postsubject = html_to_text("$shortname: ".format_string($post->subject, true));
702                 $posttext = forum_make_mail_text($course, $cm, $forum, $discussion, $post, $userfrom, $userto);
703                 $posthtml = forum_make_mail_html($course, $cm, $forum, $discussion, $post, $userfrom, $userto);
705                 // Send the post now!
707                 mtrace('Sending ', '');
709                 $eventdata = new stdClass();
710                 $eventdata->component        = 'mod_forum';
711                 $eventdata->name             = 'posts';
712                 $eventdata->userfrom         = $userfrom;
713                 $eventdata->userto           = $userto;
714                 $eventdata->subject          = $postsubject;
715                 $eventdata->fullmessage      = $posttext;
716                 $eventdata->fullmessageformat = FORMAT_PLAIN;
717                 $eventdata->fullmessagehtml  = $posthtml;
718                 $eventdata->notification = 1;
720                 // If forum_replytouser is not set then send mail using the noreplyaddress.
721                 if (empty($CFG->forum_replytouser)) {
722                     // Clone userfrom as it is referenced by $users.
723                     $cloneduserfrom = clone($userfrom);
724                     $cloneduserfrom->email = $CFG->noreplyaddress;
725                     $eventdata->userfrom = $cloneduserfrom;
726                 }
728                 $smallmessagestrings = new stdClass();
729                 $smallmessagestrings->user = fullname($userfrom);
730                 $smallmessagestrings->forumname = "$shortname: ".format_string($forum->name,true).": ".$discussion->name;
731                 $smallmessagestrings->message = $post->message;
732                 //make sure strings are in message recipients language
733                 $eventdata->smallmessage = get_string_manager()->get_string('smallmessage', 'forum', $smallmessagestrings, $userto->lang);
735                 $eventdata->contexturl = "{$CFG->wwwroot}/mod/forum/discuss.php?d={$discussion->id}#p{$post->id}";
736                 $eventdata->contexturlname = $discussion->name;
738                 $mailresult = message_send($eventdata);
739                 if (!$mailresult){
740                     mtrace("Error: mod/forum/lib.php forum_cron(): Could not send out mail for id $post->id to user $userto->id".
741                          " ($userto->email) .. not trying again.");
742                     add_to_log($course->id, 'forum', 'mail error', "discuss.php?d=$discussion->id#p$post->id",
743                                substr(format_string($post->subject,true),0,30), $cm->id, $userto->id);
744                     $errorcount[$post->id]++;
745                 } else {
746                     $mailcount[$post->id]++;
748                 // Mark post as read if forum_usermarksread is set off
749                     if (!$CFG->forum_usermarksread) {
750                         $userto->markposts[$post->id] = $post->id;
751                     }
752                 }
754                 mtrace('post '.$post->id. ': '.$post->subject);
755             }
757             // mark processed posts as read
758             forum_tp_mark_posts_read($userto, $userto->markposts);
759             unset($userto);
760         }
761     }
763     if ($posts) {
764         foreach ($posts as $post) {
765             mtrace($mailcount[$post->id]." users were sent post $post->id, '$post->subject'");
766             if ($errorcount[$post->id]) {
767                 $DB->set_field('forum_posts', 'mailed', FORUM_MAILED_ERROR, array('id' => $post->id));
768             }
769         }
770     }
772     // release some memory
773     unset($subscribedusers);
774     unset($mailcount);
775     unset($errorcount);
777     cron_setup_user();
779     $sitetimezone = $CFG->timezone;
781     // Now see if there are any digest mails waiting to be sent, and if we should send them
783     mtrace('Starting digest processing...');
785     @set_time_limit(300); // terminate if not able to fetch all digests in 5 minutes
787     if (!isset($CFG->digestmailtimelast)) {    // To catch the first time
788         set_config('digestmailtimelast', 0);
789     }
791     $timenow = time();
792     $digesttime = usergetmidnight($timenow, $sitetimezone) + ($CFG->digestmailtime * 3600);
794     // Delete any really old ones (normally there shouldn't be any)
795     $weekago = $timenow - (7 * 24 * 3600);
796     $DB->delete_records_select('forum_queue', "timemodified < ?", array($weekago));
797     mtrace ('Cleaned old digest records');
799     if ($CFG->digestmailtimelast < $digesttime and $timenow > $digesttime) {
801         mtrace('Sending forum digests: '.userdate($timenow, '', $sitetimezone));
803         $digestposts_rs = $DB->get_recordset_select('forum_queue', "timemodified < ?", array($digesttime));
805         if ($digestposts_rs->valid()) {
807             // We have work to do
808             $usermailcount = 0;
810             //caches - reuse the those filled before too
811             $discussionposts = array();
812             $userdiscussions = array();
814             foreach ($digestposts_rs as $digestpost) {
815                 if (!isset($posts[$digestpost->postid])) {
816                     if ($post = $DB->get_record('forum_posts', array('id' => $digestpost->postid))) {
817                         $posts[$digestpost->postid] = $post;
818                     } else {
819                         continue;
820                     }
821                 }
822                 $discussionid = $digestpost->discussionid;
823                 if (!isset($discussions[$discussionid])) {
824                     if ($discussion = $DB->get_record('forum_discussions', array('id' => $discussionid))) {
825                         $discussions[$discussionid] = $discussion;
826                     } else {
827                         continue;
828                     }
829                 }
830                 $forumid = $discussions[$discussionid]->forum;
831                 if (!isset($forums[$forumid])) {
832                     if ($forum = $DB->get_record('forum', array('id' => $forumid))) {
833                         $forums[$forumid] = $forum;
834                     } else {
835                         continue;
836                     }
837                 }
839                 $courseid = $forums[$forumid]->course;
840                 if (!isset($courses[$courseid])) {
841                     if ($course = $DB->get_record('course', array('id' => $courseid))) {
842                         $courses[$courseid] = $course;
843                     } else {
844                         continue;
845                     }
846                 }
848                 if (!isset($coursemodules[$forumid])) {
849                     if ($cm = get_coursemodule_from_instance('forum', $forumid, $courseid)) {
850                         $coursemodules[$forumid] = $cm;
851                     } else {
852                         continue;
853                     }
854                 }
855                 $userdiscussions[$digestpost->userid][$digestpost->discussionid] = $digestpost->discussionid;
856                 $discussionposts[$digestpost->discussionid][$digestpost->postid] = $digestpost->postid;
857             }
858             $digestposts_rs->close(); /// Finished iteration, let's close the resultset
860             // Data collected, start sending out emails to each user
861             foreach ($userdiscussions as $userid => $thesediscussions) {
863                 @set_time_limit(120); // terminate if processing of any account takes longer than 2 minutes
865                 cron_setup_user();
867                 mtrace(get_string('processingdigest', 'forum', $userid), '... ');
869                 // First of all delete all the queue entries for this user
870                 $DB->delete_records_select('forum_queue', "userid = ? AND timemodified < ?", array($userid, $digesttime));
872                 // Init user caches - we keep the cache for one cycle only,
873                 // otherwise it would unnecessarily consume memory.
874                 if (array_key_exists($userid, $users) and isset($users[$userid]->username)) {
875                     $userto = clone($users[$userid]);
876                 } else {
877                     $userto = $DB->get_record('user', array('id' => $userid));
878                     forum_cron_minimise_user_record($userto);
879                 }
880                 $userto->viewfullnames = array();
881                 $userto->canpost       = array();
882                 $userto->markposts     = array();
884                 // Override the language and timezone of the "current" user, so that
885                 // mail is customised for the receiver.
886                 cron_setup_user($userto);
888                 $postsubject = get_string('digestmailsubject', 'forum', format_string($site->shortname, true));
890                 $headerdata = new stdClass();
891                 $headerdata->sitename = format_string($site->fullname, true);
892                 $headerdata->userprefs = $CFG->wwwroot.'/user/edit.php?id='.$userid.'&amp;course='.$site->id;
894                 $posttext = get_string('digestmailheader', 'forum', $headerdata)."\n\n";
895                 $headerdata->userprefs = '<a target="_blank" href="'.$headerdata->userprefs.'">'.get_string('digestmailprefs', 'forum').'</a>';
897                 $posthtml = "<head>";
898 /*                foreach ($CFG->stylesheets as $stylesheet) {
899                     //TODO: MDL-21120
900                     $posthtml .= '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'" />'."\n";
901                 }*/
902                 $posthtml .= "</head>\n<body id=\"email\">\n";
903                 $posthtml .= '<p>'.get_string('digestmailheader', 'forum', $headerdata).'</p><br /><hr size="1" noshade="noshade" />';
905                 foreach ($thesediscussions as $discussionid) {
907                     @set_time_limit(120);   // to be reset for each post
909                     $discussion = $discussions[$discussionid];
910                     $forum      = $forums[$discussion->forum];
911                     $course     = $courses[$forum->course];
912                     $cm         = $coursemodules[$forum->id];
914                     //override language
915                     cron_setup_user($userto, $course);
917                     // Fill caches
918                     if (!isset($userto->viewfullnames[$forum->id])) {
919                         $modcontext = context_module::instance($cm->id);
920                         $userto->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $modcontext);
921                     }
922                     if (!isset($userto->canpost[$discussion->id])) {
923                         $modcontext = context_module::instance($cm->id);
924                         $userto->canpost[$discussion->id] = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
925                     }
927                     $strforums      = get_string('forums', 'forum');
928                     $canunsubscribe = ! forum_is_forcesubscribed($forum);
929                     $canreply       = $userto->canpost[$discussion->id];
930                     $shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
932                     $posttext .= "\n \n";
933                     $posttext .= '=====================================================================';
934                     $posttext .= "\n \n";
935                     $posttext .= "$shortname -> $strforums -> ".format_string($forum->name,true);
936                     if ($discussion->name != $forum->name) {
937                         $posttext  .= " -> ".format_string($discussion->name,true);
938                     }
939                     $posttext .= "\n";
941                     $posthtml .= "<p><font face=\"sans-serif\">".
942                     "<a target=\"_blank\" href=\"$CFG->wwwroot/course/view.php?id=$course->id\">$shortname</a> -> ".
943                     "<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/index.php?id=$course->id\">$strforums</a> -> ".
944                     "<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/view.php?f=$forum->id\">".format_string($forum->name,true)."</a>";
945                     if ($discussion->name == $forum->name) {
946                         $posthtml .= "</font></p>";
947                     } else {
948                         $posthtml .= " -> <a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/discuss.php?d=$discussion->id\">".format_string($discussion->name,true)."</a></font></p>";
949                     }
950                     $posthtml .= '<p>';
952                     $postsarray = $discussionposts[$discussionid];
953                     sort($postsarray);
955                     foreach ($postsarray as $postid) {
956                         $post = $posts[$postid];
958                         if (array_key_exists($post->userid, $users)) { // we might know him/her already
959                             $userfrom = $users[$post->userid];
960                             if (!isset($userfrom->idnumber)) {
961                                 $userfrom = $DB->get_record('user', array('id' => $userfrom->id));
962                                 forum_cron_minimise_user_record($userfrom);
963                             }
965                         } else if ($userfrom = $DB->get_record('user', array('id' => $post->userid))) {
966                             forum_cron_minimise_user_record($userfrom);
967                             if ($userscount <= FORUM_CRON_USER_CACHE) {
968                                 $userscount++;
969                                 $users[$userfrom->id] = $userfrom;
970                             }
972                         } else {
973                             mtrace('Could not find user '.$post->userid);
974                             continue;
975                         }
977                         if (!isset($userfrom->groups[$forum->id])) {
978                             if (!isset($userfrom->groups)) {
979                                 $userfrom->groups = array();
980                                 if (isset($users[$userfrom->id])) {
981                                     $users[$userfrom->id]->groups = array();
982                                 }
983                             }
984                             $userfrom->groups[$forum->id] = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid);
985                             if (isset($users[$userfrom->id])) {
986                                 $users[$userfrom->id]->groups[$forum->id] = $userfrom->groups[$forum->id];
987                             }
988                         }
990                         $userfrom->customheaders = array ("Precedence: Bulk");
992                         if ($userto->maildigest == 2) {
993                             // Subjects only
994                             $by = new stdClass();
995                             $by->name = fullname($userfrom);
996                             $by->date = userdate($post->modified);
997                             $posttext .= "\n".format_string($post->subject,true).' '.get_string("bynameondate", "forum", $by);
998                             $posttext .= "\n---------------------------------------------------------------------";
1000                             $by->name = "<a target=\"_blank\" href=\"$CFG->wwwroot/user/view.php?id=$userfrom->id&amp;course=$course->id\">$by->name</a>";
1001                             $posthtml .= '<div><a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$discussion->id.'#p'.$post->id.'">'.format_string($post->subject,true).'</a> '.get_string("bynameondate", "forum", $by).'</div>';
1003                         } else {
1004                             // The full treatment
1005                             $posttext .= forum_make_mail_text($course, $cm, $forum, $discussion, $post, $userfrom, $userto, true);
1006                             $posthtml .= forum_make_mail_post($course, $cm, $forum, $discussion, $post, $userfrom, $userto, false, $canreply, true, false);
1008                         // Create an array of postid's for this user to mark as read.
1009                             if (!$CFG->forum_usermarksread) {
1010                                 $userto->markposts[$post->id] = $post->id;
1011                             }
1012                         }
1013                     }
1014                     if ($canunsubscribe) {
1015                         $posthtml .= "\n<div class='mdl-right'><font size=\"1\"><a href=\"$CFG->wwwroot/mod/forum/subscribe.php?id=$forum->id\">".get_string("unsubscribe", "forum")."</a></font></div>";
1016                     } else {
1017                         $posthtml .= "\n<div class='mdl-right'><font size=\"1\">".get_string("everyoneissubscribed", "forum")."</font></div>";
1018                     }
1019                     $posthtml .= '<hr size="1" noshade="noshade" /></p>';
1020                 }
1021                 $posthtml .= '</body>';
1023                 if (empty($userto->mailformat) || $userto->mailformat != 1) {
1024                     // This user DOESN'T want to receive HTML
1025                     $posthtml = '';
1026                 }
1028                 $attachment = $attachname='';
1029                 // Directly email forum digests rather than sending them via messaging, use the
1030                 // site shortname as 'from name', the noreply address will be used by email_to_user.
1031                 $mailresult = email_to_user($userto, $site->shortname, $postsubject, $posttext, $posthtml, $attachment, $attachname);
1033                 if (!$mailresult) {
1034                     mtrace("ERROR!");
1035                     echo "Error: mod/forum/cron.php: Could not send out digest mail to user $userto->id ($userto->email)... not trying again.\n";
1036                     add_to_log($course->id, 'forum', 'mail digest error', '', '', $cm->id, $userto->id);
1037                 } else {
1038                     mtrace("success.");
1039                     $usermailcount++;
1041                     // Mark post as read if forum_usermarksread is set off
1042                     forum_tp_mark_posts_read($userto, $userto->markposts);
1043                 }
1044             }
1045         }
1046     /// We have finishied all digest emails, update $CFG->digestmailtimelast
1047         set_config('digestmailtimelast', $timenow);
1048     }
1050     cron_setup_user();
1052     if (!empty($usermailcount)) {
1053         mtrace(get_string('digestsentusers', 'forum', $usermailcount));
1054     }
1056     if (!empty($CFG->forum_lastreadclean)) {
1057         $timenow = time();
1058         if ($CFG->forum_lastreadclean + (24*3600) < $timenow) {
1059             set_config('forum_lastreadclean', $timenow);
1060             mtrace('Removing old forum read tracking info...');
1061             forum_tp_clean_read_records();
1062         }
1063     } else {
1064         set_config('forum_lastreadclean', time());
1065     }
1068     return true;
1071 /**
1072  * Builds and returns the body of the email notification in plain text.
1073  *
1074  * @global object
1075  * @global object
1076  * @uses CONTEXT_MODULE
1077  * @param object $course
1078  * @param object $cm
1079  * @param object $forum
1080  * @param object $discussion
1081  * @param object $post
1082  * @param object $userfrom
1083  * @param object $userto
1084  * @param boolean $bare
1085  * @return string The email body in plain text format.
1086  */
1087 function forum_make_mail_text($course, $cm, $forum, $discussion, $post, $userfrom, $userto, $bare = false) {
1088     global $CFG, $USER;
1090     $modcontext = context_module::instance($cm->id);
1092     if (!isset($userto->viewfullnames[$forum->id])) {
1093         $viewfullnames = has_capability('moodle/site:viewfullnames', $modcontext, $userto->id);
1094     } else {
1095         $viewfullnames = $userto->viewfullnames[$forum->id];
1096     }
1098     if (!isset($userto->canpost[$discussion->id])) {
1099         $canreply = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
1100     } else {
1101         $canreply = $userto->canpost[$discussion->id];
1102     }
1104     $by = New stdClass;
1105     $by->name = fullname($userfrom, $viewfullnames);
1106     $by->date = userdate($post->modified, "", $userto->timezone);
1108     $strbynameondate = get_string('bynameondate', 'forum', $by);
1110     $strforums = get_string('forums', 'forum');
1112     $canunsubscribe = ! forum_is_forcesubscribed($forum);
1114     $posttext = '';
1116     if (!$bare) {
1117         $shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
1118         $posttext  = "$shortname -> $strforums -> ".format_string($forum->name,true);
1120         if ($discussion->name != $forum->name) {
1121             $posttext  .= " -> ".format_string($discussion->name,true);
1122         }
1123     }
1125     // add absolute file links
1126     $post->message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php', $modcontext->id, 'mod_forum', 'post', $post->id);
1128     $posttext .= "\n---------------------------------------------------------------------\n";
1129     $posttext .= format_string($post->subject,true);
1130     if ($bare) {
1131         $posttext .= " ($CFG->wwwroot/mod/forum/discuss.php?d=$discussion->id#p$post->id)";
1132     }
1133     $posttext .= "\n".$strbynameondate."\n";
1134     $posttext .= "---------------------------------------------------------------------\n";
1135     $posttext .= format_text_email($post->message, $post->messageformat);
1136     $posttext .= "\n\n";
1137     $posttext .= forum_print_attachments($post, $cm, "text");
1139     if (!$bare && $canreply) {
1140         $posttext .= "---------------------------------------------------------------------\n";
1141         $posttext .= get_string("postmailinfo", "forum", $shortname)."\n";
1142         $posttext .= "$CFG->wwwroot/mod/forum/post.php?reply=$post->id\n";
1143     }
1144     if (!$bare && $canunsubscribe) {
1145         $posttext .= "\n---------------------------------------------------------------------\n";
1146         $posttext .= get_string("unsubscribe", "forum");
1147         $posttext .= ": $CFG->wwwroot/mod/forum/subscribe.php?id=$forum->id\n";
1148     }
1150     return $posttext;
1153 /**
1154  * Builds and returns the body of the email notification in html format.
1155  *
1156  * @global object
1157  * @param object $course
1158  * @param object $cm
1159  * @param object $forum
1160  * @param object $discussion
1161  * @param object $post
1162  * @param object $userfrom
1163  * @param object $userto
1164  * @return string The email text in HTML format
1165  */
1166 function forum_make_mail_html($course, $cm, $forum, $discussion, $post, $userfrom, $userto) {
1167     global $CFG;
1169     if ($userto->mailformat != 1) {  // Needs to be HTML
1170         return '';
1171     }
1173     if (!isset($userto->canpost[$discussion->id])) {
1174         $canreply = forum_user_can_post($forum, $discussion, $userto, $cm, $course);
1175     } else {
1176         $canreply = $userto->canpost[$discussion->id];
1177     }
1179     $strforums = get_string('forums', 'forum');
1180     $canunsubscribe = ! forum_is_forcesubscribed($forum);
1181     $shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
1183     $posthtml = '<head>';
1184 /*    foreach ($CFG->stylesheets as $stylesheet) {
1185         //TODO: MDL-21120
1186         $posthtml .= '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'" />'."\n";
1187     }*/
1188     $posthtml .= '</head>';
1189     $posthtml .= "\n<body id=\"email\">\n\n";
1191     $posthtml .= '<div class="navbar">'.
1192     '<a target="_blank" href="'.$CFG->wwwroot.'/course/view.php?id='.$course->id.'">'.$shortname.'</a> &raquo; '.
1193     '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/index.php?id='.$course->id.'">'.$strforums.'</a> &raquo; '.
1194     '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/view.php?f='.$forum->id.'">'.format_string($forum->name,true).'</a>';
1195     if ($discussion->name == $forum->name) {
1196         $posthtml .= '</div>';
1197     } else {
1198         $posthtml .= ' &raquo; <a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$discussion->id.'">'.
1199                      format_string($discussion->name,true).'</a></div>';
1200     }
1201     $posthtml .= forum_make_mail_post($course, $cm, $forum, $discussion, $post, $userfrom, $userto, false, $canreply, true, false);
1203     if ($canunsubscribe) {
1204         $posthtml .= '<hr /><div class="mdl-align unsubscribelink">
1205                       <a href="'.$CFG->wwwroot.'/mod/forum/subscribe.php?id='.$forum->id.'">'.get_string('unsubscribe', 'forum').'</a>&nbsp;
1206                       <a href="'.$CFG->wwwroot.'/mod/forum/unsubscribeall.php">'.get_string('unsubscribeall', 'forum').'</a></div>';
1207     }
1209     $posthtml .= '</body>';
1211     return $posthtml;
1215 /**
1216  *
1217  * @param object $course
1218  * @param object $user
1219  * @param object $mod TODO this is not used in this function, refactor
1220  * @param object $forum
1221  * @return object A standard object with 2 variables: info (number of posts for this user) and time (last modified)
1222  */
1223 function forum_user_outline($course, $user, $mod, $forum) {
1224     global $CFG;
1225     require_once("$CFG->libdir/gradelib.php");
1226     $grades = grade_get_grades($course->id, 'mod', 'forum', $forum->id, $user->id);
1227     if (empty($grades->items[0]->grades)) {
1228         $grade = false;
1229     } else {
1230         $grade = reset($grades->items[0]->grades);
1231     }
1233     $count = forum_count_user_posts($forum->id, $user->id);
1235     if ($count && $count->postcount > 0) {
1236         $result = new stdClass();
1237         $result->info = get_string("numposts", "forum", $count->postcount);
1238         $result->time = $count->lastpost;
1239         if ($grade) {
1240             $result->info .= ', ' . get_string('grade') . ': ' . $grade->str_long_grade;
1241         }
1242         return $result;
1243     } else if ($grade) {
1244         $result = new stdClass();
1245         $result->info = get_string('grade') . ': ' . $grade->str_long_grade;
1247         //datesubmitted == time created. dategraded == time modified or time overridden
1248         //if grade was last modified by the user themselves use date graded. Otherwise use date submitted
1249         //TODO: move this copied & pasted code somewhere in the grades API. See MDL-26704
1250         if ($grade->usermodified == $user->id || empty($grade->datesubmitted)) {
1251             $result->time = $grade->dategraded;
1252         } else {
1253             $result->time = $grade->datesubmitted;
1254         }
1256         return $result;
1257     }
1258     return NULL;
1262 /**
1263  * @global object
1264  * @global object
1265  * @param object $coure
1266  * @param object $user
1267  * @param object $mod
1268  * @param object $forum
1269  */
1270 function forum_user_complete($course, $user, $mod, $forum) {
1271     global $CFG,$USER, $OUTPUT;
1272     require_once("$CFG->libdir/gradelib.php");
1274     $grades = grade_get_grades($course->id, 'mod', 'forum', $forum->id, $user->id);
1275     if (!empty($grades->items[0]->grades)) {
1276         $grade = reset($grades->items[0]->grades);
1277         echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade);
1278         if ($grade->str_feedback) {
1279             echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
1280         }
1281     }
1283     if ($posts = forum_get_user_posts($forum->id, $user->id)) {
1285         if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $course->id)) {
1286             print_error('invalidcoursemodule');
1287         }
1288         $discussions = forum_get_user_involved_discussions($forum->id, $user->id);
1290         foreach ($posts as $post) {
1291             if (!isset($discussions[$post->discussion])) {
1292                 continue;
1293             }
1294             $discussion = $discussions[$post->discussion];
1296             forum_print_post($post, $discussion, $forum, $cm, $course, false, false, false);
1297         }
1298     } else {
1299         echo "<p>".get_string("noposts", "forum")."</p>";
1300     }
1308 /**
1309  * @global object
1310  * @global object
1311  * @global object
1312  * @param array $courses
1313  * @param array $htmlarray
1314  */
1315 function forum_print_overview($courses,&$htmlarray) {
1316     global $USER, $CFG, $DB, $SESSION;
1318     if (empty($courses) || !is_array($courses) || count($courses) == 0) {
1319         return array();
1320     }
1322     if (!$forums = get_all_instances_in_courses('forum',$courses)) {
1323         return;
1324     }
1326     // Courses to search for new posts
1327     $coursessqls = array();
1328     $params = array();
1329     foreach ($courses as $course) {
1331         // If the user has never entered into the course all posts are pending
1332         if ($course->lastaccess == 0) {
1333             $coursessqls[] = '(f.course = ?)';
1334             $params[] = $course->id;
1336         // Only posts created after the course last access
1337         } else {
1338             $coursessqls[] = '(f.course = ? AND p.created > ?)';
1339             $params[] = $course->id;
1340             $params[] = $course->lastaccess;
1341         }
1342     }
1343     $params[] = $USER->id;
1344     $coursessql = implode(' OR ', $coursessqls);
1346     $sql = "SELECT f.id, COUNT(*) as count "
1347                 .'FROM {forum} f '
1348                 .'JOIN {forum_discussions} d ON d.forum  = f.id '
1349                 .'JOIN {forum_posts} p ON p.discussion = d.id '
1350                 ."WHERE ($coursessql) "
1351                 .'AND p.userid != ? '
1352                 .'GROUP BY f.id';
1354     if (!$new = $DB->get_records_sql($sql, $params)) {
1355         $new = array(); // avoid warnings
1356     }
1358     // also get all forum tracking stuff ONCE.
1359     $trackingforums = array();
1360     foreach ($forums as $forum) {
1361         if (forum_tp_can_track_forums($forum)) {
1362             $trackingforums[$forum->id] = $forum;
1363         }
1364     }
1366     if (count($trackingforums) > 0) {
1367         $cutoffdate = isset($CFG->forum_oldpostdays) ? (time() - ($CFG->forum_oldpostdays*24*60*60)) : 0;
1368         $sql = 'SELECT d.forum,d.course,COUNT(p.id) AS count '.
1369             ' FROM {forum_posts} p '.
1370             ' JOIN {forum_discussions} d ON p.discussion = d.id '.
1371             ' LEFT JOIN {forum_read} r ON r.postid = p.id AND r.userid = ? WHERE (';
1372         $params = array($USER->id);
1374         foreach ($trackingforums as $track) {
1375             $sql .= '(d.forum = ? AND (d.groupid = -1 OR d.groupid = 0 OR d.groupid = ?)) OR ';
1376             $params[] = $track->id;
1377             if (isset($SESSION->currentgroup[$track->course])) {
1378                 $groupid =  $SESSION->currentgroup[$track->course];
1379             } else {
1380                 // get first groupid
1381                 $groupids = groups_get_all_groups($track->course, $USER->id);
1382                 if ($groupids) {
1383                     reset($groupids);
1384                     $groupid = key($groupids);
1385                     $SESSION->currentgroup[$track->course] = $groupid;
1386                 } else {
1387                     $groupid = 0;
1388                 }
1389                 unset($groupids);
1390             }
1391             $params[] = $groupid;
1392         }
1393         $sql = substr($sql,0,-3); // take off the last OR
1394         $sql .= ') AND p.modified >= ? AND r.id is NULL GROUP BY d.forum,d.course';
1395         $params[] = $cutoffdate;
1397         if (!$unread = $DB->get_records_sql($sql, $params)) {
1398             $unread = array();
1399         }
1400     } else {
1401         $unread = array();
1402     }
1404     if (empty($unread) and empty($new)) {
1405         return;
1406     }
1408     $strforum = get_string('modulename','forum');
1410     foreach ($forums as $forum) {
1411         $str = '';
1412         $count = 0;
1413         $thisunread = 0;
1414         $showunread = false;
1415         // either we have something from logs, or trackposts, or nothing.
1416         if (array_key_exists($forum->id, $new) && !empty($new[$forum->id])) {
1417             $count = $new[$forum->id]->count;
1418         }
1419         if (array_key_exists($forum->id,$unread)) {
1420             $thisunread = $unread[$forum->id]->count;
1421             $showunread = true;
1422         }
1423         if ($count > 0 || $thisunread > 0) {
1424             $str .= '<div class="overview forum"><div class="name">'.$strforum.': <a title="'.$strforum.'" href="'.$CFG->wwwroot.'/mod/forum/view.php?f='.$forum->id.'">'.
1425                 $forum->name.'</a></div>';
1426             $str .= '<div class="info"><span class="postsincelogin">';
1427             $str .= get_string('overviewnumpostssince', 'forum', $count)."</span>";
1428             if (!empty($showunread)) {
1429                 $str .= '<div class="unreadposts">'.get_string('overviewnumunread', 'forum', $thisunread).'</div>';
1430             }
1431             $str .= '</div></div>';
1432         }
1433         if (!empty($str)) {
1434             if (!array_key_exists($forum->course,$htmlarray)) {
1435                 $htmlarray[$forum->course] = array();
1436             }
1437             if (!array_key_exists('forum',$htmlarray[$forum->course])) {
1438                 $htmlarray[$forum->course]['forum'] = ''; // initialize, avoid warnings
1439             }
1440             $htmlarray[$forum->course]['forum'] .= $str;
1441         }
1442     }
1445 /**
1446  * Given a course and a date, prints a summary of all the new
1447  * messages posted in the course since that date
1448  *
1449  * @global object
1450  * @global object
1451  * @global object
1452  * @uses CONTEXT_MODULE
1453  * @uses VISIBLEGROUPS
1454  * @param object $course
1455  * @param bool $viewfullnames capability
1456  * @param int $timestart
1457  * @return bool success
1458  */
1459 function forum_print_recent_activity($course, $viewfullnames, $timestart) {
1460     global $CFG, $USER, $DB, $OUTPUT;
1462     // do not use log table if possible, it may be huge and is expensive to join with other tables
1464     if (!$posts = $DB->get_records_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid,
1465                                               d.timestart, d.timeend, d.userid AS duserid,
1466                                               u.firstname, u.lastname, u.email, u.picture
1467                                          FROM {forum_posts} p
1468                                               JOIN {forum_discussions} d ON d.id = p.discussion
1469                                               JOIN {forum} f             ON f.id = d.forum
1470                                               JOIN {user} u              ON u.id = p.userid
1471                                         WHERE p.created > ? AND f.course = ?
1472                                      ORDER BY p.id ASC", array($timestart, $course->id))) { // order by initial posting date
1473          return false;
1474     }
1476     $modinfo = get_fast_modinfo($course);
1478     $groupmodes = array();
1479     $cms    = array();
1481     $strftimerecent = get_string('strftimerecent');
1483     $printposts = array();
1484     foreach ($posts as $post) {
1485         if (!isset($modinfo->instances['forum'][$post->forum])) {
1486             // not visible
1487             continue;
1488         }
1489         $cm = $modinfo->instances['forum'][$post->forum];
1490         if (!$cm->uservisible) {
1491             continue;
1492         }
1493         $context = context_module::instance($cm->id);
1495         if (!has_capability('mod/forum:viewdiscussion', $context)) {
1496             continue;
1497         }
1499         if (!empty($CFG->forum_enabletimedposts) and $USER->id != $post->duserid
1500           and (($post->timestart > 0 and $post->timestart > time()) or ($post->timeend > 0 and $post->timeend < time()))) {
1501             if (!has_capability('mod/forum:viewhiddentimedposts', $context)) {
1502                 continue;
1503             }
1504         }
1506         $groupmode = groups_get_activity_groupmode($cm, $course);
1508         if ($groupmode) {
1509             if ($post->groupid == -1 or $groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $context)) {
1510                 // oki (Open discussions have groupid -1)
1511             } else {
1512                 // separate mode
1513                 if (isguestuser()) {
1514                     // shortcut
1515                     continue;
1516                 }
1518                 if (is_null($modinfo->groups)) {
1519                     $modinfo->groups = groups_get_user_groups($course->id); // load all my groups and cache it in modinfo
1520                 }
1522                 if (!array_key_exists($post->groupid, $modinfo->groups[0])) {
1523                     continue;
1524                 }
1525             }
1526         }
1528         $printposts[] = $post;
1529     }
1530     unset($posts);
1532     if (!$printposts) {
1533         return false;
1534     }
1536     echo $OUTPUT->heading(get_string('newforumposts', 'forum').':', 3);
1537     echo "\n<ul class='unlist'>\n";
1539     foreach ($printposts as $post) {
1540         $subjectclass = empty($post->parent) ? ' bold' : '';
1542         echo '<li><div class="head">'.
1543                '<div class="date">'.userdate($post->modified, $strftimerecent).'</div>'.
1544                '<div class="name">'.fullname($post, $viewfullnames).'</div>'.
1545              '</div>';
1546         echo '<div class="info'.$subjectclass.'">';
1547         if (empty($post->parent)) {
1548             echo '"<a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'">';
1549         } else {
1550             echo '"<a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'&amp;parent='.$post->parent.'#p'.$post->id.'">';
1551         }
1552         $post->subject = break_up_long_words(format_string($post->subject, true));
1553         echo $post->subject;
1554         echo "</a>\"</div></li>\n";
1555     }
1557     echo "</ul>\n";
1559     return true;
1562 /**
1563  * Return grade for given user or all users.
1564  *
1565  * @global object
1566  * @global object
1567  * @param object $forum
1568  * @param int $userid optional user id, 0 means all users
1569  * @return array array of grades, false if none
1570  */
1571 function forum_get_user_grades($forum, $userid = 0) {
1572     global $CFG;
1574     require_once($CFG->dirroot.'/rating/lib.php');
1576     $ratingoptions = new stdClass;
1577     $ratingoptions->component = 'mod_forum';
1578     $ratingoptions->ratingarea = 'post';
1580     //need these to work backwards to get a context id. Is there a better way to get contextid from a module instance?
1581     $ratingoptions->modulename = 'forum';
1582     $ratingoptions->moduleid   = $forum->id;
1583     $ratingoptions->userid = $userid;
1584     $ratingoptions->aggregationmethod = $forum->assessed;
1585     $ratingoptions->scaleid = $forum->scale;
1586     $ratingoptions->itemtable = 'forum_posts';
1587     $ratingoptions->itemtableusercolumn = 'userid';
1589     $rm = new rating_manager();
1590     return $rm->get_user_grades($ratingoptions);
1593 /**
1594  * Update activity grades
1595  *
1596  * @category grade
1597  * @param object $forum
1598  * @param int $userid specific user only, 0 means all
1599  * @param boolean $nullifnone return null if grade does not exist
1600  * @return void
1601  */
1602 function forum_update_grades($forum, $userid=0, $nullifnone=true) {
1603     global $CFG, $DB;
1604     require_once($CFG->libdir.'/gradelib.php');
1606     if (!$forum->assessed) {
1607         forum_grade_item_update($forum);
1609     } else if ($grades = forum_get_user_grades($forum, $userid)) {
1610         forum_grade_item_update($forum, $grades);
1612     } else if ($userid and $nullifnone) {
1613         $grade = new stdClass();
1614         $grade->userid   = $userid;
1615         $grade->rawgrade = NULL;
1616         forum_grade_item_update($forum, $grade);
1618     } else {
1619         forum_grade_item_update($forum);
1620     }
1623 /**
1624  * Update all grades in gradebook.
1625  * @global object
1626  */
1627 function forum_upgrade_grades() {
1628     global $DB;
1630     $sql = "SELECT COUNT('x')
1631               FROM {forum} f, {course_modules} cm, {modules} m
1632              WHERE m.name='forum' AND m.id=cm.module AND cm.instance=f.id";
1633     $count = $DB->count_records_sql($sql);
1635     $sql = "SELECT f.*, cm.idnumber AS cmidnumber, f.course AS courseid
1636               FROM {forum} f, {course_modules} cm, {modules} m
1637              WHERE m.name='forum' AND m.id=cm.module AND cm.instance=f.id";
1638     $rs = $DB->get_recordset_sql($sql);
1639     if ($rs->valid()) {
1640         $pbar = new progress_bar('forumupgradegrades', 500, true);
1641         $i=0;
1642         foreach ($rs as $forum) {
1643             $i++;
1644             upgrade_set_timeout(60*5); // set up timeout, may also abort execution
1645             forum_update_grades($forum, 0, false);
1646             $pbar->update($i, $count, "Updating Forum grades ($i/$count).");
1647         }
1648     }
1649     $rs->close();
1652 /**
1653  * Create/update grade item for given forum
1654  *
1655  * @category grade
1656  * @uses GRADE_TYPE_NONE
1657  * @uses GRADE_TYPE_VALUE
1658  * @uses GRADE_TYPE_SCALE
1659  * @param stdClass $forum Forum object with extra cmidnumber
1660  * @param mixed $grades Optional array/object of grade(s); 'reset' means reset grades in gradebook
1661  * @return int 0 if ok
1662  */
1663 function forum_grade_item_update($forum, $grades=NULL) {
1664     global $CFG;
1665     if (!function_exists('grade_update')) { //workaround for buggy PHP versions
1666         require_once($CFG->libdir.'/gradelib.php');
1667     }
1669     $params = array('itemname'=>$forum->name, 'idnumber'=>$forum->cmidnumber);
1671     if (!$forum->assessed or $forum->scale == 0) {
1672         $params['gradetype'] = GRADE_TYPE_NONE;
1674     } else if ($forum->scale > 0) {
1675         $params['gradetype'] = GRADE_TYPE_VALUE;
1676         $params['grademax']  = $forum->scale;
1677         $params['grademin']  = 0;
1679     } else if ($forum->scale < 0) {
1680         $params['gradetype'] = GRADE_TYPE_SCALE;
1681         $params['scaleid']   = -$forum->scale;
1682     }
1684     if ($grades  === 'reset') {
1685         $params['reset'] = true;
1686         $grades = NULL;
1687     }
1689     return grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, $grades, $params);
1692 /**
1693  * Delete grade item for given forum
1694  *
1695  * @category grade
1696  * @param stdClass $forum Forum object
1697  * @return grade_item
1698  */
1699 function forum_grade_item_delete($forum) {
1700     global $CFG;
1701     require_once($CFG->libdir.'/gradelib.php');
1703     return grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, NULL, array('deleted'=>1));
1707 /**
1708  * This function returns if a scale is being used by one forum
1709  *
1710  * @global object
1711  * @param int $forumid
1712  * @param int $scaleid negative number
1713  * @return bool
1714  */
1715 function forum_scale_used ($forumid,$scaleid) {
1716     global $DB;
1717     $return = false;
1719     $rec = $DB->get_record("forum",array("id" => "$forumid","scale" => "-$scaleid"));
1721     if (!empty($rec) && !empty($scaleid)) {
1722         $return = true;
1723     }
1725     return $return;
1728 /**
1729  * Checks if scale is being used by any instance of forum
1730  *
1731  * This is used to find out if scale used anywhere
1732  *
1733  * @global object
1734  * @param $scaleid int
1735  * @return boolean True if the scale is used by any forum
1736  */
1737 function forum_scale_used_anywhere($scaleid) {
1738     global $DB;
1739     if ($scaleid and $DB->record_exists('forum', array('scale' => -$scaleid))) {
1740         return true;
1741     } else {
1742         return false;
1743     }
1746 // SQL FUNCTIONS ///////////////////////////////////////////////////////////
1748 /**
1749  * Gets a post with all info ready for forum_print_post
1750  * Most of these joins are just to get the forum id
1751  *
1752  * @global object
1753  * @global object
1754  * @param int $postid
1755  * @return mixed array of posts or false
1756  */
1757 function forum_get_post_full($postid) {
1758     global $CFG, $DB;
1760     return $DB->get_record_sql("SELECT p.*, d.forum, u.firstname, u.lastname, u.email, u.picture, u.imagealt
1761                              FROM {forum_posts} p
1762                                   JOIN {forum_discussions} d ON p.discussion = d.id
1763                                   LEFT JOIN {user} u ON p.userid = u.id
1764                             WHERE p.id = ?", array($postid));
1767 /**
1768  * Gets posts with all info ready for forum_print_post
1769  * We pass forumid in because we always know it so no need to make a
1770  * complicated join to find it out.
1771  *
1772  * @global object
1773  * @global object
1774  * @return mixed array of posts or false
1775  */
1776 function forum_get_discussion_posts($discussion, $sort, $forumid) {
1777     global $CFG, $DB;
1779     return $DB->get_records_sql("SELECT p.*, $forumid AS forum, u.firstname, u.lastname, u.email, u.picture, u.imagealt
1780                               FROM {forum_posts} p
1781                          LEFT JOIN {user} u ON p.userid = u.id
1782                              WHERE p.discussion = ?
1783                                AND p.parent > 0 $sort", array($discussion));
1786 /**
1787  * Gets all posts in discussion including top parent.
1788  *
1789  * @global object
1790  * @global object
1791  * @global object
1792  * @param int $discussionid
1793  * @param string $sort
1794  * @param bool $tracking does user track the forum?
1795  * @return array of posts
1796  */
1797 function forum_get_all_discussion_posts($discussionid, $sort, $tracking=false) {
1798     global $CFG, $DB, $USER;
1800     $tr_sel  = "";
1801     $tr_join = "";
1802     $params = array();
1804     if ($tracking) {
1805         $now = time();
1806         $cutoffdate = $now - ($CFG->forum_oldpostdays * 24 * 3600);
1807         $tr_sel  = ", fr.id AS postread";
1808         $tr_join = "LEFT JOIN {forum_read} fr ON (fr.postid = p.id AND fr.userid = ?)";
1809         $params[] = $USER->id;
1810     }
1812     $params[] = $discussionid;
1813     if (!$posts = $DB->get_records_sql("SELECT p.*, u.firstname, u.lastname, u.email, u.picture, u.imagealt $tr_sel
1814                                      FROM {forum_posts} p
1815                                           LEFT JOIN {user} u ON p.userid = u.id
1816                                           $tr_join
1817                                     WHERE p.discussion = ?
1818                                  ORDER BY $sort", $params)) {
1819         return array();
1820     }
1822     foreach ($posts as $pid=>$p) {
1823         if ($tracking) {
1824             if (forum_tp_is_post_old($p)) {
1825                  $posts[$pid]->postread = true;
1826             }
1827         }
1828         if (!$p->parent) {
1829             continue;
1830         }
1831         if (!isset($posts[$p->parent])) {
1832             continue; // parent does not exist??
1833         }
1834         if (!isset($posts[$p->parent]->children)) {
1835             $posts[$p->parent]->children = array();
1836         }
1837         $posts[$p->parent]->children[$pid] =& $posts[$pid];
1838     }
1840     return $posts;
1843 /**
1844  * Gets posts with all info ready for forum_print_post
1845  * We pass forumid in because we always know it so no need to make a
1846  * complicated join to find it out.
1847  *
1848  * @global object
1849  * @global object
1850  * @param int $parent
1851  * @param int $forumid
1852  * @return array
1853  */
1854 function forum_get_child_posts($parent, $forumid) {
1855     global $CFG, $DB;
1857     return $DB->get_records_sql("SELECT p.*, $forumid AS forum, u.firstname, u.lastname, u.email, u.picture, u.imagealt
1858                               FROM {forum_posts} p
1859                          LEFT JOIN {user} u ON p.userid = u.id
1860                              WHERE p.parent = ?
1861                           ORDER BY p.created ASC", array($parent));
1864 /**
1865  * An array of forum objects that the user is allowed to read/search through.
1866  *
1867  * @global object
1868  * @global object
1869  * @global object
1870  * @param int $userid
1871  * @param int $courseid if 0, we look for forums throughout the whole site.
1872  * @return array of forum objects, or false if no matches
1873  *         Forum objects have the following attributes:
1874  *         id, type, course, cmid, cmvisible, cmgroupmode, accessallgroups,
1875  *         viewhiddentimedposts
1876  */
1877 function forum_get_readable_forums($userid, $courseid=0) {
1879     global $CFG, $DB, $USER;
1880     require_once($CFG->dirroot.'/course/lib.php');
1882     if (!$forummod = $DB->get_record('modules', array('name' => 'forum'))) {
1883         print_error('notinstalled', 'forum');
1884     }
1886     if ($courseid) {
1887         $courses = $DB->get_records('course', array('id' => $courseid));
1888     } else {
1889         // If no course is specified, then the user can see SITE + his courses.
1890         $courses1 = $DB->get_records('course', array('id' => SITEID));
1891         $courses2 = enrol_get_users_courses($userid, true, array('modinfo'));
1892         $courses = array_merge($courses1, $courses2);
1893     }
1894     if (!$courses) {
1895         return array();
1896     }
1898     $readableforums = array();
1900     foreach ($courses as $course) {
1902         $modinfo = get_fast_modinfo($course);
1903         if (is_null($modinfo->groups)) {
1904             $modinfo->groups = groups_get_user_groups($course->id, $userid);
1905         }
1907         if (empty($modinfo->instances['forum'])) {
1908             // hmm, no forums?
1909             continue;
1910         }
1912         $courseforums = $DB->get_records('forum', array('course' => $course->id));
1914         foreach ($modinfo->instances['forum'] as $forumid => $cm) {
1915             if (!$cm->uservisible or !isset($courseforums[$forumid])) {
1916                 continue;
1917             }
1918             $context = context_module::instance($cm->id);
1919             $forum = $courseforums[$forumid];
1920             $forum->context = $context;
1921             $forum->cm = $cm;
1923             if (!has_capability('mod/forum:viewdiscussion', $context)) {
1924                 continue;
1925             }
1927          /// group access
1928             if (groups_get_activity_groupmode($cm, $course) == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) {
1929                 if (is_null($modinfo->groups)) {
1930                     $modinfo->groups = groups_get_user_groups($course->id, $USER->id);
1931                 }
1932                 if (isset($modinfo->groups[$cm->groupingid])) {
1933                     $forum->onlygroups = $modinfo->groups[$cm->groupingid];
1934                     $forum->onlygroups[] = -1;
1935                 } else {
1936                     $forum->onlygroups = array(-1);
1937                 }
1938             }
1940         /// hidden timed discussions
1941             $forum->viewhiddentimedposts = true;
1942             if (!empty($CFG->forum_enabletimedposts)) {
1943                 if (!has_capability('mod/forum:viewhiddentimedposts', $context)) {
1944                     $forum->viewhiddentimedposts = false;
1945                 }
1946             }
1948         /// qanda access
1949             if ($forum->type == 'qanda'
1950                     && !has_capability('mod/forum:viewqandawithoutposting', $context)) {
1952                 // We need to check whether the user has posted in the qanda forum.
1953                 $forum->onlydiscussions = array();  // Holds discussion ids for the discussions
1954                                                     // the user is allowed to see in this forum.
1955                 if ($discussionspostedin = forum_discussions_user_has_posted_in($forum->id, $USER->id)) {
1956                     foreach ($discussionspostedin as $d) {
1957                         $forum->onlydiscussions[] = $d->id;
1958                     }
1959                 }
1960             }
1962             $readableforums[$forum->id] = $forum;
1963         }
1965         unset($modinfo);
1967     } // End foreach $courses
1969     return $readableforums;
1972 /**
1973  * Returns a list of posts found using an array of search terms.
1974  *
1975  * @global object
1976  * @global object
1977  * @global object
1978  * @param array $searchterms array of search terms, e.g. word +word -word
1979  * @param int $courseid if 0, we search through the whole site
1980  * @param int $limitfrom
1981  * @param int $limitnum
1982  * @param int &$totalcount
1983  * @param string $extrasql
1984  * @return array|bool Array of posts found or false
1985  */
1986 function forum_search_posts($searchterms, $courseid=0, $limitfrom=0, $limitnum=50,
1987                             &$totalcount, $extrasql='') {
1988     global $CFG, $DB, $USER;
1989     require_once($CFG->libdir.'/searchlib.php');
1991     $forums = forum_get_readable_forums($USER->id, $courseid);
1993     if (count($forums) == 0) {
1994         $totalcount = 0;
1995         return false;
1996     }
1998     $now = round(time(), -2); // db friendly
2000     $fullaccess = array();
2001     $where = array();
2002     $params = array();
2004     foreach ($forums as $forumid => $forum) {
2005         $select = array();
2007         if (!$forum->viewhiddentimedposts) {
2008             $select[] = "(d.userid = :userid{$forumid} OR (d.timestart < :timestart{$forumid} AND (d.timeend = 0 OR d.timeend > :timeend{$forumid})))";
2009             $params = array_merge($params, array('userid'.$forumid=>$USER->id, 'timestart'.$forumid=>$now, 'timeend'.$forumid=>$now));
2010         }
2012         $cm = $forum->cm;
2013         $context = $forum->context;
2015         if ($forum->type == 'qanda'
2016             && !has_capability('mod/forum:viewqandawithoutposting', $context)) {
2017             if (!empty($forum->onlydiscussions)) {
2018                 list($discussionid_sql, $discussionid_params) = $DB->get_in_or_equal($forum->onlydiscussions, SQL_PARAMS_NAMED, 'qanda'.$forumid.'_');
2019                 $params = array_merge($params, $discussionid_params);
2020                 $select[] = "(d.id $discussionid_sql OR p.parent = 0)";
2021             } else {
2022                 $select[] = "p.parent = 0";
2023             }
2024         }
2026         if (!empty($forum->onlygroups)) {
2027             list($groupid_sql, $groupid_params) = $DB->get_in_or_equal($forum->onlygroups, SQL_PARAMS_NAMED, 'grps'.$forumid.'_');
2028             $params = array_merge($params, $groupid_params);
2029             $select[] = "d.groupid $groupid_sql";
2030         }
2032         if ($select) {
2033             $selects = implode(" AND ", $select);
2034             $where[] = "(d.forum = :forum{$forumid} AND $selects)";
2035             $params['forum'.$forumid] = $forumid;
2036         } else {
2037             $fullaccess[] = $forumid;
2038         }
2039     }
2041     if ($fullaccess) {
2042         list($fullid_sql, $fullid_params) = $DB->get_in_or_equal($fullaccess, SQL_PARAMS_NAMED, 'fula');
2043         $params = array_merge($params, $fullid_params);
2044         $where[] = "(d.forum $fullid_sql)";
2045     }
2047     $selectdiscussion = "(".implode(" OR ", $where).")";
2049     $messagesearch = '';
2050     $searchstring = '';
2052     // Need to concat these back together for parser to work.
2053     foreach($searchterms as $searchterm){
2054         if ($searchstring != '') {
2055             $searchstring .= ' ';
2056         }
2057         $searchstring .= $searchterm;
2058     }
2060     // We need to allow quoted strings for the search. The quotes *should* be stripped
2061     // by the parser, but this should be examined carefully for security implications.
2062     $searchstring = str_replace("\\\"","\"",$searchstring);
2063     $parser = new search_parser();
2064     $lexer = new search_lexer($parser);
2066     if ($lexer->parse($searchstring)) {
2067         $parsearray = $parser->get_parsed_array();
2068     // Experimental feature under 1.8! MDL-8830
2069     // Use alternative text searches if defined
2070     // This feature only works under mysql until properly implemented for other DBs
2071     // Requires manual creation of text index for forum_posts before enabling it:
2072     // CREATE FULLTEXT INDEX foru_post_tix ON [prefix]forum_posts (subject, message)
2073     // Experimental feature under 1.8! MDL-8830
2074         if (!empty($CFG->forum_usetextsearches)) {
2075             list($messagesearch, $msparams) = search_generate_text_SQL($parsearray, 'p.message', 'p.subject',
2076                                                  'p.userid', 'u.id', 'u.firstname',
2077                                                  'u.lastname', 'p.modified', 'd.forum');
2078         } else {
2079             list($messagesearch, $msparams) = search_generate_SQL($parsearray, 'p.message', 'p.subject',
2080                                                  'p.userid', 'u.id', 'u.firstname',
2081                                                  'u.lastname', 'p.modified', 'd.forum');
2082         }
2083         $params = array_merge($params, $msparams);
2084     }
2086     $fromsql = "{forum_posts} p,
2087                   {forum_discussions} d,
2088                   {user} u";
2090     $selectsql = " $messagesearch
2091                AND p.discussion = d.id
2092                AND p.userid = u.id
2093                AND $selectdiscussion
2094                    $extrasql";
2096     $countsql = "SELECT COUNT(*)
2097                    FROM $fromsql
2098                   WHERE $selectsql";
2100     $searchsql = "SELECT p.*,
2101                          d.forum,
2102                          u.firstname,
2103                          u.lastname,
2104                          u.email,
2105                          u.picture,
2106                          u.imagealt
2107                     FROM $fromsql
2108                    WHERE $selectsql
2109                 ORDER BY p.modified DESC";
2111     $totalcount = $DB->count_records_sql($countsql, $params);
2113     return $DB->get_records_sql($searchsql, $params, $limitfrom, $limitnum);
2116 /**
2117  * Returns a list of ratings for a particular post - sorted.
2118  *
2119  * TODO: Check if this function is actually used anywhere.
2120  * Up until the fix for MDL-27471 this function wasn't even returning.
2121  *
2122  * @param stdClass $context
2123  * @param int $postid
2124  * @param string $sort
2125  * @return array Array of ratings or false
2126  */
2127 function forum_get_ratings($context, $postid, $sort = "u.firstname ASC") {
2128     $options = new stdClass;
2129     $options->context = $context;
2130     $options->component = 'mod_forum';
2131     $options->ratingarea = 'post';
2132     $options->itemid = $postid;
2133     $options->sort = "ORDER BY $sort";
2135     $rm = new rating_manager();
2136     return $rm->get_all_ratings_for_item($options);
2139 /**
2140  * Returns a list of all new posts that have not been mailed yet
2141  *
2142  * @param int $starttime posts created after this time
2143  * @param int $endtime posts created before this
2144  * @param int $now used for timed discussions only
2145  * @return array
2146  */
2147 function forum_get_unmailed_posts($starttime, $endtime, $now=null) {
2148     global $CFG, $DB;
2150     $params = array();
2151     $params['mailed'] = FORUM_MAILED_PENDING;
2152     $params['ptimestart'] = $starttime;
2153     $params['ptimeend'] = $endtime;
2154     $params['mailnow'] = 1;
2156     if (!empty($CFG->forum_enabletimedposts)) {
2157         if (empty($now)) {
2158             $now = time();
2159         }
2160         $timedsql = "AND (d.timestart < :dtimestart AND (d.timeend = 0 OR d.timeend > :dtimeend))";
2161         $params['dtimestart'] = $now;
2162         $params['dtimeend'] = $now;
2163     } else {
2164         $timedsql = "";
2165     }
2167     return $DB->get_records_sql("SELECT p.*, d.course, d.forum
2168                                  FROM {forum_posts} p
2169                                  JOIN {forum_discussions} d ON d.id = p.discussion
2170                                  WHERE p.mailed = :mailed
2171                                  AND p.created >= :ptimestart
2172                                  AND (p.created < :ptimeend OR p.mailnow = :mailnow)
2173                                  $timedsql
2174                                  ORDER BY p.modified ASC", $params);
2177 /**
2178  * Marks posts before a certain time as being mailed already
2179  *
2180  * @global object
2181  * @global object
2182  * @param int $endtime
2183  * @param int $now Defaults to time()
2184  * @return bool
2185  */
2186 function forum_mark_old_posts_as_mailed($endtime, $now=null) {
2187     global $CFG, $DB;
2189     if (empty($now)) {
2190         $now = time();
2191     }
2193     $params = array();
2194     $params['mailedsuccess'] = FORUM_MAILED_SUCCESS;
2195     $params['now'] = $now;
2196     $params['endtime'] = $endtime;
2197     $params['mailnow'] = 1;
2198     $params['mailedpending'] = FORUM_MAILED_PENDING;
2200     if (empty($CFG->forum_enabletimedposts)) {
2201         return $DB->execute("UPDATE {forum_posts}
2202                              SET mailed = :mailedsuccess
2203                              WHERE (created < :endtime OR mailnow = :mailnow)
2204                              AND mailed = :mailedpending", $params);
2205     } else {
2206         return $DB->execute("UPDATE {forum_posts}
2207                              SET mailed = :mailedsuccess
2208                              WHERE discussion NOT IN (SELECT d.id
2209                                                       FROM {forum_discussions} d
2210                                                       WHERE d.timestart > :now)
2211                              AND (created < :endtime OR mailnow = :mailnow)
2212                              AND mailed = :mailedpending", $params);
2213     }
2216 /**
2217  * Get all the posts for a user in a forum suitable for forum_print_post
2218  *
2219  * @global object
2220  * @global object
2221  * @uses CONTEXT_MODULE
2222  * @return array
2223  */
2224 function forum_get_user_posts($forumid, $userid) {
2225     global $CFG, $DB;
2227     $timedsql = "";
2228     $params = array($forumid, $userid);
2230     if (!empty($CFG->forum_enabletimedposts)) {
2231         $cm = get_coursemodule_from_instance('forum', $forumid);
2232         if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
2233             $now = time();
2234             $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2235             $params[] = $now;
2236             $params[] = $now;
2237         }
2238     }
2240     return $DB->get_records_sql("SELECT p.*, d.forum, u.firstname, u.lastname, u.email, u.picture, u.imagealt
2241                               FROM {forum} f
2242                                    JOIN {forum_discussions} d ON d.forum = f.id
2243                                    JOIN {forum_posts} p       ON p.discussion = d.id
2244                                    JOIN {user} u              ON u.id = p.userid
2245                              WHERE f.id = ?
2246                                    AND p.userid = ?
2247                                    $timedsql
2248                           ORDER BY p.modified ASC", $params);
2251 /**
2252  * Get all the discussions user participated in
2253  *
2254  * @global object
2255  * @global object
2256  * @uses CONTEXT_MODULE
2257  * @param int $forumid
2258  * @param int $userid
2259  * @return array Array or false
2260  */
2261 function forum_get_user_involved_discussions($forumid, $userid) {
2262     global $CFG, $DB;
2264     $timedsql = "";
2265     $params = array($forumid, $userid);
2266     if (!empty($CFG->forum_enabletimedposts)) {
2267         $cm = get_coursemodule_from_instance('forum', $forumid);
2268         if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
2269             $now = time();
2270             $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2271             $params[] = $now;
2272             $params[] = $now;
2273         }
2274     }
2276     return $DB->get_records_sql("SELECT DISTINCT d.*
2277                               FROM {forum} f
2278                                    JOIN {forum_discussions} d ON d.forum = f.id
2279                                    JOIN {forum_posts} p       ON p.discussion = d.id
2280                              WHERE f.id = ?
2281                                    AND p.userid = ?
2282                                    $timedsql", $params);
2285 /**
2286  * Get all the posts for a user in a forum suitable for forum_print_post
2287  *
2288  * @global object
2289  * @global object
2290  * @param int $forumid
2291  * @param int $userid
2292  * @return array of counts or false
2293  */
2294 function forum_count_user_posts($forumid, $userid) {
2295     global $CFG, $DB;
2297     $timedsql = "";
2298     $params = array($forumid, $userid);
2299     if (!empty($CFG->forum_enabletimedposts)) {
2300         $cm = get_coursemodule_from_instance('forum', $forumid);
2301         if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
2302             $now = time();
2303             $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2304             $params[] = $now;
2305             $params[] = $now;
2306         }
2307     }
2309     return $DB->get_record_sql("SELECT COUNT(p.id) AS postcount, MAX(p.modified) AS lastpost
2310                              FROM {forum} f
2311                                   JOIN {forum_discussions} d ON d.forum = f.id
2312                                   JOIN {forum_posts} p       ON p.discussion = d.id
2313                                   JOIN {user} u              ON u.id = p.userid
2314                             WHERE f.id = ?
2315                                   AND p.userid = ?
2316                                   $timedsql", $params);
2319 /**
2320  * Given a log entry, return the forum post details for it.
2321  *
2322  * @global object
2323  * @global object
2324  * @param object $log
2325  * @return array|null
2326  */
2327 function forum_get_post_from_log($log) {
2328     global $CFG, $DB;
2330     if ($log->action == "add post") {
2332         return $DB->get_record_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid,
2333                                            u.firstname, u.lastname, u.email, u.picture
2334                                  FROM {forum_discussions} d,
2335                                       {forum_posts} p,
2336                                       {forum} f,
2337                                       {user} u
2338                                 WHERE p.id = ?
2339                                   AND d.id = p.discussion
2340                                   AND p.userid = u.id
2341                                   AND u.deleted <> '1'
2342                                   AND f.id = d.forum", array($log->info));
2345     } else if ($log->action == "add discussion") {
2347         return $DB->get_record_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid,
2348                                            u.firstname, u.lastname, u.email, u.picture
2349                                  FROM {forum_discussions} d,
2350                                       {forum_posts} p,
2351                                       {forum} f,
2352                                       {user} u
2353                                 WHERE d.id = ?
2354                                   AND d.firstpost = p.id
2355                                   AND p.userid = u.id
2356                                   AND u.deleted <> '1'
2357                                   AND f.id = d.forum", array($log->info));
2358     }
2359     return NULL;
2362 /**
2363  * Given a discussion id, return the first post from the discussion
2364  *
2365  * @global object
2366  * @global object
2367  * @param int $dicsussionid
2368  * @return array
2369  */
2370 function forum_get_firstpost_from_discussion($discussionid) {
2371     global $CFG, $DB;
2373     return $DB->get_record_sql("SELECT p.*
2374                              FROM {forum_discussions} d,
2375                                   {forum_posts} p
2376                             WHERE d.id = ?
2377                               AND d.firstpost = p.id ", array($discussionid));
2380 /**
2381  * Returns an array of counts of replies to each discussion
2382  *
2383  * @global object
2384  * @global object
2385  * @param int $forumid
2386  * @param string $forumsort
2387  * @param int $limit
2388  * @param int $page
2389  * @param int $perpage
2390  * @return array
2391  */
2392 function forum_count_discussion_replies($forumid, $forumsort="", $limit=-1, $page=-1, $perpage=0) {
2393     global $CFG, $DB;
2395     if ($limit > 0) {
2396         $limitfrom = 0;
2397         $limitnum  = $limit;
2398     } else if ($page != -1) {
2399         $limitfrom = $page*$perpage;
2400         $limitnum  = $perpage;
2401     } else {
2402         $limitfrom = 0;
2403         $limitnum  = 0;
2404     }
2406     if ($forumsort == "") {
2407         $orderby = "";
2408         $groupby = "";
2410     } else {
2411         $orderby = "ORDER BY $forumsort";
2412         $groupby = ", ".strtolower($forumsort);
2413         $groupby = str_replace('desc', '', $groupby);
2414         $groupby = str_replace('asc', '', $groupby);
2415     }
2417     if (($limitfrom == 0 and $limitnum == 0) or $forumsort == "") {
2418         $sql = "SELECT p.discussion, COUNT(p.id) AS replies, MAX(p.id) AS lastpostid
2419                   FROM {forum_posts} p
2420                        JOIN {forum_discussions} d ON p.discussion = d.id
2421                  WHERE p.parent > 0 AND d.forum = ?
2422               GROUP BY p.discussion";
2423         return $DB->get_records_sql($sql, array($forumid));
2425     } else {
2426         $sql = "SELECT p.discussion, (COUNT(p.id) - 1) AS replies, MAX(p.id) AS lastpostid
2427                   FROM {forum_posts} p
2428                        JOIN {forum_discussions} d ON p.discussion = d.id
2429                  WHERE d.forum = ?
2430               GROUP BY p.discussion $groupby
2431               $orderby";
2432         return $DB->get_records_sql("SELECT * FROM ($sql) sq", array($forumid), $limitfrom, $limitnum);
2433     }
2436 /**
2437  * @global object
2438  * @global object
2439  * @global object
2440  * @staticvar array $cache
2441  * @param object $forum
2442  * @param object $cm
2443  * @param object $course
2444  * @return mixed
2445  */
2446 function forum_count_discussions($forum, $cm, $course) {
2447     global $CFG, $DB, $USER;
2449     static $cache = array();
2451     $now = round(time(), -2); // db cache friendliness
2453     $params = array($course->id);
2455     if (!isset($cache[$course->id])) {
2456         if (!empty($CFG->forum_enabletimedposts)) {
2457             $timedsql = "AND d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?)";
2458             $params[] = $now;
2459             $params[] = $now;
2460         } else {
2461             $timedsql = "";
2462         }
2464         $sql = "SELECT f.id, COUNT(d.id) as dcount
2465                   FROM {forum} f
2466                        JOIN {forum_discussions} d ON d.forum = f.id
2467                  WHERE f.course = ?
2468                        $timedsql
2469               GROUP BY f.id";
2471         if ($counts = $DB->get_records_sql($sql, $params)) {
2472             foreach ($counts as $count) {
2473                 $counts[$count->id] = $count->dcount;
2474             }
2475             $cache[$course->id] = $counts;
2476         } else {
2477             $cache[$course->id] = array();
2478         }
2479     }
2481     if (empty($cache[$course->id][$forum->id])) {
2482         return 0;
2483     }
2485     $groupmode = groups_get_activity_groupmode($cm, $course);
2487     if ($groupmode != SEPARATEGROUPS) {
2488         return $cache[$course->id][$forum->id];
2489     }
2491     if (has_capability('moodle/site:accessallgroups', context_module::instance($cm->id))) {
2492         return $cache[$course->id][$forum->id];
2493     }
2495     require_once($CFG->dirroot.'/course/lib.php');
2497     $modinfo = get_fast_modinfo($course);
2498     if (is_null($modinfo->groups)) {
2499         $modinfo->groups = groups_get_user_groups($course->id, $USER->id);
2500     }
2502     if (array_key_exists($cm->groupingid, $modinfo->groups)) {
2503         $mygroups = $modinfo->groups[$cm->groupingid];
2504     } else {
2505         $mygroups = false; // Will be set below
2506     }
2508     // add all groups posts
2509     if (empty($mygroups)) {
2510         $mygroups = array(-1=>-1);
2511     } else {
2512         $mygroups[-1] = -1;
2513     }
2515     list($mygroups_sql, $params) = $DB->get_in_or_equal($mygroups);
2516     $params[] = $forum->id;
2518     if (!empty($CFG->forum_enabletimedposts)) {
2519         $timedsql = "AND d.timestart < $now AND (d.timeend = 0 OR d.timeend > $now)";
2520         $params[] = $now;
2521         $params[] = $now;
2522     } else {
2523         $timedsql = "";
2524     }
2526     $sql = "SELECT COUNT(d.id)
2527               FROM {forum_discussions} d
2528              WHERE d.groupid $mygroups_sql AND d.forum = ?
2529                    $timedsql";
2531     return $DB->get_field_sql($sql, $params);
2534 /**
2535  * How many posts by other users are unrated by a given user in the given discussion?
2536  *
2537  * TODO: Is this function still used anywhere?
2538  *
2539  * @param int $discussionid
2540  * @param int $userid
2541  * @return mixed
2542  */
2543 function forum_count_unrated_posts($discussionid, $userid) {
2544     global $CFG, $DB;
2546     $sql = "SELECT COUNT(*) as num
2547               FROM {forum_posts}
2548              WHERE parent > 0
2549                AND discussion = :discussionid
2550                AND userid <> :userid";
2551     $params = array('discussionid' => $discussionid, 'userid' => $userid);
2552     $posts = $DB->get_record_sql($sql, $params);
2553     if ($posts) {
2554         $sql = "SELECT count(*) as num
2555                   FROM {forum_posts} p,
2556                        {rating} r
2557                  WHERE p.discussion = :discussionid AND
2558                        p.id = r.itemid AND
2559                        r.userid = userid AND
2560                        r.component = 'mod_forum' AND
2561                        r.ratingarea = 'post'";
2562         $rated = $DB->get_record_sql($sql, $params);
2563         if ($rated) {
2564             if ($posts->num > $rated->num) {
2565                 return $posts->num - $rated->num;
2566             } else {
2567                 return 0;    // Just in case there was a counting error
2568             }
2569         } else {
2570             return $posts->num;
2571         }
2572     } else {
2573         return 0;
2574     }
2577 /**
2578  * Get all discussions in a forum
2579  *
2580  * @global object
2581  * @global object
2582  * @global object
2583  * @uses CONTEXT_MODULE
2584  * @uses VISIBLEGROUPS
2585  * @param object $cm
2586  * @param string $forumsort
2587  * @param bool $fullpost
2588  * @param int $unused
2589  * @param int $limit
2590  * @param bool $userlastmodified
2591  * @param int $page
2592  * @param int $perpage
2593  * @return array
2594  */
2595 function forum_get_discussions($cm, $forumsort="d.timemodified DESC", $fullpost=true, $unused=-1, $limit=-1, $userlastmodified=false, $page=-1, $perpage=0) {
2596     global $CFG, $DB, $USER;
2598     $timelimit = '';
2600     $now = round(time(), -2);
2601     $params = array($cm->instance);
2603     $modcontext = context_module::instance($cm->id);
2605     if (!has_capability('mod/forum:viewdiscussion', $modcontext)) { /// User must have perms to view discussions
2606         return array();
2607     }
2609     if (!empty($CFG->forum_enabletimedposts)) { /// Users must fulfill timed posts
2611         if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
2612             $timelimit = " AND ((d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?))";
2613             $params[] = $now;
2614             $params[] = $now;
2615             if (isloggedin()) {
2616                 $timelimit .= " OR d.userid = ?";
2617                 $params[] = $USER->id;
2618             }
2619             $timelimit .= ")";
2620         }
2621     }
2623     if ($limit > 0) {
2624         $limitfrom = 0;
2625         $limitnum  = $limit;
2626     } else if ($page != -1) {
2627         $limitfrom = $page*$perpage;
2628         $limitnum  = $perpage;
2629     } else {
2630         $limitfrom = 0;
2631         $limitnum  = 0;
2632     }
2634     $groupmode    = groups_get_activity_groupmode($cm);
2635     $currentgroup = groups_get_activity_group($cm);
2637     if ($groupmode) {
2638         if (empty($modcontext)) {
2639             $modcontext = context_module::instance($cm->id);
2640         }
2642         if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
2643             if ($currentgroup) {
2644                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2645                 $params[] = $currentgroup;
2646             } else {
2647                 $groupselect = "";
2648             }
2650         } else {
2651             //seprate groups without access all
2652             if ($currentgroup) {
2653                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2654                 $params[] = $currentgroup;
2655             } else {
2656                 $groupselect = "AND d.groupid = -1";
2657             }
2658         }
2659     } else {
2660         $groupselect = "";
2661     }
2664     if (empty($forumsort)) {
2665         $forumsort = "d.timemodified DESC";
2666     }
2667     if (empty($fullpost)) {
2668         $postdata = "p.id,p.subject,p.modified,p.discussion,p.userid";
2669     } else {
2670         $postdata = "p.*";
2671     }
2673     if (empty($userlastmodified)) {  // We don't need to know this
2674         $umfields = "";
2675         $umtable  = "";
2676     } else {
2677         $umfields = ", um.firstname AS umfirstname, um.lastname AS umlastname";
2678         $umtable  = " LEFT JOIN {user} um ON (d.usermodified = um.id)";
2679     }
2681     $sql = "SELECT $postdata, d.name, d.timemodified, d.usermodified, d.groupid, d.timestart, d.timeend,
2682                    u.firstname, u.lastname, u.email, u.picture, u.imagealt $umfields
2683               FROM {forum_discussions} d
2684                    JOIN {forum_posts} p ON p.discussion = d.id
2685                    JOIN {user} u ON p.userid = u.id
2686                    $umtable
2687              WHERE d.forum = ? AND p.parent = 0
2688                    $timelimit $groupselect
2689           ORDER BY $forumsort";
2690     return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
2693 /**
2694  *
2695  * @global object
2696  * @global object
2697  * @global object
2698  * @uses CONTEXT_MODULE
2699  * @uses VISIBLEGROUPS
2700  * @param object $cm
2701  * @return array
2702  */
2703 function forum_get_discussions_unread($cm) {
2704     global $CFG, $DB, $USER;
2706     $now = round(time(), -2);
2707     $cutoffdate = $now - ($CFG->forum_oldpostdays*24*60*60);
2709     $params = array();
2710     $groupmode    = groups_get_activity_groupmode($cm);
2711     $currentgroup = groups_get_activity_group($cm);
2713     if ($groupmode) {
2714         $modcontext = context_module::instance($cm->id);
2716         if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
2717             if ($currentgroup) {
2718                 $groupselect = "AND (d.groupid = :currentgroup OR d.groupid = -1)";
2719                 $params['currentgroup'] = $currentgroup;
2720             } else {
2721                 $groupselect = "";
2722             }
2724         } else {
2725             //separate groups without access all
2726             if ($currentgroup) {
2727                 $groupselect = "AND (d.groupid = :currentgroup OR d.groupid = -1)";
2728                 $params['currentgroup'] = $currentgroup;
2729             } else {
2730                 $groupselect = "AND d.groupid = -1";
2731             }
2732         }
2733     } else {
2734         $groupselect = "";
2735     }
2737     if (!empty($CFG->forum_enabletimedposts)) {
2738         $timedsql = "AND d.timestart < :now1 AND (d.timeend = 0 OR d.timeend > :now2)";
2739         $params['now1'] = $now;
2740         $params['now2'] = $now;
2741     } else {
2742         $timedsql = "";
2743     }
2745     $sql = "SELECT d.id, COUNT(p.id) AS unread
2746               FROM {forum_discussions} d
2747                    JOIN {forum_posts} p     ON p.discussion = d.id
2748                    LEFT JOIN {forum_read} r ON (r.postid = p.id AND r.userid = $USER->id)
2749              WHERE d.forum = {$cm->instance}
2750                    AND p.modified >= :cutoffdate AND r.id is NULL
2751                    $groupselect
2752                    $timedsql
2753           GROUP BY d.id";
2754     $params['cutoffdate'] = $cutoffdate;
2756     if ($unreads = $DB->get_records_sql($sql, $params)) {
2757         foreach ($unreads as $unread) {
2758             $unreads[$unread->id] = $unread->unread;
2759         }
2760         return $unreads;
2761     } else {
2762         return array();
2763     }
2766 /**
2767  * @global object
2768  * @global object
2769  * @global object
2770  * @uses CONEXT_MODULE
2771  * @uses VISIBLEGROUPS
2772  * @param object $cm
2773  * @return array
2774  */
2775 function forum_get_discussions_count($cm) {
2776     global $CFG, $DB, $USER;
2778     $now = round(time(), -2);
2779     $params = array($cm->instance);
2780     $groupmode    = groups_get_activity_groupmode($cm);
2781     $currentgroup = groups_get_activity_group($cm);
2783     if ($groupmode) {
2784         $modcontext = context_module::instance($cm->id);
2786         if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
2787             if ($currentgroup) {
2788                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2789                 $params[] = $currentgroup;
2790             } else {
2791                 $groupselect = "";
2792             }
2794         } else {
2795             //seprate groups without access all
2796             if ($currentgroup) {
2797                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2798                 $params[] = $currentgroup;
2799             } else {
2800                 $groupselect = "AND d.groupid = -1";
2801             }
2802         }
2803     } else {
2804         $groupselect = "";
2805     }
2807     $cutoffdate = $now - ($CFG->forum_oldpostdays*24*60*60);
2809     $timelimit = "";
2811     if (!empty($CFG->forum_enabletimedposts)) {
2813         $modcontext = context_module::instance($cm->id);
2815         if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
2816             $timelimit = " AND ((d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?))";
2817             $params[] = $now;
2818             $params[] = $now;
2819             if (isloggedin()) {
2820                 $timelimit .= " OR d.userid = ?";
2821                 $params[] = $USER->id;
2822             }
2823             $timelimit .= ")";
2824         }
2825     }
2827     $sql = "SELECT COUNT(d.id)
2828               FROM {forum_discussions} d
2829                    JOIN {forum_posts} p ON p.discussion = d.id
2830              WHERE d.forum = ? AND p.parent = 0
2831                    $groupselect $timelimit";
2833     return $DB->get_field_sql($sql, $params);
2837 /**
2838  * Get all discussions started by a particular user in a course (or group)
2839  * This function no longer used ...
2840  *
2841  * @todo Remove this function if no longer used
2842  * @global object
2843  * @global object
2844  * @param int $courseid
2845  * @param int $userid
2846  * @param int $groupid
2847  * @return array
2848  */
2849 function forum_get_user_discussions($courseid, $userid, $groupid=0) {
2850     global $CFG, $DB;
2851     $params = array($courseid, $userid);
2852     if ($groupid) {
2853         $groupselect = " AND d.groupid = ? ";
2854         $params[] = $groupid;
2855     } else  {
2856         $groupselect = "";
2857     }
2859     return $DB->get_records_sql("SELECT p.*, d.groupid, u.firstname, u.lastname, u.email, u.picture, u.imagealt,
2860                                    f.type as forumtype, f.name as forumname, f.id as forumid
2861                               FROM {forum_discussions} d,
2862                                    {forum_posts} p,
2863                                    {user} u,
2864                                    {forum} f
2865                              WHERE d.course = ?
2866                                AND p.discussion = d.id
2867                                AND p.parent = 0
2868                                AND p.userid = u.id
2869                                AND u.id = ?
2870                                AND d.forum = f.id $groupselect
2871                           ORDER BY p.created DESC", $params);
2874 /**
2875  * Get the list of potential subscribers to a forum.
2876  *
2877  * @param object $forumcontext the forum context.
2878  * @param integer $groupid the id of a group, or 0 for all groups.
2879  * @param string $fields the list of fields to return for each user. As for get_users_by_capability.
2880  * @param string $sort sort order. As for get_users_by_capability.
2881  * @return array list of users.
2882  */
2883 function forum_get_potential_subscribers($forumcontext, $groupid, $fields, $sort = '') {
2884     global $DB;
2886     // only active enrolled users or everybody on the frontpage
2887     list($esql, $params) = get_enrolled_sql($forumcontext, 'mod/forum:allowforcesubscribe', $groupid, true);
2888     if (!$sort) {
2889         list($sort, $sortparams) = users_order_by_sql('u');
2890         $params = array_merge($params, $sortparams);
2891     }
2893     $sql = "SELECT $fields
2894               FROM {user} u
2895               JOIN ($esql) je ON je.id = u.id
2896           ORDER BY $sort";
2898     return $DB->get_records_sql($sql, $params);
2901 /**
2902  * Returns list of user objects that are subscribed to this forum
2903  *
2904  * @global object
2905  * @global object
2906  * @param object $course the course
2907  * @param forum $forum the forum
2908  * @param integer $groupid group id, or 0 for all.
2909  * @param object $context the forum context, to save re-fetching it where possible.
2910  * @param string $fields requested user fields (with "u." table prefix)
2911  * @return array list of users.
2912  */
2913 function forum_subscribed_users($course, $forum, $groupid=0, $context = null, $fields = null) {
2914     global $CFG, $DB;
2916     if (empty($fields)) {
2917         $fields ="u.id,
2918                   u.username,
2919                   u.firstname,
2920                   u.lastname,
2921                   u.maildisplay,
2922                   u.mailformat,
2923                   u.maildigest,
2924                   u.imagealt,
2925                   u.email,
2926                   u.emailstop,
2927                   u.city,
2928                   u.country,
2929                   u.lastaccess,
2930                   u.lastlogin,
2931                   u.picture,
2932                   u.timezone,
2933                   u.theme,
2934                   u.lang,
2935                   u.trackforums,
2936                   u.mnethostid";
2937     }
2939     if (empty($context)) {
2940         $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id);
2941         $context = context_module::instance($cm->id);
2942     }
2944     if (forum_is_forcesubscribed($forum)) {
2945         $results = forum_get_potential_subscribers($context, $groupid, $fields, "u.email ASC");
2947     } else {
2948         // only active enrolled users or everybody on the frontpage
2949         list($esql, $params) = get_enrolled_sql($context, '', $groupid, true);
2950         $params['forumid'] = $forum->id;
2951         $results = $DB->get_records_sql("SELECT $fields
2952                                            FROM {user} u
2953                                            JOIN ($esql) je ON je.id = u.id
2954                                            JOIN {forum_subscriptions} s ON s.userid = u.id
2955                                           WHERE s.forum = :forumid
2956                                        ORDER BY u.email ASC", $params);
2957     }
2959     // Guest user should never be subscribed to a forum.
2960     unset($results[$CFG->siteguest]);
2962     return $results;
2967 // OTHER FUNCTIONS ///////////////////////////////////////////////////////////
2970 /**
2971  * @global object
2972  * @global object
2973  * @param int $courseid
2974  * @param string $type
2975  */
2976 function forum_get_course_forum($courseid, $type) {
2977 // How to set up special 1-per-course forums
2978     global $CFG, $DB, $OUTPUT, $USER;
2980     if ($forums = $DB->get_records_select("forum", "course = ? AND type = ?", array($courseid, $type), "id ASC")) {
2981         // There should always only be ONE, but with the right combination of
2982         // errors there might be more.  In this case, just return the oldest one (lowest ID).
2983         foreach ($forums as $forum) {
2984             return $forum;   // ie the first one
2985         }
2986     }
2988     // Doesn't exist, so create one now.
2989     $forum = new stdClass();
2990     $forum->course = $courseid;
2991     $forum->type = "$type";
2992     if (!empty($USER->htmleditor)) {
2993         $forum->introformat = $USER->htmleditor;
2994     }
2995     switch ($forum->type) {
2996         case "news":
2997             $forum->name  = get_string("namenews", "forum");
2998             $forum->intro = get_string("intronews", "forum");
2999             $forum->forcesubscribe = FORUM_FORCESUBSCRIBE;
3000             $forum->assessed = 0;
3001             if ($courseid == SITEID) {
3002                 $forum->name  = get_string("sitenews");
3003                 $forum->forcesubscribe = 0;
3004             }
3005             break;
3006         case "social":
3007             $forum->name  = get_string("namesocial", "forum");
3008             $forum->intro = get_string("introsocial", "forum");
3009             $forum->assessed = 0;
3010             $forum->forcesubscribe = 0;
3011             break;
3012         case "blog":
3013             $forum->name = get_string('blogforum', 'forum');
3014             $forum->intro = get_string('introblog', 'forum');
3015             $forum->assessed = 0;
3016             $forum->forcesubscribe = 0;
3017             break;
3018         default:
3019             echo $OUTPUT->notification("That forum type doesn't exist!");
3020             return false;
3021             break;
3022     }
3024     $forum->timemodified = time();
3025     $forum->id = $DB->insert_record("forum", $forum);
3027     if (! $module = $DB->get_record("modules", array("name" => "forum"))) {
3028         echo $OUTPUT->notification("Could not find forum module!!");
3029         return false;
3030     }
3031     $mod = new stdClass();
3032     $mod->course = $courseid;
3033     $mod->module = $module->id;
3034     $mod->instance = $forum->id;
3035     $mod->section = 0;
3036     include_once("$CFG->dirroot/course/lib.php");
3037     if (! $mod->coursemodule = add_course_module($mod) ) {
3038         echo $OUTPUT->notification("Could not add a new course module to the course '" . $courseid . "'");
3039         return false;
3040     }
3041     $sectionid = course_add_cm_to_section($courseid, $mod->coursemodule, 0);
3042     return $DB->get_record("forum", array("id" => "$forum->id"));
3046 /**
3047  * Given the data about a posting, builds up the HTML to display it and
3048  * returns the HTML in a string.  This is designed for sending via HTML email.
3049  *
3050  * @global object
3051  * @param object $course
3052  * @param object $cm
3053  * @param object $forum
3054  * @param object $discussion
3055  * @param object $post
3056  * @param object $userform
3057  * @param object $userto
3058  * @param bool $ownpost
3059  * @param bool $reply
3060  * @param bool $link
3061  * @param bool $rate
3062  * @param string $footer
3063  * @return string
3064  */
3065 function forum_make_mail_post($course, $cm, $forum, $discussion, $post, $userfrom, $userto,
3066                               $ownpost=false, $reply=false, $link=false, $rate=false, $footer="") {
3068     global $CFG, $OUTPUT;
3070     $modcontext = context_module::instance($cm->id);
3072     if (!isset($userto->viewfullnames[$forum->id])) {
3073         $viewfullnames = has_capability('moodle/site:viewfullnames', $modcontext, $userto->id);
3074     } else {
3075         $viewfullnames = $userto->viewfullnames[$forum->id];
3076     }
3078     // add absolute file links
3079     $post->message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php', $modcontext->id, 'mod_forum', 'post', $post->id);
3081     // format the post body
3082     $options = new stdClass();
3083     $options->para = true;
3084     $formattedtext = format_text($post->message, $post->messageformat, $options, $course->id);
3086     $output = '<table border="0" cellpadding="3" cellspacing="0" class="forumpost">';
3088     $output .= '<tr class="header"><td width="35" valign="top" class="picture left">';
3089     $output .= $OUTPUT->user_picture($userfrom, array('courseid'=>$course->id));
3090     $output .= '</td>';
3092     if ($post->parent) {
3093         $output .= '<td class="topic">';
3094     } else {
3095         $output .= '<td class="topic starter">';
3096     }
3097     $output .= '<div class="subject">'.format_string($post->subject).'</div>';
3099     $fullname = fullname($userfrom, $viewfullnames);
3100     $by = new stdClass();
3101     $by->name = '<a href="'.$CFG->wwwroot.'/user/view.php?id='.$userfrom->id.'&amp;course='.$course->id.'">'.$fullname.'</a>';
3102     $by->date = userdate($post->modified, '', $userto->timezone);
3103     $output .= '<div class="author">'.get_string('bynameondate', 'forum', $by).'</div>';
3105     $output .= '</td></tr>';
3107     $output .= '<tr><td class="left side" valign="top">';
3109     if (isset($userfrom->groups)) {
3110         $groups = $userfrom->groups[$forum->id];
3111     } else {
3112         $groups = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid);
3113     }
3115     if ($groups) {
3116         $output .= print_group_picture($groups, $course->id, false, true, true);
3117     } else {
3118         $output .= '&nbsp;';
3119     }
3121     $output .= '</td><td class="content">';
3123     $attachments = forum_print_attachments($post, $cm, 'html');
3124     if ($attachments !== '') {
3125         $output .= '<div class="attachments">';
3126         $output .= $attachments;
3127         $output .= '</div>';
3128     }
3130     $output .= $formattedtext;
3132 // Commands
3133     $commands = array();
3135     if ($post->parent) {
3136         $commands[] = '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.
3137                       $post->discussion.'&amp;parent='.$post->parent.'">'.get_string('parent', 'forum').'</a>';
3138     }
3140     if ($reply) {
3141         $commands[] = '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/post.php?reply='.$post->id.'">'.
3142                       get_string('reply', 'forum').'</a>';
3143     }
3145     $output .= '<div class="commands">';
3146     $output .= implode(' | ', $commands);
3147     $output .= '</div>';
3149 // Context link to post if required
3150     if ($link) {
3151         $output .= '<div class="link">';
3152         $output .= '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'#p'.$post->id.'">'.
3153                      get_string('postincontext', 'forum').'</a>';
3154         $output .= '</div>';
3155     }
3157     if ($footer) {
3158         $output .= '<div class="footer">'.$footer.'</div>';
3159     }
3160     $output .= '</td></tr></table>'."\n\n";
3162     return $output;
3165 /**
3166  * Print a forum post
3167  *
3168  * @global object
3169  * @global object
3170  * @uses FORUM_MODE_THREADED
3171  * @uses PORTFOLIO_FORMAT_PLAINHTML
3172  * @uses PORTFOLIO_FORMAT_FILE
3173  * @uses PORTFOLIO_FORMAT_RICHHTML
3174  * @uses PORTFOLIO_ADD_TEXT_LINK
3175  * @uses CONTEXT_MODULE
3176  * @param object $post The post to print.
3177  * @param object $discussion
3178  * @param object $forum
3179  * @param object $cm
3180  * @param object $course
3181  * @param boolean $ownpost Whether this post belongs to the current user.
3182  * @param boolean $reply Whether to print a 'reply' link at the bottom of the message.
3183  * @param boolean $link Just print a shortened version of the post as a link to the full post.
3184  * @param string $footer Extra stuff to print after the message.
3185  * @param string $highlight Space-separated list of terms to highlight.
3186  * @param int $post_read true, false or -99. If we already know whether this user
3187  *          has read this post, pass that in, otherwise, pass in -99, and this
3188  *          function will work it out.
3189  * @param boolean $dummyifcantsee When forum_user_can_see_post says that
3190  *          the current user can't see this post, if this argument is true
3191  *          (the default) then print a dummy 'you can't see this post' post.
3192  *          If false, don't output anything at all.
3193  * @param bool|null $istracked
3194  * @return void
3195  */
3196 function forum_print_post($post, $discussion, $forum, &$cm, $course, $ownpost=false, $reply=false, $link=false,
3197                           $footer="", $highlight="", $postisread=null, $dummyifcantsee=true, $istracked=null, $return=false) {
3198     global $USER, $CFG, $OUTPUT;
3200     require_once($CFG->libdir . '/filelib.php');
3202     // String cache
3203     static $str;
3205     $modcontext = context_module::instance($cm->id);
3207     $post->course = $course->id;
3208     $post->forum  = $forum->id;
3209     $post->message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php', $modcontext->id, 'mod_forum', 'post', $post->id);
3210     if (!empty($CFG->enableplagiarism)) {
3211         require_once($CFG->libdir.'/plagiarismlib.php');
3212         $post->message .= plagiarism_get_links(array('userid' => $post->userid,
3213             'content' => $post->message,
3214             'cmid' => $cm->id,
3215             'course' => $post->course,
3216             'forum' => $post->forum));
3217     }
3219     // caching
3220     if (!isset($cm->cache)) {
3221         $cm->cache = new stdClass;
3222     }
3224     if (!isset($cm->cache->caps)) {
3225         $cm->cache->caps = array();
3226         $cm->cache->caps['mod/forum:viewdiscussion']   = has_capability('mod/forum:viewdiscussion', $modcontext);
3227         $cm->cache->caps['moodle/site:viewfullnames']  = has_capability('moodle/site:viewfullnames', $modcontext);
3228         $cm->cache->caps['mod/forum:editanypost']      = has_capability('mod/forum:editanypost', $modcontext);
3229         $cm->cache->caps['mod/forum:splitdiscussions'] = has_capability('mod/forum:splitdiscussions', $modcontext);
3230         $cm->cache->caps['mod/forum:deleteownpost']    = has_capability('mod/forum:deleteownpost', $modcontext);
3231         $cm->cache->caps['mod/forum:deleteanypost']    = has_capability('mod/forum:deleteanypost', $modcontext);
3232         $cm->cache->caps['mod/forum:viewanyrating']    = has_capability('mod/forum:viewanyrating', $modcontext);
3233         $cm->cache->caps['mod/forum:exportpost']       = has_capability('mod/forum:exportpost', $modcontext);
3234         $cm->cache->caps['mod/forum:exportownpost']    = has_capability('mod/forum:exportownpost', $modcontext);
3235     }
3237     if (!isset($cm->uservisible)) {
3238         $cm->uservisible = coursemodule_visible_for_user($cm);
3239     }
3241     if ($istracked && is_null($postisread)) {
3242         $postisread = forum_tp_is_post_read($USER->id, $post);
3243     }
3245     if (!forum_user_can_see_post($forum, $discussion, $post, NULL, $cm)) {
3246         $output = '';
3247         if (!$dummyifcantsee) {
3248             if ($return) {
3249                 return $output;
3250             }
3251             echo $output;
3252             return;
3253         }
3254         $output .= html_writer::tag('a', '', array('id'=>'p'.$post->id));
3255         $output .= html_writer::start_tag('div', array('class'=>'forumpost clearfix'));
3256         $output .= html_writer::start_tag('div', array('class'=>'row header'));
3257         $output .= html_writer::tag('div', '', array('class'=>'left picture')); // Picture
3258         if ($post->parent) {
3259             $output .= html_writer::start_tag('div', array('class'=>'topic'));
3260         } else {
3261             $output .= html_writer::start_tag('div', array('class'=>'topic starter'));
3262         }
3263         $output .= html_writer::tag('div', get_string('forumsubjecthidden','forum'), array('class'=>'subject')); // Subject
3264         $output .= html_writer::tag('div', get_string('forumauthorhidden','forum'), array('class'=>'author')); // author
3265         $output .= html_writer::end_tag('div');
3266         $output .= html_writer::end_tag('div'); // row
3267         $output .= html_writer::start_tag('div', array('class'=>'row'));
3268         $output .= html_writer::tag('div', '&nbsp;', array('class'=>'left side')); // Groups
3269         $output .= html_writer::tag('div', get_string('forumbodyhidden','forum'), array('class'=>'content')); // Content
3270         $output .= html_writer::end_tag('div'); // row
3271         $output .= html_writer::end_tag('div'); // forumpost
3273         if ($return) {
3274             return $output;
3275         }
3276         echo $output;
3277         return;
3278     }
3280     if (empty($str)) {
3281         $str = new stdClass;
3282         $str->edit         = get_string('edit', 'forum');
3283         $str->delete       = get_string('delete', 'forum');
3284         $str->reply        = get_string('reply', 'forum');
3285         $str->parent       = get_string('parent', 'forum');
3286         $str->pruneheading = get_string('pruneheading', 'forum');
3287         $str->prune        = get_string('prune', 'forum');
3288         $str->displaymode     = get_user_preferences('forum_displaymode', $CFG->forum_displaymode);
3289         $str->markread     = get_string('markread', 'forum');
3290         $str->markunread   = get_string('markunread', 'forum');
3291     }
3293     $discussionlink = new moodle_url('/mod/forum/discuss.php', array('d'=>$post->discussion));
3295     // Build an object that represents the posting user
3296     $postuser = new stdClass;
3297     $postuser->id        = $post->userid;
3298     $postuser->firstname = $post->firstname;
3299     $postuser->lastname  = $post->lastname;
3300     $postuser->imagealt  = $post->imagealt;
3301     $postuser->picture   = $post->picture;
3302     $postuser->email     = $post->email;
3303     // Some handy things for later on
3304     $postuser->fullname    = fullname($postuser, $cm->cache->caps['moodle/site:viewfullnames']);
3305     $postuser->profilelink = new moodle_url('/user/view.php', array('id'=>$post->userid, 'course'=>$course->id));
3307     // Prepare the groups the posting user belongs to
3308     if (isset($cm->cache->usersgroups)) {
3309         $groups = array();
3310         if (isset($cm->cache->usersgroups[$post->userid])) {
3311             foreach ($cm->cache->usersgroups[$post->userid] as $gid) {
3312                 $groups[$gid] = $cm->cache->groups[$gid];
3313             }
3314         }
3315     } else {
3316         $groups = groups_get_all_groups($course->id, $post->userid, $cm->groupingid);
3317     }
3319     // Prepare the attachements for the post, files then images
3320     list($attachments, $attachedimages) = forum_print_attachments($post, $cm, 'separateimages');
3322     // Determine if we need to shorten this post
3323     $shortenpost = ($link && (strlen(strip_tags($post->message)) > $CFG->forum_longpost));
3326     // Prepare an array of commands
3327     $commands = array();
3329     // SPECIAL CASE: The front page can display a news item post to non-logged in users.
3330     // Don't display the mark read / unread controls in this case.
3331     if ($istracked && $CFG->forum_usermarksread && isloggedin()) {
3332         $url = new moodle_url($discussionlink, array('postid'=>$post->id, 'mark'=>'unread'));
3333         $text = $str->markunread;
3334         if (!$postisread) {
3335             $url->param('mark', 'read');
3336             $text = $str->markread;
3337         }
3338         if ($str->displaymode == FORUM_MODE_THREADED) {
3339             $url->param('parent', $post->parent);
3340         } else {
3341             $url->set_anchor('p'.$post->id);
3342         }
3343         $commands[] = array('url'=>$url, 'text'=>$text);
3344     }
3346     // Zoom in to the parent specifically
3347     if ($post->parent) {
3348         $url = new moodle_url($discussionlink);
3349         if ($str->displaymode == FORUM_MODE_THREADED) {
3350             $url->param('parent', $post->parent);
3351         } else {
3352             $url->set_anchor('p'.$post->parent);
3353         }
3354         $commands[] = array('url'=>$url, 'text'=>$str->parent);
3355     }
3357     // Hack for allow to edit news posts those are not displayed yet until they are displayed
3358     $age = time() - $post->created;
3359     if (!$post->parent && $forum->type == 'news' && $discussion->timestart > time()) {
3360         $age = 0;
3361     }
3363     if ($forum->type == 'single' and $discussion->firstpost == $post->id) {
3364         if (has_capability('moodle/course:manageactivities', $modcontext)) {
3365             // The first post in single simple is the forum description.
3366             $commands[] = array('url'=>new moodle_url('/course/modedit.php', array('update'=>$cm->id, 'sesskey'=>sesskey(), 'return'=>1)), 'text'=>$str->edit);
3367         }
3368     } else if (($ownpost && $age < $CFG->maxeditingtime) || $cm->cache->caps['mod/forum:editanypost']) {
3369         $commands[] = array('url'=>new moodle_url('/mod/forum/post.php', array('edit'=>$post->id)), 'text'=>$str->edit);
3370     }
3372     if ($cm->cache->caps['mod/forum:splitdiscussions'] && $post->parent && $forum->type != 'single') {
3373         $commands[] = array('url'=>new moodle_url('/mod/forum/post.php', array('prune'=>$post->id)), 'text'=>$str->prune, 'title'=>$str->pruneheading);
3374     }
3376     if ($forum->type == 'single' and $discussion->firstpost == $post->id) {
3377         // Do not allow deleting of first post in single simple type.
3378     } else if (($ownpost && $age < $CFG->maxeditingtime && $cm->cache->caps['mod/forum:deleteownpost']) || $cm->cache->caps['mod/forum:deleteanypost']) {
3379         $commands[] = array('url'=>new moodle_url('/mod/forum/post.php', array('delete'=>$post->id)), 'text'=>$str->delete);
3380     }
3382     if ($reply) {
3383         $commands[] = array('url'=>new moodle_url('/mod/forum/post.php#mformforum', array('reply'=>$post->id)), 'text'=>$str->reply);
3384     }
3386     if ($CFG->enableportfolios && ($cm->cache->caps['mod/forum:exportpost'] || ($ownpost && $cm->cache->caps['mod/forum:exportownpost']))) {
3387         $p = array('postid' => $post->id);
3388         require_once($CFG->libdir.'/portfoliolib.php');
3389         $button = new portfolio_add_button();
3390         $button->set_callback_options('forum_portfolio_caller', array('postid' => $post->id), 'mod_forum');
3391         if (empty($attachments)) {
3392             $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML);
3393         } else {
3394             $button->set_formats(PORTFOLIO_FORMAT_RICHHTML);
3395         }
3397         $porfoliohtml = $button->to_html(PORTFOLIO_ADD_TEXT_LINK);
3398         if (!empty($porfoliohtml)) {
3399             $commands[] = $porfoliohtml;
3400         }
3401     }
3402     // Finished building commands
3405     // Begin output
3407     $output  = '';
3409     if ($istracked) {
3410         if ($postisread) {
3411             $forumpostclass = ' read';
3412         } else {
3413             $forumpostclass = ' unread';
3414             $output .= html_writer::tag('a', '', array('name'=>'unread'));
3415         }
3416     } else {
3417         // ignore trackign status if not tracked or tracked param missing
3418         $forumpostclass = '';
3419     }
3421     $topicclass = '';
3422     if (empty($post->parent)) {
3423         $topicclass = ' firstpost starter';
3424     }
3426     $output .= html_writer::tag('a', '', array('id'=>'p'.$post->id));
3427     $output .= html_writer::start_tag('div', array('class'=>'forumpost clearfix'.$forumpostclass.$topicclass));
3428     $output .= html_writer::start_tag('div', array('class'=>'row header clearfix'));
3429     $output .= html_writer::start_tag('div', array('class'=>'left picture'));
3430     $output .= $OUTPUT->user_picture($postuser, array('courseid'=>$course->id));
3431     $output .= html_writer::end_tag('div');
3434     $output .= html_writer::start_tag('div', array('class'=>'topic'.$topicclass));
3436     $postsubject = $post->subject;
3437     if (empty($post->subjectnoformat)) {
3438         $postsubject = format_string($postsubject);
3439     }
3440     $output .= html_writer::tag('div', $postsubject, array('class'=>'subject'));
3442     $by = new stdClass();
3443     $by->name = html_writer::link($postuser->profilelink, $postuser->fullname);
3444     $by->date = userdate($post->modified);
3445     $output .= html_writer::tag('div', get_string('bynameondate', 'forum', $by), array('class'=>'author'));
3447     $output .= html_writer::end_tag('div'); //topic
3448     $output .= html_writer::end_tag('div'); //row
3450     $output .= html_writer::start_tag('div', array('class'=>'row maincontent clearfix'));
3451     $output .= html_writer::start_tag('div', array('class'=>'left'));
3453     $groupoutput = '';
3454     if ($groups) {
3455         $groupoutput = print_group_picture($groups, $course->id, false, true, true);
3456     }
3457     if (empty($groupoutput)) {
3458         $groupoutput = '&nbsp;';
3459     }
3460     $output .= html_writer::tag('div', $groupoutput, array('class'=>'grouppictures'));
3462     $output .= html_writer::end_tag('div'); //left side
3463     $output .= html_writer::start_tag('div', array('class'=>'no-overflow'));
3464     $output .= html_writer::start_tag('div', array('class'=>'content'));
3465     if (!empty($attachments)) {
3466         $output .= html_writer::tag('div', $attachments, array('class'=>'attachments'));
3467     }
3469     $options = new stdClass;
3470     $options->para    = false;
3471     $options->trusted = $post->messagetrust;
3472     $options->context = $modcontext;
3473     if ($shortenpost) {
3474         // Prepare shortened version
3475         $postclass    = 'shortenedpost';
3476         $postcontent  = format_text(forum_shorten_post($post->message), $post->messageformat, $options, $course->id);
3477         $postcontent .= html_writer::link($discussionlink, get_string('readtherest', 'forum'));
3478         $postcontent .= html_writer::tag('div', '('.get_string('numwords', 'moodle', count_words($post->message)).')',
3479             array('class'=>'post-word-count'));
3480     } else {
3481         // Prepare whole post
3482         $postclass    = 'fullpost';
3483         $postcontent  = format_text($post->message, $post->messageformat, $options, $course->id);
3484         if (!empty($highlight)) {
3485             $postcontent = highlight($highlight, $postcontent);
3486         }
3487         if (!empty($forum->displaywordcount)) {
3488             $postcontent .= html_writer::tag('div', get_string('numwords', 'moodle', count_words($post->message)),
3489                 array('class'=>'post-word-count'));
3490         }
3491         $postcontent .= html_writer::tag('div', $attachedimages, array('class'=>'attachedimages'));
3492     }