MDL-30377 forum: do not allow users to view not yet started or expired discussions
[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 if (!defined('FORUM_CRON_USER_CACHE')) {
49     /** Defines how many full user records are cached in forum cron. */
50     define('FORUM_CRON_USER_CACHE', 5000);
51 }
53 /// STANDARD FUNCTIONS ///////////////////////////////////////////////////////////
55 /**
56  * Given an object containing all the necessary data,
57  * (defined by the form in mod_form.php) this function
58  * will create a new instance and return the id number
59  * of the new instance.
60  *
61  * @param stdClass $forum add forum instance
62  * @param mod_forum_mod_form $mform
63  * @return int intance id
64  */
65 function forum_add_instance($forum, $mform = null) {
66     global $CFG, $DB;
68     $forum->timemodified = time();
70     if (empty($forum->assessed)) {
71         $forum->assessed = 0;
72     }
74     if (empty($forum->ratingtime) or empty($forum->assessed)) {
75         $forum->assesstimestart  = 0;
76         $forum->assesstimefinish = 0;
77     }
79     $forum->id = $DB->insert_record('forum', $forum);
80     $modcontext = context_module::instance($forum->coursemodule);
82     if ($forum->type == 'single') {  // Create related discussion.
83         $discussion = new stdClass();
84         $discussion->course        = $forum->course;
85         $discussion->forum         = $forum->id;
86         $discussion->name          = $forum->name;
87         $discussion->assessed      = $forum->assessed;
88         $discussion->message       = $forum->intro;
89         $discussion->messageformat = $forum->introformat;
90         $discussion->messagetrust  = trusttext_trusted(context_course::instance($forum->course));
91         $discussion->mailnow       = false;
92         $discussion->groupid       = -1;
94         $message = '';
96         $discussion->id = forum_add_discussion($discussion, null, $message);
98         if ($mform and $draftid = file_get_submitted_draft_itemid('introeditor')) {
99             // ugly hack - we need to copy the files somehow
100             $discussion = $DB->get_record('forum_discussions', array('id'=>$discussion->id), '*', MUST_EXIST);
101             $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost), '*', MUST_EXIST);
103             $post->message = file_save_draft_area_files($draftid, $modcontext->id, 'mod_forum', 'post', $post->id,
104                     mod_forum_post_form::attachment_options($forum), $post->message);
105             $DB->set_field('forum_posts', 'message', $post->message, array('id'=>$post->id));
106         }
107     }
109     if ($forum->forcesubscribe == FORUM_INITIALSUBSCRIBE) {
110         $users = forum_get_potential_subscribers($modcontext, 0, 'u.id, u.email');
111         foreach ($users as $user) {
112             forum_subscribe($user->id, $forum->id);
113         }
114     }
116     forum_grade_item_update($forum);
118     return $forum->id;
122 /**
123  * Given an object containing all the necessary data,
124  * (defined by the form in mod_form.php) this function
125  * will update an existing instance with new data.
126  *
127  * @global object
128  * @param object $forum forum instance (with magic quotes)
129  * @return bool success
130  */
131 function forum_update_instance($forum, $mform) {
132     global $DB, $OUTPUT, $USER;
134     $forum->timemodified = time();
135     $forum->id           = $forum->instance;
137     if (empty($forum->assessed)) {
138         $forum->assessed = 0;
139     }
141     if (empty($forum->ratingtime) or empty($forum->assessed)) {
142         $forum->assesstimestart  = 0;
143         $forum->assesstimefinish = 0;
144     }
146     $oldforum = $DB->get_record('forum', array('id'=>$forum->id));
148     // MDL-3942 - if the aggregation type or scale (i.e. max grade) changes then recalculate the grades for the entire forum
149     // if  scale changes - do we need to recheck the ratings, if ratings higher than scale how do we want to respond?
150     // 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
151     if (($oldforum->assessed<>$forum->assessed) or ($oldforum->scale<>$forum->scale)) {
152         forum_update_grades($forum); // recalculate grades for the forum
153     }
155     if ($forum->type == 'single') {  // Update related discussion and post.
156         $discussions = $DB->get_records('forum_discussions', array('forum'=>$forum->id), 'timemodified ASC');
157         if (!empty($discussions)) {
158             if (count($discussions) > 1) {
159                 echo $OUTPUT->notification(get_string('warnformorepost', 'forum'));
160             }
161             $discussion = array_pop($discussions);
162         } else {
163             // try to recover by creating initial discussion - MDL-16262
164             $discussion = new stdClass();
165             $discussion->course          = $forum->course;
166             $discussion->forum           = $forum->id;
167             $discussion->name            = $forum->name;
168             $discussion->assessed        = $forum->assessed;
169             $discussion->message         = $forum->intro;
170             $discussion->messageformat   = $forum->introformat;
171             $discussion->messagetrust    = true;
172             $discussion->mailnow         = false;
173             $discussion->groupid         = -1;
175             $message = '';
177             forum_add_discussion($discussion, null, $message);
179             if (! $discussion = $DB->get_record('forum_discussions', array('forum'=>$forum->id))) {
180                 print_error('cannotadd', 'forum');
181             }
182         }
183         if (! $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost))) {
184             print_error('cannotfindfirstpost', 'forum');
185         }
187         $cm         = get_coursemodule_from_instance('forum', $forum->id);
188         $modcontext = context_module::instance($cm->id, MUST_EXIST);
190         if ($mform and $draftid = file_get_submitted_draft_itemid('introeditor')) {
191             // ugly hack - we need to copy the files somehow
192             $discussion = $DB->get_record('forum_discussions', array('id'=>$discussion->id), '*', MUST_EXIST);
193             $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost), '*', MUST_EXIST);
195             $post->message = file_save_draft_area_files($draftid, $modcontext->id, 'mod_forum', 'post', $post->id,
196                     mod_forum_post_form::editor_options(), $post->message);
197         }
199         $post->subject       = $forum->name;
200         $post->message       = $forum->intro;
201         $post->messageformat = $forum->introformat;
202         $post->messagetrust  = trusttext_trusted($modcontext);
203         $post->modified      = $forum->timemodified;
204         $post->userid        = $USER->id;    // MDL-18599, so that current teacher can take ownership of activities
206         $DB->update_record('forum_posts', $post);
207         $discussion->name = $forum->name;
208         $DB->update_record('forum_discussions', $discussion);
209     }
211     $DB->update_record('forum', $forum);
213     forum_grade_item_update($forum);
215     return true;
219 /**
220  * Given an ID of an instance of this module,
221  * this function will permanently delete the instance
222  * and any data that depends on it.
223  *
224  * @global object
225  * @param int $id forum instance id
226  * @return bool success
227  */
228 function forum_delete_instance($id) {
229     global $DB;
231     if (!$forum = $DB->get_record('forum', array('id'=>$id))) {
232         return false;
233     }
234     if (!$cm = get_coursemodule_from_instance('forum', $forum->id)) {
235         return false;
236     }
237     if (!$course = $DB->get_record('course', array('id'=>$cm->course))) {
238         return false;
239     }
241     $context = context_module::instance($cm->id);
243     // now get rid of all files
244     $fs = get_file_storage();
245     $fs->delete_area_files($context->id);
247     $result = true;
249     if ($discussions = $DB->get_records('forum_discussions', array('forum'=>$forum->id))) {
250         foreach ($discussions as $discussion) {
251             if (!forum_delete_discussion($discussion, true, $course, $cm, $forum)) {
252                 $result = false;
253             }
254         }
255     }
257     if (!$DB->delete_records('forum_subscriptions', array('forum'=>$forum->id))) {
258         $result = false;
259     }
261     forum_tp_delete_read_records(-1, -1, -1, $forum->id);
263     if (!$DB->delete_records('forum', array('id'=>$forum->id))) {
264         $result = false;
265     }
267     forum_grade_item_delete($forum);
269     return $result;
273 /**
274  * Indicates API features that the forum supports.
275  *
276  * @uses FEATURE_GROUPS
277  * @uses FEATURE_GROUPINGS
278  * @uses FEATURE_GROUPMEMBERSONLY
279  * @uses FEATURE_MOD_INTRO
280  * @uses FEATURE_COMPLETION_TRACKS_VIEWS
281  * @uses FEATURE_COMPLETION_HAS_RULES
282  * @uses FEATURE_GRADE_HAS_GRADE
283  * @uses FEATURE_GRADE_OUTCOMES
284  * @param string $feature
285  * @return mixed True if yes (some features may use other values)
286  */
287 function forum_supports($feature) {
288     switch($feature) {
289         case FEATURE_GROUPS:                  return true;
290         case FEATURE_GROUPINGS:               return true;
291         case FEATURE_GROUPMEMBERSONLY:        return true;
292         case FEATURE_MOD_INTRO:               return true;
293         case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
294         case FEATURE_COMPLETION_HAS_RULES:    return true;
295         case FEATURE_GRADE_HAS_GRADE:         return true;
296         case FEATURE_GRADE_OUTCOMES:          return true;
297         case FEATURE_RATE:                    return true;
298         case FEATURE_BACKUP_MOODLE2:          return true;
299         case FEATURE_SHOW_DESCRIPTION:        return true;
300         case FEATURE_PLAGIARISM:              return true;
302         default: return null;
303     }
307 /**
308  * Obtains the automatic completion state for this forum based on any conditions
309  * in forum settings.
310  *
311  * @global object
312  * @global object
313  * @param object $course Course
314  * @param object $cm Course-module
315  * @param int $userid User ID
316  * @param bool $type Type of comparison (or/and; can be used as return value if no conditions)
317  * @return bool True if completed, false if not. (If no conditions, then return
318  *   value depends on comparison type)
319  */
320 function forum_get_completion_state($course,$cm,$userid,$type) {
321     global $CFG,$DB;
323     // Get forum details
324     if (!($forum=$DB->get_record('forum',array('id'=>$cm->instance)))) {
325         throw new Exception("Can't find forum {$cm->instance}");
326     }
328     $result=$type; // Default return value
330     $postcountparams=array('userid'=>$userid,'forumid'=>$forum->id);
331     $postcountsql="
332 SELECT
333     COUNT(1)
334 FROM
335     {forum_posts} fp
336     INNER JOIN {forum_discussions} fd ON fp.discussion=fd.id
337 WHERE
338     fp.userid=:userid AND fd.forum=:forumid";
340     if ($forum->completiondiscussions) {
341         $value = $forum->completiondiscussions <=
342                  $DB->count_records('forum_discussions',array('forum'=>$forum->id,'userid'=>$userid));
343         if ($type == COMPLETION_AND) {
344             $result = $result && $value;
345         } else {
346             $result = $result || $value;
347         }
348     }
349     if ($forum->completionreplies) {
350         $value = $forum->completionreplies <=
351                  $DB->get_field_sql( $postcountsql.' AND fp.parent<>0',$postcountparams);
352         if ($type==COMPLETION_AND) {
353             $result = $result && $value;
354         } else {
355             $result = $result || $value;
356         }
357     }
358     if ($forum->completionposts) {
359         $value = $forum->completionposts <= $DB->get_field_sql($postcountsql,$postcountparams);
360         if ($type == COMPLETION_AND) {
361             $result = $result && $value;
362         } else {
363             $result = $result || $value;
364         }
365     }
367     return $result;
370 /**
371  * Create a message-id string to use in the custom headers of forum notification emails
372  *
373  * message-id is used by email clients to identify emails and to nest conversations
374  *
375  * @param int $postid The ID of the forum post we are notifying the user about
376  * @param int $usertoid The ID of the user being notified
377  * @param string $hostname The server's hostname
378  * @return string A unique message-id
379  */
380 function forum_get_email_message_id($postid, $usertoid, $hostname) {
381     return '<'.hash('sha256',$postid.'to'.$usertoid).'@'.$hostname.'>';
384 /**
385  * Removes properties from user record that are not necessary
386  * for sending post notifications.
387  * @param stdClass $user
388  * @return void, $user parameter is modified
389  */
390 function forum_cron_minimise_user_record(stdClass $user) {
392     // We store large amount of users in one huge array,
393     // make sure we do not store info there we do not actually need
394     // in mail generation code or messaging.
396     unset($user->institution);
397     unset($user->department);
398     unset($user->address);
399     unset($user->city);
400     unset($user->url);
401     unset($user->currentlogin);
402     unset($user->description);
403     unset($user->descriptionformat);
406 /**
407  * Function to be run periodically according to the moodle cron
408  * Finds all posts that have yet to be mailed out, and mails them
409  * out to all subscribers
410  *
411  * @global object
412  * @global object
413  * @global object
414  * @uses CONTEXT_MODULE
415  * @uses CONTEXT_COURSE
416  * @uses SITEID
417  * @uses FORMAT_PLAIN
418  * @return void
419  */
420 function forum_cron() {
421     global $CFG, $USER, $DB;
423     $site = get_site();
425     // All users that are subscribed to any post that needs sending,
426     // please increase $CFG->extramemorylimit on large sites that
427     // send notifications to a large number of users.
428     $users = array();
429     $userscount = 0; // Cached user counter - count($users) in PHP is horribly slow!!!
431     // status arrays
432     $mailcount  = array();
433     $errorcount = array();
435     // caches
436     $discussions     = array();
437     $forums          = array();
438     $courses         = array();
439     $coursemodules   = array();
440     $subscribedusers = array();
443     // Posts older than 2 days will not be mailed.  This is to avoid the problem where
444     // cron has not been running for a long time, and then suddenly people are flooded
445     // with mail from the past few weeks or months
446     $timenow   = time();
447     $endtime   = $timenow - $CFG->maxeditingtime;
448     $starttime = $endtime - 48 * 3600;   // Two days earlier
450     if ($posts = forum_get_unmailed_posts($starttime, $endtime, $timenow)) {
451         // Mark them all now as being mailed.  It's unlikely but possible there
452         // might be an error later so that a post is NOT actually mailed out,
453         // but since mail isn't crucial, we can accept this risk.  Doing it now
454         // prevents the risk of duplicated mails, which is a worse problem.
456         if (!forum_mark_old_posts_as_mailed($endtime)) {
457             mtrace('Errors occurred while trying to mark some posts as being mailed.');
458             return false;  // Don't continue trying to mail them, in case we are in a cron loop
459         }
461         // checking post validity, and adding users to loop through later
462         foreach ($posts as $pid => $post) {
464             $discussionid = $post->discussion;
465             if (!isset($discussions[$discussionid])) {
466                 if ($discussion = $DB->get_record('forum_discussions', array('id'=> $post->discussion))) {
467                     $discussions[$discussionid] = $discussion;
468                 } else {
469                     mtrace('Could not find discussion '.$discussionid);
470                     unset($posts[$pid]);
471                     continue;
472                 }
473             }
474             $forumid = $discussions[$discussionid]->forum;
475             if (!isset($forums[$forumid])) {
476                 if ($forum = $DB->get_record('forum', array('id' => $forumid))) {
477                     $forums[$forumid] = $forum;
478                 } else {
479                     mtrace('Could not find forum '.$forumid);
480                     unset($posts[$pid]);
481                     continue;
482                 }
483             }
484             $courseid = $forums[$forumid]->course;
485             if (!isset($courses[$courseid])) {
486                 if ($course = $DB->get_record('course', array('id' => $courseid))) {
487                     $courses[$courseid] = $course;
488                 } else {
489                     mtrace('Could not find course '.$courseid);
490                     unset($posts[$pid]);
491                     continue;
492                 }
493             }
494             if (!isset($coursemodules[$forumid])) {
495                 if ($cm = get_coursemodule_from_instance('forum', $forumid, $courseid)) {
496                     $coursemodules[$forumid] = $cm;
497                 } else {
498                     mtrace('Could not find course module for forum '.$forumid);
499                     unset($posts[$pid]);
500                     continue;
501                 }
502             }
505             // caching subscribed users of each forum
506             if (!isset($subscribedusers[$forumid])) {
507                 $modcontext = context_module::instance($coursemodules[$forumid]->id);
508                 if ($subusers = forum_subscribed_users($courses[$courseid], $forums[$forumid], 0, $modcontext, "u.*")) {
509                     foreach ($subusers as $postuser) {
510                         // this user is subscribed to this forum
511                         $subscribedusers[$forumid][$postuser->id] = $postuser->id;
512                         $userscount++;
513                         if ($userscount > FORUM_CRON_USER_CACHE) {
514                             // Store minimal user info.
515                             $minuser = new stdClass();
516                             $minuser->id = $postuser->id;
517                             $users[$postuser->id] = $minuser;
518                         } else {
519                             // Cache full user record.
520                             forum_cron_minimise_user_record($postuser);
521                             $users[$postuser->id] = $postuser;
522                         }
523                     }
524                     // Release memory.
525                     unset($subusers);
526                     unset($postuser);
527                 }
528             }
530             $mailcount[$pid] = 0;
531             $errorcount[$pid] = 0;
532         }
533     }
535     if ($users && $posts) {
537         $urlinfo = parse_url($CFG->wwwroot);
538         $hostname = $urlinfo['host'];
540         foreach ($users as $userto) {
542             @set_time_limit(120); // terminate if processing of any account takes longer than 2 minutes
544             mtrace('Processing user '.$userto->id);
546             // Init user caches - we keep the cache for one cycle only,
547             // otherwise it could consume too much memory.
548             if (isset($userto->username)) {
549                 $userto = clone($userto);
550             } else {
551                 $userto = $DB->get_record('user', array('id' => $userto->id));
552                 forum_cron_minimise_user_record($userto);
553             }
554             $userto->viewfullnames = array();
555             $userto->canpost       = array();
556             $userto->markposts     = array();
558             // set this so that the capabilities are cached, and environment matches receiving user
559             cron_setup_user($userto);
561             // reset the caches
562             foreach ($coursemodules as $forumid=>$unused) {
563                 $coursemodules[$forumid]->cache       = new stdClass();
564                 $coursemodules[$forumid]->cache->caps = array();
565                 unset($coursemodules[$forumid]->uservisible);
566             }
568             foreach ($posts as $pid => $post) {
570                 // Set up the environment for the post, discussion, forum, course
571                 $discussion = $discussions[$post->discussion];
572                 $forum      = $forums[$discussion->forum];
573                 $course     = $courses[$forum->course];
574                 $cm         =& $coursemodules[$forum->id];
576                 // Do some checks  to see if we can bail out now
577                 // Only active enrolled users are in the list of subscribers
578                 if (!isset($subscribedusers[$forum->id][$userto->id])) {
579                     continue; // user does not subscribe to this forum
580                 }
582                 // Don't send email if the forum is Q&A and the user has not posted
583                 // Initial topics are still mailed
584                 if ($forum->type == 'qanda' && !forum_get_user_posted_time($discussion->id, $userto->id) && $pid != $discussion->firstpost) {
585                     mtrace('Did not email '.$userto->id.' because user has not posted in discussion');
586                     continue;
587                 }
589                 // Get info about the sending user
590                 if (array_key_exists($post->userid, $users)) { // we might know him/her already
591                     $userfrom = $users[$post->userid];
592                     if (!isset($userfrom->idnumber)) {
593                         // Minimalised user info, fetch full record.
594                         $userfrom = $DB->get_record('user', array('id' => $userfrom->id));
595                         forum_cron_minimise_user_record($userfrom);
596                     }
598                 } else if ($userfrom = $DB->get_record('user', array('id' => $post->userid))) {
599                     forum_cron_minimise_user_record($userfrom);
600                     // Fetch only once if possible, we can add it to user list, it will be skipped anyway.
601                     if ($userscount <= FORUM_CRON_USER_CACHE) {
602                         $userscount++;
603                         $users[$userfrom->id] = $userfrom;
604                     }
606                 } else {
607                     mtrace('Could not find user '.$post->userid);
608                     continue;
609                 }
611                 //if we want to check that userto and userfrom are not the same person this is probably the spot to do it
613                 // setup global $COURSE properly - needed for roles and languages
614                 cron_setup_user($userto, $course);
616                 // Fill caches
617                 if (!isset($userto->viewfullnames[$forum->id])) {
618                     $modcontext = context_module::instance($cm->id);
619                     $userto->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $modcontext);
620                 }
621                 if (!isset($userto->canpost[$discussion->id])) {
622                     $modcontext = context_module::instance($cm->id);
623                     $userto->canpost[$discussion->id] = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
624                 }
625                 if (!isset($userfrom->groups[$forum->id])) {
626                     if (!isset($userfrom->groups)) {
627                         $userfrom->groups = array();
628                         if (isset($users[$userfrom->id])) {
629                             $users[$userfrom->id]->groups = array();
630                         }
631                     }
632                     $userfrom->groups[$forum->id] = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid);
633                     if (isset($users[$userfrom->id])) {
634                         $users[$userfrom->id]->groups[$forum->id] = $userfrom->groups[$forum->id];
635                     }
636                 }
638                 // Make sure groups allow this user to see this email
639                 if ($discussion->groupid > 0 and $groupmode = groups_get_activity_groupmode($cm, $course)) {   // Groups are being used
640                     if (!groups_group_exists($discussion->groupid)) { // Can't find group
641                         continue;                           // Be safe and don't send it to anyone
642                     }
644                     if (!groups_is_member($discussion->groupid) and !has_capability('moodle/site:accessallgroups', $modcontext)) {
645                         // do not send posts from other groups when in SEPARATEGROUPS or VISIBLEGROUPS
646                         continue;
647                     }
648                 }
650                 // Make sure we're allowed to see it...
651                 if (!forum_user_can_see_post($forum, $discussion, $post, NULL, $cm)) {
652                     mtrace('user '.$userto->id. ' can not see '.$post->id);
653                     continue;
654                 }
656                 // OK so we need to send the email.
658                 // Does the user want this post in a digest?  If so postpone it for now.
659                 if ($userto->maildigest > 0) {
660                     // This user wants the mails to be in digest form
661                     $queue = new stdClass();
662                     $queue->userid       = $userto->id;
663                     $queue->discussionid = $discussion->id;
664                     $queue->postid       = $post->id;
665                     $queue->timemodified = $post->created;
666                     $DB->insert_record('forum_queue', $queue);
667                     continue;
668                 }
671                 // Prepare to actually send the post now, and build up the content
673                 $cleanforumname = str_replace('"', "'", strip_tags(format_string($forum->name)));
675                 $userfrom->customheaders = array (  // Headers to make emails easier to track
676                            'Precedence: Bulk',
677                            'List-Id: "'.$cleanforumname.'" <moodleforum'.$forum->id.'@'.$hostname.'>',
678                            'List-Help: '.$CFG->wwwroot.'/mod/forum/view.php?f='.$forum->id,
679                            'Message-ID: '.forum_get_email_message_id($post->id, $userto->id, $hostname),
680                            'X-Course-Id: '.$course->id,
681                            'X-Course-Name: '.format_string($course->fullname, true)
682                 );
684                 if ($post->parent) {  // This post is a reply, so add headers for threading (see MDL-22551)
685                     $userfrom->customheaders[] = 'In-Reply-To: '.forum_get_email_message_id($post->parent, $userto->id, $hostname);
686                     $userfrom->customheaders[] = 'References: '.forum_get_email_message_id($post->parent, $userto->id, $hostname);
687                 }
689                 $shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
691                 $postsubject = html_to_text("$shortname: ".format_string($post->subject, true));
692                 $posttext = forum_make_mail_text($course, $cm, $forum, $discussion, $post, $userfrom, $userto);
693                 $posthtml = forum_make_mail_html($course, $cm, $forum, $discussion, $post, $userfrom, $userto);
695                 // Send the post now!
697                 mtrace('Sending ', '');
699                 $eventdata = new stdClass();
700                 $eventdata->component        = 'mod_forum';
701                 $eventdata->name             = 'posts';
702                 $eventdata->userfrom         = $userfrom;
703                 $eventdata->userto           = $userto;
704                 $eventdata->subject          = $postsubject;
705                 $eventdata->fullmessage      = $posttext;
706                 $eventdata->fullmessageformat = FORMAT_PLAIN;
707                 $eventdata->fullmessagehtml  = $posthtml;
708                 $eventdata->notification = 1;
710                 $smallmessagestrings = new stdClass();
711                 $smallmessagestrings->user = fullname($userfrom);
712                 $smallmessagestrings->forumname = "$shortname: ".format_string($forum->name,true).": ".$discussion->name;
713                 $smallmessagestrings->message = $post->message;
714                 //make sure strings are in message recipients language
715                 $eventdata->smallmessage = get_string_manager()->get_string('smallmessage', 'forum', $smallmessagestrings, $userto->lang);
717                 $eventdata->contexturl = "{$CFG->wwwroot}/mod/forum/discuss.php?d={$discussion->id}#p{$post->id}";
718                 $eventdata->contexturlname = $discussion->name;
720                 $mailresult = message_send($eventdata);
721                 if (!$mailresult){
722                     mtrace("Error: mod/forum/lib.php forum_cron(): Could not send out mail for id $post->id to user $userto->id".
723                          " ($userto->email) .. not trying again.");
724                     add_to_log($course->id, 'forum', 'mail error', "discuss.php?d=$discussion->id#p$post->id",
725                                substr(format_string($post->subject,true),0,30), $cm->id, $userto->id);
726                     $errorcount[$post->id]++;
727                 } else {
728                     $mailcount[$post->id]++;
730                 // Mark post as read if forum_usermarksread is set off
731                     if (!$CFG->forum_usermarksread) {
732                         $userto->markposts[$post->id] = $post->id;
733                     }
734                 }
736                 mtrace('post '.$post->id. ': '.$post->subject);
737             }
739             // mark processed posts as read
740             forum_tp_mark_posts_read($userto, $userto->markposts);
741             unset($userto);
742         }
743     }
745     if ($posts) {
746         foreach ($posts as $post) {
747             mtrace($mailcount[$post->id]." users were sent post $post->id, '$post->subject'");
748             if ($errorcount[$post->id]) {
749                 $DB->set_field("forum_posts", "mailed", "2", array("id" => "$post->id"));
750             }
751         }
752     }
754     // release some memory
755     unset($subscribedusers);
756     unset($mailcount);
757     unset($errorcount);
759     cron_setup_user();
761     $sitetimezone = $CFG->timezone;
763     // Now see if there are any digest mails waiting to be sent, and if we should send them
765     mtrace('Starting digest processing...');
767     @set_time_limit(300); // terminate if not able to fetch all digests in 5 minutes
769     if (!isset($CFG->digestmailtimelast)) {    // To catch the first time
770         set_config('digestmailtimelast', 0);
771     }
773     $timenow = time();
774     $digesttime = usergetmidnight($timenow, $sitetimezone) + ($CFG->digestmailtime * 3600);
776     // Delete any really old ones (normally there shouldn't be any)
777     $weekago = $timenow - (7 * 24 * 3600);
778     $DB->delete_records_select('forum_queue', "timemodified < ?", array($weekago));
779     mtrace ('Cleaned old digest records');
781     if ($CFG->digestmailtimelast < $digesttime and $timenow > $digesttime) {
783         mtrace('Sending forum digests: '.userdate($timenow, '', $sitetimezone));
785         $digestposts_rs = $DB->get_recordset_select('forum_queue', "timemodified < ?", array($digesttime));
787         if ($digestposts_rs->valid()) {
789             // We have work to do
790             $usermailcount = 0;
792             //caches - reuse the those filled before too
793             $discussionposts = array();
794             $userdiscussions = array();
796             foreach ($digestposts_rs as $digestpost) {
797                 if (!isset($posts[$digestpost->postid])) {
798                     if ($post = $DB->get_record('forum_posts', array('id' => $digestpost->postid))) {
799                         $posts[$digestpost->postid] = $post;
800                     } else {
801                         continue;
802                     }
803                 }
804                 $discussionid = $digestpost->discussionid;
805                 if (!isset($discussions[$discussionid])) {
806                     if ($discussion = $DB->get_record('forum_discussions', array('id' => $discussionid))) {
807                         $discussions[$discussionid] = $discussion;
808                     } else {
809                         continue;
810                     }
811                 }
812                 $forumid = $discussions[$discussionid]->forum;
813                 if (!isset($forums[$forumid])) {
814                     if ($forum = $DB->get_record('forum', array('id' => $forumid))) {
815                         $forums[$forumid] = $forum;
816                     } else {
817                         continue;
818                     }
819                 }
821                 $courseid = $forums[$forumid]->course;
822                 if (!isset($courses[$courseid])) {
823                     if ($course = $DB->get_record('course', array('id' => $courseid))) {
824                         $courses[$courseid] = $course;
825                     } else {
826                         continue;
827                     }
828                 }
830                 if (!isset($coursemodules[$forumid])) {
831                     if ($cm = get_coursemodule_from_instance('forum', $forumid, $courseid)) {
832                         $coursemodules[$forumid] = $cm;
833                     } else {
834                         continue;
835                     }
836                 }
837                 $userdiscussions[$digestpost->userid][$digestpost->discussionid] = $digestpost->discussionid;
838                 $discussionposts[$digestpost->discussionid][$digestpost->postid] = $digestpost->postid;
839             }
840             $digestposts_rs->close(); /// Finished iteration, let's close the resultset
842             // Data collected, start sending out emails to each user
843             foreach ($userdiscussions as $userid => $thesediscussions) {
845                 @set_time_limit(120); // terminate if processing of any account takes longer than 2 minutes
847                 cron_setup_user();
849                 mtrace(get_string('processingdigest', 'forum', $userid), '... ');
851                 // First of all delete all the queue entries for this user
852                 $DB->delete_records_select('forum_queue', "userid = ? AND timemodified < ?", array($userid, $digesttime));
854                 // Init user caches - we keep the cache for one cycle only,
855                 // otherwise it would unnecessarily consume memory.
856                 if (array_key_exists($userid, $users) and isset($users[$userid]->username)) {
857                     $userto = clone($users[$userid]);
858                 } else {
859                     $userto = $DB->get_record('user', array('id' => $userid));
860                     forum_cron_minimise_user_record($userto);
861                 }
862                 $userto->viewfullnames = array();
863                 $userto->canpost       = array();
864                 $userto->markposts     = array();
866                 // Override the language and timezone of the "current" user, so that
867                 // mail is customised for the receiver.
868                 cron_setup_user($userto);
870                 $postsubject = get_string('digestmailsubject', 'forum', format_string($site->shortname, true));
872                 $headerdata = new stdClass();
873                 $headerdata->sitename = format_string($site->fullname, true);
874                 $headerdata->userprefs = $CFG->wwwroot.'/user/edit.php?id='.$userid.'&amp;course='.$site->id;
876                 $posttext = get_string('digestmailheader', 'forum', $headerdata)."\n\n";
877                 $headerdata->userprefs = '<a target="_blank" href="'.$headerdata->userprefs.'">'.get_string('digestmailprefs', 'forum').'</a>';
879                 $posthtml = "<head>";
880 /*                foreach ($CFG->stylesheets as $stylesheet) {
881                     //TODO: MDL-21120
882                     $posthtml .= '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'" />'."\n";
883                 }*/
884                 $posthtml .= "</head>\n<body id=\"email\">\n";
885                 $posthtml .= '<p>'.get_string('digestmailheader', 'forum', $headerdata).'</p><br /><hr size="1" noshade="noshade" />';
887                 foreach ($thesediscussions as $discussionid) {
889                     @set_time_limit(120);   // to be reset for each post
891                     $discussion = $discussions[$discussionid];
892                     $forum      = $forums[$discussion->forum];
893                     $course     = $courses[$forum->course];
894                     $cm         = $coursemodules[$forum->id];
896                     //override language
897                     cron_setup_user($userto, $course);
899                     // Fill caches
900                     if (!isset($userto->viewfullnames[$forum->id])) {
901                         $modcontext = context_module::instance($cm->id);
902                         $userto->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $modcontext);
903                     }
904                     if (!isset($userto->canpost[$discussion->id])) {
905                         $modcontext = context_module::instance($cm->id);
906                         $userto->canpost[$discussion->id] = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
907                     }
909                     $strforums      = get_string('forums', 'forum');
910                     $canunsubscribe = ! forum_is_forcesubscribed($forum);
911                     $canreply       = $userto->canpost[$discussion->id];
912                     $shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
914                     $posttext .= "\n \n";
915                     $posttext .= '=====================================================================';
916                     $posttext .= "\n \n";
917                     $posttext .= "$shortname -> $strforums -> ".format_string($forum->name,true);
918                     if ($discussion->name != $forum->name) {
919                         $posttext  .= " -> ".format_string($discussion->name,true);
920                     }
921                     $posttext .= "\n";
923                     $posthtml .= "<p><font face=\"sans-serif\">".
924                     "<a target=\"_blank\" href=\"$CFG->wwwroot/course/view.php?id=$course->id\">$shortname</a> -> ".
925                     "<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/index.php?id=$course->id\">$strforums</a> -> ".
926                     "<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/view.php?f=$forum->id\">".format_string($forum->name,true)."</a>";
927                     if ($discussion->name == $forum->name) {
928                         $posthtml .= "</font></p>";
929                     } else {
930                         $posthtml .= " -> <a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/discuss.php?d=$discussion->id\">".format_string($discussion->name,true)."</a></font></p>";
931                     }
932                     $posthtml .= '<p>';
934                     $postsarray = $discussionposts[$discussionid];
935                     sort($postsarray);
937                     foreach ($postsarray as $postid) {
938                         $post = $posts[$postid];
940                         if (array_key_exists($post->userid, $users)) { // we might know him/her already
941                             $userfrom = $users[$post->userid];
942                             if (!isset($userfrom->idnumber)) {
943                                 $userfrom = $DB->get_record('user', array('id' => $userfrom->id));
944                                 forum_cron_minimise_user_record($userfrom);
945                             }
947                         } else if ($userfrom = $DB->get_record('user', array('id' => $post->userid))) {
948                             forum_cron_minimise_user_record($userfrom);
949                             if ($userscount <= FORUM_CRON_USER_CACHE) {
950                                 $userscount++;
951                                 $users[$userfrom->id] = $userfrom;
952                             }
954                         } else {
955                             mtrace('Could not find user '.$post->userid);
956                             continue;
957                         }
959                         if (!isset($userfrom->groups[$forum->id])) {
960                             if (!isset($userfrom->groups)) {
961                                 $userfrom->groups = array();
962                                 if (isset($users[$userfrom->id])) {
963                                     $users[$userfrom->id]->groups = array();
964                                 }
965                             }
966                             $userfrom->groups[$forum->id] = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid);
967                             if (isset($users[$userfrom->id])) {
968                                 $users[$userfrom->id]->groups[$forum->id] = $userfrom->groups[$forum->id];
969                             }
970                         }
972                         $userfrom->customheaders = array ("Precedence: Bulk");
974                         if ($userto->maildigest == 2) {
975                             // Subjects only
976                             $by = new stdClass();
977                             $by->name = fullname($userfrom);
978                             $by->date = userdate($post->modified);
979                             $posttext .= "\n".format_string($post->subject,true).' '.get_string("bynameondate", "forum", $by);
980                             $posttext .= "\n---------------------------------------------------------------------";
982                             $by->name = "<a target=\"_blank\" href=\"$CFG->wwwroot/user/view.php?id=$userfrom->id&amp;course=$course->id\">$by->name</a>";
983                             $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>';
985                         } else {
986                             // The full treatment
987                             $posttext .= forum_make_mail_text($course, $cm, $forum, $discussion, $post, $userfrom, $userto, true);
988                             $posthtml .= forum_make_mail_post($course, $cm, $forum, $discussion, $post, $userfrom, $userto, false, $canreply, true, false);
990                         // Create an array of postid's for this user to mark as read.
991                             if (!$CFG->forum_usermarksread) {
992                                 $userto->markposts[$post->id] = $post->id;
993                             }
994                         }
995                     }
996                     if ($canunsubscribe) {
997                         $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>";
998                     } else {
999                         $posthtml .= "\n<div class='mdl-right'><font size=\"1\">".get_string("everyoneissubscribed", "forum")."</font></div>";
1000                     }
1001                     $posthtml .= '<hr size="1" noshade="noshade" /></p>';
1002                 }
1003                 $posthtml .= '</body>';
1005                 if (empty($userto->mailformat) || $userto->mailformat != 1) {
1006                     // This user DOESN'T want to receive HTML
1007                     $posthtml = '';
1008                 }
1010                 $attachment = $attachname='';
1011                 $usetrueaddress = true;
1012                 // Directly email forum digests rather than sending them via messaging, use the
1013                 // site shortname as 'from name', the noreply address will be used by email_to_user.
1014                 $mailresult = email_to_user($userto, $site->shortname, $postsubject, $posttext, $posthtml, $attachment, $attachname, $usetrueaddress, $CFG->forum_replytouser);
1016                 if (!$mailresult) {
1017                     mtrace("ERROR!");
1018                     echo "Error: mod/forum/cron.php: Could not send out digest mail to user $userto->id ($userto->email)... not trying again.\n";
1019                     add_to_log($course->id, 'forum', 'mail digest error', '', '', $cm->id, $userto->id);
1020                 } else {
1021                     mtrace("success.");
1022                     $usermailcount++;
1024                     // Mark post as read if forum_usermarksread is set off
1025                     forum_tp_mark_posts_read($userto, $userto->markposts);
1026                 }
1027             }
1028         }
1029     /// We have finishied all digest emails, update $CFG->digestmailtimelast
1030         set_config('digestmailtimelast', $timenow);
1031     }
1033     cron_setup_user();
1035     if (!empty($usermailcount)) {
1036         mtrace(get_string('digestsentusers', 'forum', $usermailcount));
1037     }
1039     if (!empty($CFG->forum_lastreadclean)) {
1040         $timenow = time();
1041         if ($CFG->forum_lastreadclean + (24*3600) < $timenow) {
1042             set_config('forum_lastreadclean', $timenow);
1043             mtrace('Removing old forum read tracking info...');
1044             forum_tp_clean_read_records();
1045         }
1046     } else {
1047         set_config('forum_lastreadclean', time());
1048     }
1051     return true;
1054 /**
1055  * Builds and returns the body of the email notification in plain text.
1056  *
1057  * @global object
1058  * @global object
1059  * @uses CONTEXT_MODULE
1060  * @param object $course
1061  * @param object $cm
1062  * @param object $forum
1063  * @param object $discussion
1064  * @param object $post
1065  * @param object $userfrom
1066  * @param object $userto
1067  * @param boolean $bare
1068  * @return string The email body in plain text format.
1069  */
1070 function forum_make_mail_text($course, $cm, $forum, $discussion, $post, $userfrom, $userto, $bare = false) {
1071     global $CFG, $USER;
1073     $modcontext = context_module::instance($cm->id);
1075     if (!isset($userto->viewfullnames[$forum->id])) {
1076         $viewfullnames = has_capability('moodle/site:viewfullnames', $modcontext, $userto->id);
1077     } else {
1078         $viewfullnames = $userto->viewfullnames[$forum->id];
1079     }
1081     if (!isset($userto->canpost[$discussion->id])) {
1082         $canreply = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
1083     } else {
1084         $canreply = $userto->canpost[$discussion->id];
1085     }
1087     $by = New stdClass;
1088     $by->name = fullname($userfrom, $viewfullnames);
1089     $by->date = userdate($post->modified, "", $userto->timezone);
1091     $strbynameondate = get_string('bynameondate', 'forum', $by);
1093     $strforums = get_string('forums', 'forum');
1095     $canunsubscribe = ! forum_is_forcesubscribed($forum);
1097     $posttext = '';
1099     if (!$bare) {
1100         $shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
1101         $posttext  = "$shortname -> $strforums -> ".format_string($forum->name,true);
1103         if ($discussion->name != $forum->name) {
1104             $posttext  .= " -> ".format_string($discussion->name,true);
1105         }
1106     }
1108     // add absolute file links
1109     $post->message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php', $modcontext->id, 'mod_forum', 'post', $post->id);
1111     $posttext .= "\n---------------------------------------------------------------------\n";
1112     $posttext .= format_string($post->subject,true);
1113     if ($bare) {
1114         $posttext .= " ($CFG->wwwroot/mod/forum/discuss.php?d=$discussion->id#p$post->id)";
1115     }
1116     $posttext .= "\n".$strbynameondate."\n";
1117     $posttext .= "---------------------------------------------------------------------\n";
1118     $posttext .= format_text_email($post->message, $post->messageformat);
1119     $posttext .= "\n\n";
1120     $posttext .= forum_print_attachments($post, $cm, "text");
1122     if (!$bare && $canreply) {
1123         $posttext .= "---------------------------------------------------------------------\n";
1124         $posttext .= get_string("postmailinfo", "forum", $shortname)."\n";
1125         $posttext .= "$CFG->wwwroot/mod/forum/post.php?reply=$post->id\n";
1126     }
1127     if (!$bare && $canunsubscribe) {
1128         $posttext .= "\n---------------------------------------------------------------------\n";
1129         $posttext .= get_string("unsubscribe", "forum");
1130         $posttext .= ": $CFG->wwwroot/mod/forum/subscribe.php?id=$forum->id\n";
1131     }
1133     return $posttext;
1136 /**
1137  * Builds and returns the body of the email notification in html format.
1138  *
1139  * @global object
1140  * @param object $course
1141  * @param object $cm
1142  * @param object $forum
1143  * @param object $discussion
1144  * @param object $post
1145  * @param object $userfrom
1146  * @param object $userto
1147  * @return string The email text in HTML format
1148  */
1149 function forum_make_mail_html($course, $cm, $forum, $discussion, $post, $userfrom, $userto) {
1150     global $CFG;
1152     if ($userto->mailformat != 1) {  // Needs to be HTML
1153         return '';
1154     }
1156     if (!isset($userto->canpost[$discussion->id])) {
1157         $canreply = forum_user_can_post($forum, $discussion, $userto, $cm, $course);
1158     } else {
1159         $canreply = $userto->canpost[$discussion->id];
1160     }
1162     $strforums = get_string('forums', 'forum');
1163     $canunsubscribe = ! forum_is_forcesubscribed($forum);
1164     $shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
1166     $posthtml = '<head>';
1167 /*    foreach ($CFG->stylesheets as $stylesheet) {
1168         //TODO: MDL-21120
1169         $posthtml .= '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'" />'."\n";
1170     }*/
1171     $posthtml .= '</head>';
1172     $posthtml .= "\n<body id=\"email\">\n\n";
1174     $posthtml .= '<div class="navbar">'.
1175     '<a target="_blank" href="'.$CFG->wwwroot.'/course/view.php?id='.$course->id.'">'.$shortname.'</a> &raquo; '.
1176     '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/index.php?id='.$course->id.'">'.$strforums.'</a> &raquo; '.
1177     '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/view.php?f='.$forum->id.'">'.format_string($forum->name,true).'</a>';
1178     if ($discussion->name == $forum->name) {
1179         $posthtml .= '</div>';
1180     } else {
1181         $posthtml .= ' &raquo; <a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$discussion->id.'">'.
1182                      format_string($discussion->name,true).'</a></div>';
1183     }
1184     $posthtml .= forum_make_mail_post($course, $cm, $forum, $discussion, $post, $userfrom, $userto, false, $canreply, true, false);
1186     if ($canunsubscribe) {
1187         $posthtml .= '<hr /><div class="mdl-align unsubscribelink">
1188                       <a href="'.$CFG->wwwroot.'/mod/forum/subscribe.php?id='.$forum->id.'">'.get_string('unsubscribe', 'forum').'</a>&nbsp;
1189                       <a href="'.$CFG->wwwroot.'/mod/forum/unsubscribeall.php">'.get_string('unsubscribeall', 'forum').'</a></div>';
1190     }
1192     $posthtml .= '</body>';
1194     return $posthtml;
1198 /**
1199  *
1200  * @param object $course
1201  * @param object $user
1202  * @param object $mod TODO this is not used in this function, refactor
1203  * @param object $forum
1204  * @return object A standard object with 2 variables: info (number of posts for this user) and time (last modified)
1205  */
1206 function forum_user_outline($course, $user, $mod, $forum) {
1207     global $CFG;
1208     require_once("$CFG->libdir/gradelib.php");
1209     $grades = grade_get_grades($course->id, 'mod', 'forum', $forum->id, $user->id);
1210     if (empty($grades->items[0]->grades)) {
1211         $grade = false;
1212     } else {
1213         $grade = reset($grades->items[0]->grades);
1214     }
1216     $count = forum_count_user_posts($forum->id, $user->id);
1218     if ($count && $count->postcount > 0) {
1219         $result = new stdClass();
1220         $result->info = get_string("numposts", "forum", $count->postcount);
1221         $result->time = $count->lastpost;
1222         if ($grade) {
1223             $result->info .= ', ' . get_string('grade') . ': ' . $grade->str_long_grade;
1224         }
1225         return $result;
1226     } else if ($grade) {
1227         $result = new stdClass();
1228         $result->info = get_string('grade') . ': ' . $grade->str_long_grade;
1230         //datesubmitted == time created. dategraded == time modified or time overridden
1231         //if grade was last modified by the user themselves use date graded. Otherwise use date submitted
1232         //TODO: move this copied & pasted code somewhere in the grades API. See MDL-26704
1233         if ($grade->usermodified == $user->id || empty($grade->datesubmitted)) {
1234             $result->time = $grade->dategraded;
1235         } else {
1236             $result->time = $grade->datesubmitted;
1237         }
1239         return $result;
1240     }
1241     return NULL;
1245 /**
1246  * @global object
1247  * @global object
1248  * @param object $coure
1249  * @param object $user
1250  * @param object $mod
1251  * @param object $forum
1252  */
1253 function forum_user_complete($course, $user, $mod, $forum) {
1254     global $CFG,$USER, $OUTPUT;
1255     require_once("$CFG->libdir/gradelib.php");
1257     $grades = grade_get_grades($course->id, 'mod', 'forum', $forum->id, $user->id);
1258     if (!empty($grades->items[0]->grades)) {
1259         $grade = reset($grades->items[0]->grades);
1260         echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade);
1261         if ($grade->str_feedback) {
1262             echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
1263         }
1264     }
1266     if ($posts = forum_get_user_posts($forum->id, $user->id)) {
1268         if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $course->id)) {
1269             print_error('invalidcoursemodule');
1270         }
1271         $discussions = forum_get_user_involved_discussions($forum->id, $user->id);
1273         foreach ($posts as $post) {
1274             if (!isset($discussions[$post->discussion])) {
1275                 continue;
1276             }
1277             $discussion = $discussions[$post->discussion];
1279             forum_print_post($post, $discussion, $forum, $cm, $course, false, false, false);
1280         }
1281     } else {
1282         echo "<p>".get_string("noposts", "forum")."</p>";
1283     }
1291 /**
1292  * @global object
1293  * @global object
1294  * @global object
1295  * @param array $courses
1296  * @param array $htmlarray
1297  */
1298 function forum_print_overview($courses,&$htmlarray) {
1299     global $USER, $CFG, $DB, $SESSION;
1301     if (empty($courses) || !is_array($courses) || count($courses) == 0) {
1302         return array();
1303     }
1305     if (!$forums = get_all_instances_in_courses('forum',$courses)) {
1306         return;
1307     }
1309     // Courses to search for new posts
1310     $coursessqls = array();
1311     $params = array();
1312     foreach ($courses as $course) {
1314         // If the user has never entered into the course all posts are pending
1315         if ($course->lastaccess == 0) {
1316             $coursessqls[] = '(f.course = ?)';
1317             $params[] = $course->id;
1319         // Only posts created after the course last access
1320         } else {
1321             $coursessqls[] = '(f.course = ? AND p.created > ?)';
1322             $params[] = $course->id;
1323             $params[] = $course->lastaccess;
1324         }
1325     }
1326     $params[] = $USER->id;
1327     $coursessql = implode(' OR ', $coursessqls);
1329     $sql = "SELECT f.id, COUNT(*) as count "
1330                 .'FROM {forum} f '
1331                 .'JOIN {forum_discussions} d ON d.forum  = f.id '
1332                 .'JOIN {forum_posts} p ON p.discussion = d.id '
1333                 ."WHERE ($coursessql) "
1334                 .'AND p.userid != ? '
1335                 .'GROUP BY f.id';
1337     if (!$new = $DB->get_records_sql($sql, $params)) {
1338         $new = array(); // avoid warnings
1339     }
1341     // also get all forum tracking stuff ONCE.
1342     $trackingforums = array();
1343     foreach ($forums as $forum) {
1344         if (forum_tp_can_track_forums($forum)) {
1345             $trackingforums[$forum->id] = $forum;
1346         }
1347     }
1349     if (count($trackingforums) > 0) {
1350         $cutoffdate = isset($CFG->forum_oldpostdays) ? (time() - ($CFG->forum_oldpostdays*24*60*60)) : 0;
1351         $sql = 'SELECT d.forum,d.course,COUNT(p.id) AS count '.
1352             ' FROM {forum_posts} p '.
1353             ' JOIN {forum_discussions} d ON p.discussion = d.id '.
1354             ' LEFT JOIN {forum_read} r ON r.postid = p.id AND r.userid = ? WHERE (';
1355         $params = array($USER->id);
1357         foreach ($trackingforums as $track) {
1358             $sql .= '(d.forum = ? AND (d.groupid = -1 OR d.groupid = 0 OR d.groupid = ?)) OR ';
1359             $params[] = $track->id;
1360             if (isset($SESSION->currentgroup[$track->course])) {
1361                 $groupid =  $SESSION->currentgroup[$track->course];
1362             } else {
1363                 // get first groupid
1364                 $groupids = groups_get_all_groups($track->course, $USER->id);
1365                 if ($groupids) {
1366                     reset($groupids);
1367                     $groupid = key($groupids);
1368                     $SESSION->currentgroup[$track->course] = $groupid;
1369                 } else {
1370                     $groupid = 0;
1371                 }
1372                 unset($groupids);
1373             }
1374             $params[] = $groupid;
1375         }
1376         $sql = substr($sql,0,-3); // take off the last OR
1377         $sql .= ') AND p.modified >= ? AND r.id is NULL GROUP BY d.forum,d.course';
1378         $params[] = $cutoffdate;
1380         if (!$unread = $DB->get_records_sql($sql, $params)) {
1381             $unread = array();
1382         }
1383     } else {
1384         $unread = array();
1385     }
1387     if (empty($unread) and empty($new)) {
1388         return;
1389     }
1391     $strforum = get_string('modulename','forum');
1393     foreach ($forums as $forum) {
1394         $str = '';
1395         $count = 0;
1396         $thisunread = 0;
1397         $showunread = false;
1398         // either we have something from logs, or trackposts, or nothing.
1399         if (array_key_exists($forum->id, $new) && !empty($new[$forum->id])) {
1400             $count = $new[$forum->id]->count;
1401         }
1402         if (array_key_exists($forum->id,$unread)) {
1403             $thisunread = $unread[$forum->id]->count;
1404             $showunread = true;
1405         }
1406         if ($count > 0 || $thisunread > 0) {
1407             $str .= '<div class="overview forum"><div class="name">'.$strforum.': <a title="'.$strforum.'" href="'.$CFG->wwwroot.'/mod/forum/view.php?f='.$forum->id.'">'.
1408                 $forum->name.'</a></div>';
1409             $str .= '<div class="info"><span class="postsincelogin">';
1410             $str .= get_string('overviewnumpostssince', 'forum', $count)."</span>";
1411             if (!empty($showunread)) {
1412                 $str .= '<div class="unreadposts">'.get_string('overviewnumunread', 'forum', $thisunread).'</div>';
1413             }
1414             $str .= '</div></div>';
1415         }
1416         if (!empty($str)) {
1417             if (!array_key_exists($forum->course,$htmlarray)) {
1418                 $htmlarray[$forum->course] = array();
1419             }
1420             if (!array_key_exists('forum',$htmlarray[$forum->course])) {
1421                 $htmlarray[$forum->course]['forum'] = ''; // initialize, avoid warnings
1422             }
1423             $htmlarray[$forum->course]['forum'] .= $str;
1424         }
1425     }
1428 /**
1429  * Given a course and a date, prints a summary of all the new
1430  * messages posted in the course since that date
1431  *
1432  * @global object
1433  * @global object
1434  * @global object
1435  * @uses CONTEXT_MODULE
1436  * @uses VISIBLEGROUPS
1437  * @param object $course
1438  * @param bool $viewfullnames capability
1439  * @param int $timestart
1440  * @return bool success
1441  */
1442 function forum_print_recent_activity($course, $viewfullnames, $timestart) {
1443     global $CFG, $USER, $DB, $OUTPUT;
1445     // do not use log table if possible, it may be huge and is expensive to join with other tables
1447     if (!$posts = $DB->get_records_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid,
1448                                               d.timestart, d.timeend, d.userid AS duserid,
1449                                               u.firstname, u.lastname, u.email, u.picture
1450                                          FROM {forum_posts} p
1451                                               JOIN {forum_discussions} d ON d.id = p.discussion
1452                                               JOIN {forum} f             ON f.id = d.forum
1453                                               JOIN {user} u              ON u.id = p.userid
1454                                         WHERE p.created > ? AND f.course = ?
1455                                      ORDER BY p.id ASC", array($timestart, $course->id))) { // order by initial posting date
1456          return false;
1457     }
1459     $modinfo = get_fast_modinfo($course);
1461     $groupmodes = array();
1462     $cms    = array();
1464     $strftimerecent = get_string('strftimerecent');
1466     $printposts = array();
1467     foreach ($posts as $post) {
1468         if (!isset($modinfo->instances['forum'][$post->forum])) {
1469             // not visible
1470             continue;
1471         }
1472         $cm = $modinfo->instances['forum'][$post->forum];
1473         if (!$cm->uservisible) {
1474             continue;
1475         }
1476         $context = context_module::instance($cm->id);
1478         if (!has_capability('mod/forum:viewdiscussion', $context)) {
1479             continue;
1480         }
1482         if (!empty($CFG->forum_enabletimedposts) and $USER->id != $post->duserid
1483           and (($post->timestart > 0 and $post->timestart > time()) or ($post->timeend > 0 and $post->timeend < time()))) {
1484             if (!has_capability('mod/forum:viewhiddentimedposts', $context)) {
1485                 continue;
1486             }
1487         }
1489         $groupmode = groups_get_activity_groupmode($cm, $course);
1491         if ($groupmode) {
1492             if ($post->groupid == -1 or $groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $context)) {
1493                 // oki (Open discussions have groupid -1)
1494             } else {
1495                 // separate mode
1496                 if (isguestuser()) {
1497                     // shortcut
1498                     continue;
1499                 }
1501                 if (is_null($modinfo->groups)) {
1502                     $modinfo->groups = groups_get_user_groups($course->id); // load all my groups and cache it in modinfo
1503                 }
1505                 if (!array_key_exists($post->groupid, $modinfo->groups[0])) {
1506                     continue;
1507                 }
1508             }
1509         }
1511         $printposts[] = $post;
1512     }
1513     unset($posts);
1515     if (!$printposts) {
1516         return false;
1517     }
1519     echo $OUTPUT->heading(get_string('newforumposts', 'forum').':', 3);
1520     echo "\n<ul class='unlist'>\n";
1522     foreach ($printposts as $post) {
1523         $subjectclass = empty($post->parent) ? ' bold' : '';
1525         echo '<li><div class="head">'.
1526                '<div class="date">'.userdate($post->modified, $strftimerecent).'</div>'.
1527                '<div class="name">'.fullname($post, $viewfullnames).'</div>'.
1528              '</div>';
1529         echo '<div class="info'.$subjectclass.'">';
1530         if (empty($post->parent)) {
1531             echo '"<a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'">';
1532         } else {
1533             echo '"<a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'&amp;parent='.$post->parent.'#p'.$post->id.'">';
1534         }
1535         $post->subject = break_up_long_words(format_string($post->subject, true));
1536         echo $post->subject;
1537         echo "</a>\"</div></li>\n";
1538     }
1540     echo "</ul>\n";
1542     return true;
1545 /**
1546  * Return grade for given user or all users.
1547  *
1548  * @global object
1549  * @global object
1550  * @param object $forum
1551  * @param int $userid optional user id, 0 means all users
1552  * @return array array of grades, false if none
1553  */
1554 function forum_get_user_grades($forum, $userid = 0) {
1555     global $CFG;
1557     require_once($CFG->dirroot.'/rating/lib.php');
1559     $ratingoptions = new stdClass;
1560     $ratingoptions->component = 'mod_forum';
1561     $ratingoptions->ratingarea = 'post';
1563     //need these to work backwards to get a context id. Is there a better way to get contextid from a module instance?
1564     $ratingoptions->modulename = 'forum';
1565     $ratingoptions->moduleid   = $forum->id;
1566     $ratingoptions->userid = $userid;
1567     $ratingoptions->aggregationmethod = $forum->assessed;
1568     $ratingoptions->scaleid = $forum->scale;
1569     $ratingoptions->itemtable = 'forum_posts';
1570     $ratingoptions->itemtableusercolumn = 'userid';
1572     $rm = new rating_manager();
1573     return $rm->get_user_grades($ratingoptions);
1576 /**
1577  * Update activity grades
1578  *
1579  * @category grade
1580  * @param object $forum
1581  * @param int $userid specific user only, 0 means all
1582  * @param boolean $nullifnone return null if grade does not exist
1583  * @return void
1584  */
1585 function forum_update_grades($forum, $userid=0, $nullifnone=true) {
1586     global $CFG, $DB;
1587     require_once($CFG->libdir.'/gradelib.php');
1589     if (!$forum->assessed) {
1590         forum_grade_item_update($forum);
1592     } else if ($grades = forum_get_user_grades($forum, $userid)) {
1593         forum_grade_item_update($forum, $grades);
1595     } else if ($userid and $nullifnone) {
1596         $grade = new stdClass();
1597         $grade->userid   = $userid;
1598         $grade->rawgrade = NULL;
1599         forum_grade_item_update($forum, $grade);
1601     } else {
1602         forum_grade_item_update($forum);
1603     }
1606 /**
1607  * Update all grades in gradebook.
1608  * @global object
1609  */
1610 function forum_upgrade_grades() {
1611     global $DB;
1613     $sql = "SELECT COUNT('x')
1614               FROM {forum} f, {course_modules} cm, {modules} m
1615              WHERE m.name='forum' AND m.id=cm.module AND cm.instance=f.id";
1616     $count = $DB->count_records_sql($sql);
1618     $sql = "SELECT f.*, cm.idnumber AS cmidnumber, f.course AS courseid
1619               FROM {forum} f, {course_modules} cm, {modules} m
1620              WHERE m.name='forum' AND m.id=cm.module AND cm.instance=f.id";
1621     $rs = $DB->get_recordset_sql($sql);
1622     if ($rs->valid()) {
1623         $pbar = new progress_bar('forumupgradegrades', 500, true);
1624         $i=0;
1625         foreach ($rs as $forum) {
1626             $i++;
1627             upgrade_set_timeout(60*5); // set up timeout, may also abort execution
1628             forum_update_grades($forum, 0, false);
1629             $pbar->update($i, $count, "Updating Forum grades ($i/$count).");
1630         }
1631     }
1632     $rs->close();
1635 /**
1636  * Create/update grade item for given forum
1637  *
1638  * @category grade
1639  * @uses GRADE_TYPE_NONE
1640  * @uses GRADE_TYPE_VALUE
1641  * @uses GRADE_TYPE_SCALE
1642  * @param stdClass $forum Forum object with extra cmidnumber
1643  * @param mixed $grades Optional array/object of grade(s); 'reset' means reset grades in gradebook
1644  * @return int 0 if ok
1645  */
1646 function forum_grade_item_update($forum, $grades=NULL) {
1647     global $CFG;
1648     if (!function_exists('grade_update')) { //workaround for buggy PHP versions
1649         require_once($CFG->libdir.'/gradelib.php');
1650     }
1652     $params = array('itemname'=>$forum->name, 'idnumber'=>$forum->cmidnumber);
1654     if (!$forum->assessed or $forum->scale == 0) {
1655         $params['gradetype'] = GRADE_TYPE_NONE;
1657     } else if ($forum->scale > 0) {
1658         $params['gradetype'] = GRADE_TYPE_VALUE;
1659         $params['grademax']  = $forum->scale;
1660         $params['grademin']  = 0;
1662     } else if ($forum->scale < 0) {
1663         $params['gradetype'] = GRADE_TYPE_SCALE;
1664         $params['scaleid']   = -$forum->scale;
1665     }
1667     if ($grades  === 'reset') {
1668         $params['reset'] = true;
1669         $grades = NULL;
1670     }
1672     return grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, $grades, $params);
1675 /**
1676  * Delete grade item for given forum
1677  *
1678  * @category grade
1679  * @param stdClass $forum Forum object
1680  * @return grade_item
1681  */
1682 function forum_grade_item_delete($forum) {
1683     global $CFG;
1684     require_once($CFG->libdir.'/gradelib.php');
1686     return grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, NULL, array('deleted'=>1));
1690 /**
1691  * This function returns if a scale is being used by one forum
1692  *
1693  * @global object
1694  * @param int $forumid
1695  * @param int $scaleid negative number
1696  * @return bool
1697  */
1698 function forum_scale_used ($forumid,$scaleid) {
1699     global $DB;
1700     $return = false;
1702     $rec = $DB->get_record("forum",array("id" => "$forumid","scale" => "-$scaleid"));
1704     if (!empty($rec) && !empty($scaleid)) {
1705         $return = true;
1706     }
1708     return $return;
1711 /**
1712  * Checks if scale is being used by any instance of forum
1713  *
1714  * This is used to find out if scale used anywhere
1715  *
1716  * @global object
1717  * @param $scaleid int
1718  * @return boolean True if the scale is used by any forum
1719  */
1720 function forum_scale_used_anywhere($scaleid) {
1721     global $DB;
1722     if ($scaleid and $DB->record_exists('forum', array('scale' => -$scaleid))) {
1723         return true;
1724     } else {
1725         return false;
1726     }
1729 // SQL FUNCTIONS ///////////////////////////////////////////////////////////
1731 /**
1732  * Gets a post with all info ready for forum_print_post
1733  * Most of these joins are just to get the forum id
1734  *
1735  * @global object
1736  * @global object
1737  * @param int $postid
1738  * @return mixed array of posts or false
1739  */
1740 function forum_get_post_full($postid) {
1741     global $CFG, $DB;
1743     return $DB->get_record_sql("SELECT p.*, d.forum, u.firstname, u.lastname, u.email, u.picture, u.imagealt
1744                              FROM {forum_posts} p
1745                                   JOIN {forum_discussions} d ON p.discussion = d.id
1746                                   LEFT JOIN {user} u ON p.userid = u.id
1747                             WHERE p.id = ?", array($postid));
1750 /**
1751  * Gets posts with all info ready for forum_print_post
1752  * We pass forumid in because we always know it so no need to make a
1753  * complicated join to find it out.
1754  *
1755  * @global object
1756  * @global object
1757  * @return mixed array of posts or false
1758  */
1759 function forum_get_discussion_posts($discussion, $sort, $forumid) {
1760     global $CFG, $DB;
1762     return $DB->get_records_sql("SELECT p.*, $forumid AS forum, u.firstname, u.lastname, u.email, u.picture, u.imagealt
1763                               FROM {forum_posts} p
1764                          LEFT JOIN {user} u ON p.userid = u.id
1765                              WHERE p.discussion = ?
1766                                AND p.parent > 0 $sort", array($discussion));
1769 /**
1770  * Gets all posts in discussion including top parent.
1771  *
1772  * @global object
1773  * @global object
1774  * @global object
1775  * @param int $discussionid
1776  * @param string $sort
1777  * @param bool $tracking does user track the forum?
1778  * @return array of posts
1779  */
1780 function forum_get_all_discussion_posts($discussionid, $sort, $tracking=false) {
1781     global $CFG, $DB, $USER;
1783     $tr_sel  = "";
1784     $tr_join = "";
1785     $params = array();
1787     if ($tracking) {
1788         $now = time();
1789         $cutoffdate = $now - ($CFG->forum_oldpostdays * 24 * 3600);
1790         $tr_sel  = ", fr.id AS postread";
1791         $tr_join = "LEFT JOIN {forum_read} fr ON (fr.postid = p.id AND fr.userid = ?)";
1792         $params[] = $USER->id;
1793     }
1795     $params[] = $discussionid;
1796     if (!$posts = $DB->get_records_sql("SELECT p.*, u.firstname, u.lastname, u.email, u.picture, u.imagealt $tr_sel
1797                                      FROM {forum_posts} p
1798                                           LEFT JOIN {user} u ON p.userid = u.id
1799                                           $tr_join
1800                                     WHERE p.discussion = ?
1801                                  ORDER BY $sort", $params)) {
1802         return array();
1803     }
1805     foreach ($posts as $pid=>$p) {
1806         if ($tracking) {
1807             if (forum_tp_is_post_old($p)) {
1808                  $posts[$pid]->postread = true;
1809             }
1810         }
1811         if (!$p->parent) {
1812             continue;
1813         }
1814         if (!isset($posts[$p->parent])) {
1815             continue; // parent does not exist??
1816         }
1817         if (!isset($posts[$p->parent]->children)) {
1818             $posts[$p->parent]->children = array();
1819         }
1820         $posts[$p->parent]->children[$pid] =& $posts[$pid];
1821     }
1823     return $posts;
1826 /**
1827  * Gets posts with all info ready for forum_print_post
1828  * We pass forumid in because we always know it so no need to make a
1829  * complicated join to find it out.
1830  *
1831  * @global object
1832  * @global object
1833  * @param int $parent
1834  * @param int $forumid
1835  * @return array
1836  */
1837 function forum_get_child_posts($parent, $forumid) {
1838     global $CFG, $DB;
1840     return $DB->get_records_sql("SELECT p.*, $forumid AS forum, u.firstname, u.lastname, u.email, u.picture, u.imagealt
1841                               FROM {forum_posts} p
1842                          LEFT JOIN {user} u ON p.userid = u.id
1843                              WHERE p.parent = ?
1844                           ORDER BY p.created ASC", array($parent));
1847 /**
1848  * An array of forum objects that the user is allowed to read/search through.
1849  *
1850  * @global object
1851  * @global object
1852  * @global object
1853  * @param int $userid
1854  * @param int $courseid if 0, we look for forums throughout the whole site.
1855  * @return array of forum objects, or false if no matches
1856  *         Forum objects have the following attributes:
1857  *         id, type, course, cmid, cmvisible, cmgroupmode, accessallgroups,
1858  *         viewhiddentimedposts
1859  */
1860 function forum_get_readable_forums($userid, $courseid=0) {
1862     global $CFG, $DB, $USER;
1863     require_once($CFG->dirroot.'/course/lib.php');
1865     if (!$forummod = $DB->get_record('modules', array('name' => 'forum'))) {
1866         print_error('notinstalled', 'forum');
1867     }
1869     if ($courseid) {
1870         $courses = $DB->get_records('course', array('id' => $courseid));
1871     } else {
1872         // If no course is specified, then the user can see SITE + his courses.
1873         $courses1 = $DB->get_records('course', array('id' => SITEID));
1874         $courses2 = enrol_get_users_courses($userid, true, array('modinfo'));
1875         $courses = array_merge($courses1, $courses2);
1876     }
1877     if (!$courses) {
1878         return array();
1879     }
1881     $readableforums = array();
1883     foreach ($courses as $course) {
1885         $modinfo = get_fast_modinfo($course);
1886         if (is_null($modinfo->groups)) {
1887             $modinfo->groups = groups_get_user_groups($course->id, $userid);
1888         }
1890         if (empty($modinfo->instances['forum'])) {
1891             // hmm, no forums?
1892             continue;
1893         }
1895         $courseforums = $DB->get_records('forum', array('course' => $course->id));
1897         foreach ($modinfo->instances['forum'] as $forumid => $cm) {
1898             if (!$cm->uservisible or !isset($courseforums[$forumid])) {
1899                 continue;
1900             }
1901             $context = context_module::instance($cm->id);
1902             $forum = $courseforums[$forumid];
1903             $forum->context = $context;
1904             $forum->cm = $cm;
1906             if (!has_capability('mod/forum:viewdiscussion', $context)) {
1907                 continue;
1908             }
1910          /// group access
1911             if (groups_get_activity_groupmode($cm, $course) == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) {
1912                 if (is_null($modinfo->groups)) {
1913                     $modinfo->groups = groups_get_user_groups($course->id, $USER->id);
1914                 }
1915                 if (isset($modinfo->groups[$cm->groupingid])) {
1916                     $forum->onlygroups = $modinfo->groups[$cm->groupingid];
1917                     $forum->onlygroups[] = -1;
1918                 } else {
1919                     $forum->onlygroups = array(-1);
1920                 }
1921             }
1923         /// hidden timed discussions
1924             $forum->viewhiddentimedposts = true;
1925             if (!empty($CFG->forum_enabletimedposts)) {
1926                 if (!has_capability('mod/forum:viewhiddentimedposts', $context)) {
1927                     $forum->viewhiddentimedposts = false;
1928                 }
1929             }
1931         /// qanda access
1932             if ($forum->type == 'qanda'
1933                     && !has_capability('mod/forum:viewqandawithoutposting', $context)) {
1935                 // We need to check whether the user has posted in the qanda forum.
1936                 $forum->onlydiscussions = array();  // Holds discussion ids for the discussions
1937                                                     // the user is allowed to see in this forum.
1938                 if ($discussionspostedin = forum_discussions_user_has_posted_in($forum->id, $USER->id)) {
1939                     foreach ($discussionspostedin as $d) {
1940                         $forum->onlydiscussions[] = $d->id;
1941                     }
1942                 }
1943             }
1945             $readableforums[$forum->id] = $forum;
1946         }
1948         unset($modinfo);
1950     } // End foreach $courses
1952     return $readableforums;
1955 /**
1956  * Returns a list of posts found using an array of search terms.
1957  *
1958  * @global object
1959  * @global object
1960  * @global object
1961  * @param array $searchterms array of search terms, e.g. word +word -word
1962  * @param int $courseid if 0, we search through the whole site
1963  * @param int $limitfrom
1964  * @param int $limitnum
1965  * @param int &$totalcount
1966  * @param string $extrasql
1967  * @return array|bool Array of posts found or false
1968  */
1969 function forum_search_posts($searchterms, $courseid=0, $limitfrom=0, $limitnum=50,
1970                             &$totalcount, $extrasql='') {
1971     global $CFG, $DB, $USER;
1972     require_once($CFG->libdir.'/searchlib.php');
1974     $forums = forum_get_readable_forums($USER->id, $courseid);
1976     if (count($forums) == 0) {
1977         $totalcount = 0;
1978         return false;
1979     }
1981     $now = round(time(), -2); // db friendly
1983     $fullaccess = array();
1984     $where = array();
1985     $params = array();
1987     foreach ($forums as $forumid => $forum) {
1988         $select = array();
1990         if (!$forum->viewhiddentimedposts) {
1991             $select[] = "(d.userid = :userid{$forumid} OR (d.timestart < :timestart{$forumid} AND (d.timeend = 0 OR d.timeend > :timeend{$forumid})))";
1992             $params = array_merge($params, array('userid'.$forumid=>$USER->id, 'timestart'.$forumid=>$now, 'timeend'.$forumid=>$now));
1993         }
1995         $cm = $forum->cm;
1996         $context = $forum->context;
1998         if ($forum->type == 'qanda'
1999             && !has_capability('mod/forum:viewqandawithoutposting', $context)) {
2000             if (!empty($forum->onlydiscussions)) {
2001                 list($discussionid_sql, $discussionid_params) = $DB->get_in_or_equal($forum->onlydiscussions, SQL_PARAMS_NAMED, 'qanda'.$forumid.'_');
2002                 $params = array_merge($params, $discussionid_params);
2003                 $select[] = "(d.id $discussionid_sql OR p.parent = 0)";
2004             } else {
2005                 $select[] = "p.parent = 0";
2006             }
2007         }
2009         if (!empty($forum->onlygroups)) {
2010             list($groupid_sql, $groupid_params) = $DB->get_in_or_equal($forum->onlygroups, SQL_PARAMS_NAMED, 'grps'.$forumid.'_');
2011             $params = array_merge($params, $groupid_params);
2012             $select[] = "d.groupid $groupid_sql";
2013         }
2015         if ($select) {
2016             $selects = implode(" AND ", $select);
2017             $where[] = "(d.forum = :forum{$forumid} AND $selects)";
2018             $params['forum'.$forumid] = $forumid;
2019         } else {
2020             $fullaccess[] = $forumid;
2021         }
2022     }
2024     if ($fullaccess) {
2025         list($fullid_sql, $fullid_params) = $DB->get_in_or_equal($fullaccess, SQL_PARAMS_NAMED, 'fula');
2026         $params = array_merge($params, $fullid_params);
2027         $where[] = "(d.forum $fullid_sql)";
2028     }
2030     $selectdiscussion = "(".implode(" OR ", $where).")";
2032     $messagesearch = '';
2033     $searchstring = '';
2035     // Need to concat these back together for parser to work.
2036     foreach($searchterms as $searchterm){
2037         if ($searchstring != '') {
2038             $searchstring .= ' ';
2039         }
2040         $searchstring .= $searchterm;
2041     }
2043     // We need to allow quoted strings for the search. The quotes *should* be stripped
2044     // by the parser, but this should be examined carefully for security implications.
2045     $searchstring = str_replace("\\\"","\"",$searchstring);
2046     $parser = new search_parser();
2047     $lexer = new search_lexer($parser);
2049     if ($lexer->parse($searchstring)) {
2050         $parsearray = $parser->get_parsed_array();
2051     // Experimental feature under 1.8! MDL-8830
2052     // Use alternative text searches if defined
2053     // This feature only works under mysql until properly implemented for other DBs
2054     // Requires manual creation of text index for forum_posts before enabling it:
2055     // CREATE FULLTEXT INDEX foru_post_tix ON [prefix]forum_posts (subject, message)
2056     // Experimental feature under 1.8! MDL-8830
2057         if (!empty($CFG->forum_usetextsearches)) {
2058             list($messagesearch, $msparams) = search_generate_text_SQL($parsearray, 'p.message', 'p.subject',
2059                                                  'p.userid', 'u.id', 'u.firstname',
2060                                                  'u.lastname', 'p.modified', 'd.forum');
2061         } else {
2062             list($messagesearch, $msparams) = search_generate_SQL($parsearray, 'p.message', 'p.subject',
2063                                                  'p.userid', 'u.id', 'u.firstname',
2064                                                  'u.lastname', 'p.modified', 'd.forum');
2065         }
2066         $params = array_merge($params, $msparams);
2067     }
2069     $fromsql = "{forum_posts} p,
2070                   {forum_discussions} d,
2071                   {user} u";
2073     $selectsql = " $messagesearch
2074                AND p.discussion = d.id
2075                AND p.userid = u.id
2076                AND $selectdiscussion
2077                    $extrasql";
2079     $countsql = "SELECT COUNT(*)
2080                    FROM $fromsql
2081                   WHERE $selectsql";
2083     $searchsql = "SELECT p.*,
2084                          d.forum,
2085                          u.firstname,
2086                          u.lastname,
2087                          u.email,
2088                          u.picture,
2089                          u.imagealt
2090                     FROM $fromsql
2091                    WHERE $selectsql
2092                 ORDER BY p.modified DESC";
2094     $totalcount = $DB->count_records_sql($countsql, $params);
2096     return $DB->get_records_sql($searchsql, $params, $limitfrom, $limitnum);
2099 /**
2100  * Returns a list of ratings for a particular post - sorted.
2101  *
2102  * TODO: Check if this function is actually used anywhere.
2103  * Up until the fix for MDL-27471 this function wasn't even returning.
2104  *
2105  * @param stdClass $context
2106  * @param int $postid
2107  * @param string $sort
2108  * @return array Array of ratings or false
2109  */
2110 function forum_get_ratings($context, $postid, $sort = "u.firstname ASC") {
2111     $options = new stdClass;
2112     $options->context = $context;
2113     $options->component = 'mod_forum';
2114     $options->ratingarea = 'post';
2115     $options->itemid = $postid;
2116     $options->sort = "ORDER BY $sort";
2118     $rm = new rating_manager();
2119     return $rm->get_all_ratings_for_item($options);
2122 /**
2123  * Returns a list of all new posts that have not been mailed yet
2124  *
2125  * @param int $starttime posts created after this time
2126  * @param int $endtime posts created before this
2127  * @param int $now used for timed discussions only
2128  * @return array
2129  */
2130 function forum_get_unmailed_posts($starttime, $endtime, $now=null) {
2131     global $CFG, $DB;
2133     $params = array($starttime, $endtime);
2134     if (!empty($CFG->forum_enabletimedposts)) {
2135         if (empty($now)) {
2136             $now = time();
2137         }
2138         $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2139         $params[] = $now;
2140         $params[] = $now;
2141     } else {
2142         $timedsql = "";
2143     }
2145     return $DB->get_records_sql("SELECT p.*, d.course, d.forum
2146                               FROM {forum_posts} p
2147                                    JOIN {forum_discussions} d ON d.id = p.discussion
2148                              WHERE p.mailed = 0
2149                                    AND p.created >= ?
2150                                    AND (p.created < ? OR p.mailnow = 1)
2151                                    $timedsql
2152                           ORDER BY p.modified ASC", $params);
2155 /**
2156  * Marks posts before a certain time as being mailed already
2157  *
2158  * @global object
2159  * @global object
2160  * @param int $endtime
2161  * @param int $now Defaults to time()
2162  * @return bool
2163  */
2164 function forum_mark_old_posts_as_mailed($endtime, $now=null) {
2165     global $CFG, $DB;
2166     if (empty($now)) {
2167         $now = time();
2168     }
2170     if (empty($CFG->forum_enabletimedposts)) {
2171         return $DB->execute("UPDATE {forum_posts}
2172                                SET mailed = '1'
2173                              WHERE (created < ? OR mailnow = 1)
2174                                    AND mailed = 0", array($endtime));
2176     } else {
2177         return $DB->execute("UPDATE {forum_posts}
2178                                SET mailed = '1'
2179                              WHERE discussion NOT IN (SELECT d.id
2180                                                         FROM {forum_discussions} d
2181                                                        WHERE d.timestart > ?)
2182                                    AND (created < ? OR mailnow = 1)
2183                                    AND mailed = 0", array($now, $endtime));
2184     }
2187 /**
2188  * Get all the posts for a user in a forum suitable for forum_print_post
2189  *
2190  * @global object
2191  * @global object
2192  * @uses CONTEXT_MODULE
2193  * @return array
2194  */
2195 function forum_get_user_posts($forumid, $userid) {
2196     global $CFG, $DB;
2198     $timedsql = "";
2199     $params = array($forumid, $userid);
2201     if (!empty($CFG->forum_enabletimedposts)) {
2202         $cm = get_coursemodule_from_instance('forum', $forumid);
2203         if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
2204             $now = time();
2205             $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2206             $params[] = $now;
2207             $params[] = $now;
2208         }
2209     }
2211     return $DB->get_records_sql("SELECT p.*, d.forum, u.firstname, u.lastname, u.email, u.picture, u.imagealt
2212                               FROM {forum} f
2213                                    JOIN {forum_discussions} d ON d.forum = f.id
2214                                    JOIN {forum_posts} p       ON p.discussion = d.id
2215                                    JOIN {user} u              ON u.id = p.userid
2216                              WHERE f.id = ?
2217                                    AND p.userid = ?
2218                                    $timedsql
2219                           ORDER BY p.modified ASC", $params);
2222 /**
2223  * Get all the discussions user participated in
2224  *
2225  * @global object
2226  * @global object
2227  * @uses CONTEXT_MODULE
2228  * @param int $forumid
2229  * @param int $userid
2230  * @return array Array or false
2231  */
2232 function forum_get_user_involved_discussions($forumid, $userid) {
2233     global $CFG, $DB;
2235     $timedsql = "";
2236     $params = array($forumid, $userid);
2237     if (!empty($CFG->forum_enabletimedposts)) {
2238         $cm = get_coursemodule_from_instance('forum', $forumid);
2239         if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
2240             $now = time();
2241             $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2242             $params[] = $now;
2243             $params[] = $now;
2244         }
2245     }
2247     return $DB->get_records_sql("SELECT DISTINCT d.*
2248                               FROM {forum} f
2249                                    JOIN {forum_discussions} d ON d.forum = f.id
2250                                    JOIN {forum_posts} p       ON p.discussion = d.id
2251                              WHERE f.id = ?
2252                                    AND p.userid = ?
2253                                    $timedsql", $params);
2256 /**
2257  * Get all the posts for a user in a forum suitable for forum_print_post
2258  *
2259  * @global object
2260  * @global object
2261  * @param int $forumid
2262  * @param int $userid
2263  * @return array of counts or false
2264  */
2265 function forum_count_user_posts($forumid, $userid) {
2266     global $CFG, $DB;
2268     $timedsql = "";
2269     $params = array($forumid, $userid);
2270     if (!empty($CFG->forum_enabletimedposts)) {
2271         $cm = get_coursemodule_from_instance('forum', $forumid);
2272         if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
2273             $now = time();
2274             $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2275             $params[] = $now;
2276             $params[] = $now;
2277         }
2278     }
2280     return $DB->get_record_sql("SELECT COUNT(p.id) AS postcount, MAX(p.modified) AS lastpost
2281                              FROM {forum} f
2282                                   JOIN {forum_discussions} d ON d.forum = f.id
2283                                   JOIN {forum_posts} p       ON p.discussion = d.id
2284                                   JOIN {user} u              ON u.id = p.userid
2285                             WHERE f.id = ?
2286                                   AND p.userid = ?
2287                                   $timedsql", $params);
2290 /**
2291  * Given a log entry, return the forum post details for it.
2292  *
2293  * @global object
2294  * @global object
2295  * @param object $log
2296  * @return array|null
2297  */
2298 function forum_get_post_from_log($log) {
2299     global $CFG, $DB;
2301     if ($log->action == "add post") {
2303         return $DB->get_record_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid,
2304                                            u.firstname, u.lastname, u.email, u.picture
2305                                  FROM {forum_discussions} d,
2306                                       {forum_posts} p,
2307                                       {forum} f,
2308                                       {user} u
2309                                 WHERE p.id = ?
2310                                   AND d.id = p.discussion
2311                                   AND p.userid = u.id
2312                                   AND u.deleted <> '1'
2313                                   AND f.id = d.forum", array($log->info));
2316     } else if ($log->action == "add discussion") {
2318         return $DB->get_record_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid,
2319                                            u.firstname, u.lastname, u.email, u.picture
2320                                  FROM {forum_discussions} d,
2321                                       {forum_posts} p,
2322                                       {forum} f,
2323                                       {user} u
2324                                 WHERE d.id = ?
2325                                   AND d.firstpost = p.id
2326                                   AND p.userid = u.id
2327                                   AND u.deleted <> '1'
2328                                   AND f.id = d.forum", array($log->info));
2329     }
2330     return NULL;
2333 /**
2334  * Given a discussion id, return the first post from the discussion
2335  *
2336  * @global object
2337  * @global object
2338  * @param int $dicsussionid
2339  * @return array
2340  */
2341 function forum_get_firstpost_from_discussion($discussionid) {
2342     global $CFG, $DB;
2344     return $DB->get_record_sql("SELECT p.*
2345                              FROM {forum_discussions} d,
2346                                   {forum_posts} p
2347                             WHERE d.id = ?
2348                               AND d.firstpost = p.id ", array($discussionid));
2351 /**
2352  * Returns an array of counts of replies to each discussion
2353  *
2354  * @global object
2355  * @global object
2356  * @param int $forumid
2357  * @param string $forumsort
2358  * @param int $limit
2359  * @param int $page
2360  * @param int $perpage
2361  * @return array
2362  */
2363 function forum_count_discussion_replies($forumid, $forumsort="", $limit=-1, $page=-1, $perpage=0) {
2364     global $CFG, $DB;
2366     if ($limit > 0) {
2367         $limitfrom = 0;
2368         $limitnum  = $limit;
2369     } else if ($page != -1) {
2370         $limitfrom = $page*$perpage;
2371         $limitnum  = $perpage;
2372     } else {
2373         $limitfrom = 0;
2374         $limitnum  = 0;
2375     }
2377     if ($forumsort == "") {
2378         $orderby = "";
2379         $groupby = "";
2381     } else {
2382         $orderby = "ORDER BY $forumsort";
2383         $groupby = ", ".strtolower($forumsort);
2384         $groupby = str_replace('desc', '', $groupby);
2385         $groupby = str_replace('asc', '', $groupby);
2386     }
2388     if (($limitfrom == 0 and $limitnum == 0) or $forumsort == "") {
2389         $sql = "SELECT p.discussion, COUNT(p.id) AS replies, MAX(p.id) AS lastpostid
2390                   FROM {forum_posts} p
2391                        JOIN {forum_discussions} d ON p.discussion = d.id
2392                  WHERE p.parent > 0 AND d.forum = ?
2393               GROUP BY p.discussion";
2394         return $DB->get_records_sql($sql, array($forumid));
2396     } else {
2397         $sql = "SELECT p.discussion, (COUNT(p.id) - 1) AS replies, MAX(p.id) AS lastpostid
2398                   FROM {forum_posts} p
2399                        JOIN {forum_discussions} d ON p.discussion = d.id
2400                  WHERE d.forum = ?
2401               GROUP BY p.discussion $groupby
2402               $orderby";
2403         return $DB->get_records_sql("SELECT * FROM ($sql) sq", array($forumid), $limitfrom, $limitnum);
2404     }
2407 /**
2408  * @global object
2409  * @global object
2410  * @global object
2411  * @staticvar array $cache
2412  * @param object $forum
2413  * @param object $cm
2414  * @param object $course
2415  * @return mixed
2416  */
2417 function forum_count_discussions($forum, $cm, $course) {
2418     global $CFG, $DB, $USER;
2420     static $cache = array();
2422     $now = round(time(), -2); // db cache friendliness
2424     $params = array($course->id);
2426     if (!isset($cache[$course->id])) {
2427         if (!empty($CFG->forum_enabletimedposts)) {
2428             $timedsql = "AND d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?)";
2429             $params[] = $now;
2430             $params[] = $now;
2431         } else {
2432             $timedsql = "";
2433         }
2435         $sql = "SELECT f.id, COUNT(d.id) as dcount
2436                   FROM {forum} f
2437                        JOIN {forum_discussions} d ON d.forum = f.id
2438                  WHERE f.course = ?
2439                        $timedsql
2440               GROUP BY f.id";
2442         if ($counts = $DB->get_records_sql($sql, $params)) {
2443             foreach ($counts as $count) {
2444                 $counts[$count->id] = $count->dcount;
2445             }
2446             $cache[$course->id] = $counts;
2447         } else {
2448             $cache[$course->id] = array();
2449         }
2450     }
2452     if (empty($cache[$course->id][$forum->id])) {
2453         return 0;
2454     }
2456     $groupmode = groups_get_activity_groupmode($cm, $course);
2458     if ($groupmode != SEPARATEGROUPS) {
2459         return $cache[$course->id][$forum->id];
2460     }
2462     if (has_capability('moodle/site:accessallgroups', context_module::instance($cm->id))) {
2463         return $cache[$course->id][$forum->id];
2464     }
2466     require_once($CFG->dirroot.'/course/lib.php');
2468     $modinfo = get_fast_modinfo($course);
2469     if (is_null($modinfo->groups)) {
2470         $modinfo->groups = groups_get_user_groups($course->id, $USER->id);
2471     }
2473     if (array_key_exists($cm->groupingid, $modinfo->groups)) {
2474         $mygroups = $modinfo->groups[$cm->groupingid];
2475     } else {
2476         $mygroups = false; // Will be set below
2477     }
2479     // add all groups posts
2480     if (empty($mygroups)) {
2481         $mygroups = array(-1=>-1);
2482     } else {
2483         $mygroups[-1] = -1;
2484     }
2486     list($mygroups_sql, $params) = $DB->get_in_or_equal($mygroups);
2487     $params[] = $forum->id;
2489     if (!empty($CFG->forum_enabletimedposts)) {
2490         $timedsql = "AND d.timestart < $now AND (d.timeend = 0 OR d.timeend > $now)";
2491         $params[] = $now;
2492         $params[] = $now;
2493     } else {
2494         $timedsql = "";
2495     }
2497     $sql = "SELECT COUNT(d.id)
2498               FROM {forum_discussions} d
2499              WHERE d.groupid $mygroups_sql AND d.forum = ?
2500                    $timedsql";
2502     return $DB->get_field_sql($sql, $params);
2505 /**
2506  * How many posts by other users are unrated by a given user in the given discussion?
2507  *
2508  * TODO: Is this function still used anywhere?
2509  *
2510  * @param int $discussionid
2511  * @param int $userid
2512  * @return mixed
2513  */
2514 function forum_count_unrated_posts($discussionid, $userid) {
2515     global $CFG, $DB;
2517     $sql = "SELECT COUNT(*) as num
2518               FROM {forum_posts}
2519              WHERE parent > 0
2520                AND discussion = :discussionid
2521                AND userid <> :userid";
2522     $params = array('discussionid' => $discussionid, 'userid' => $userid);
2523     $posts = $DB->get_record_sql($sql, $params);
2524     if ($posts) {
2525         $sql = "SELECT count(*) as num
2526                   FROM {forum_posts} p,
2527                        {rating} r
2528                  WHERE p.discussion = :discussionid AND
2529                        p.id = r.itemid AND
2530                        r.userid = userid AND
2531                        r.component = 'mod_forum' AND
2532                        r.ratingarea = 'post'";
2533         $rated = $DB->get_record_sql($sql, $params);
2534         if ($rated) {
2535             if ($posts->num > $rated->num) {
2536                 return $posts->num - $rated->num;
2537             } else {
2538                 return 0;    // Just in case there was a counting error
2539             }
2540         } else {
2541             return $posts->num;
2542         }
2543     } else {
2544         return 0;
2545     }
2548 /**
2549  * Get all discussions in a forum
2550  *
2551  * @global object
2552  * @global object
2553  * @global object
2554  * @uses CONTEXT_MODULE
2555  * @uses VISIBLEGROUPS
2556  * @param object $cm
2557  * @param string $forumsort
2558  * @param bool $fullpost
2559  * @param int $unused
2560  * @param int $limit
2561  * @param bool $userlastmodified
2562  * @param int $page
2563  * @param int $perpage
2564  * @return array
2565  */
2566 function forum_get_discussions($cm, $forumsort="d.timemodified DESC", $fullpost=true, $unused=-1, $limit=-1, $userlastmodified=false, $page=-1, $perpage=0) {
2567     global $CFG, $DB, $USER;
2569     $timelimit = '';
2571     $now = round(time(), -2);
2572     $params = array($cm->instance);
2574     $modcontext = context_module::instance($cm->id);
2576     if (!has_capability('mod/forum:viewdiscussion', $modcontext)) { /// User must have perms to view discussions
2577         return array();
2578     }
2580     if (!empty($CFG->forum_enabletimedposts)) { /// Users must fulfill timed posts
2582         if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
2583             $timelimit = " AND ((d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?))";
2584             $params[] = $now;
2585             $params[] = $now;
2586             if (isloggedin()) {
2587                 $timelimit .= " OR d.userid = ?";
2588                 $params[] = $USER->id;
2589             }
2590             $timelimit .= ")";
2591         }
2592     }
2594     if ($limit > 0) {
2595         $limitfrom = 0;
2596         $limitnum  = $limit;
2597     } else if ($page != -1) {
2598         $limitfrom = $page*$perpage;
2599         $limitnum  = $perpage;
2600     } else {
2601         $limitfrom = 0;
2602         $limitnum  = 0;
2603     }
2605     $groupmode    = groups_get_activity_groupmode($cm);
2606     $currentgroup = groups_get_activity_group($cm);
2608     if ($groupmode) {
2609         if (empty($modcontext)) {
2610             $modcontext = context_module::instance($cm->id);
2611         }
2613         if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
2614             if ($currentgroup) {
2615                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2616                 $params[] = $currentgroup;
2617             } else {
2618                 $groupselect = "";
2619             }
2621         } else {
2622             //seprate groups without access all
2623             if ($currentgroup) {
2624                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2625                 $params[] = $currentgroup;
2626             } else {
2627                 $groupselect = "AND d.groupid = -1";
2628             }
2629         }
2630     } else {
2631         $groupselect = "";
2632     }
2635     if (empty($forumsort)) {
2636         $forumsort = "d.timemodified DESC";
2637     }
2638     if (empty($fullpost)) {
2639         $postdata = "p.id,p.subject,p.modified,p.discussion,p.userid";
2640     } else {
2641         $postdata = "p.*";
2642     }
2644     if (empty($userlastmodified)) {  // We don't need to know this
2645         $umfields = "";
2646         $umtable  = "";
2647     } else {
2648         $umfields = ", um.firstname AS umfirstname, um.lastname AS umlastname";
2649         $umtable  = " LEFT JOIN {user} um ON (d.usermodified = um.id)";
2650     }
2652     $sql = "SELECT $postdata, d.name, d.timemodified, d.usermodified, d.groupid, d.timestart, d.timeend,
2653                    u.firstname, u.lastname, u.email, u.picture, u.imagealt $umfields
2654               FROM {forum_discussions} d
2655                    JOIN {forum_posts} p ON p.discussion = d.id
2656                    JOIN {user} u ON p.userid = u.id
2657                    $umtable
2658              WHERE d.forum = ? AND p.parent = 0
2659                    $timelimit $groupselect
2660           ORDER BY $forumsort";
2661     return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
2664 /**
2665  *
2666  * @global object
2667  * @global object
2668  * @global object
2669  * @uses CONTEXT_MODULE
2670  * @uses VISIBLEGROUPS
2671  * @param object $cm
2672  * @return array
2673  */
2674 function forum_get_discussions_unread($cm) {
2675     global $CFG, $DB, $USER;
2677     $now = round(time(), -2);
2678     $cutoffdate = $now - ($CFG->forum_oldpostdays*24*60*60);
2680     $params = array();
2681     $groupmode    = groups_get_activity_groupmode($cm);
2682     $currentgroup = groups_get_activity_group($cm);
2684     if ($groupmode) {
2685         $modcontext = context_module::instance($cm->id);
2687         if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
2688             if ($currentgroup) {
2689                 $groupselect = "AND (d.groupid = :currentgroup OR d.groupid = -1)";
2690                 $params['currentgroup'] = $currentgroup;
2691             } else {
2692                 $groupselect = "";
2693             }
2695         } else {
2696             //separate groups without access all
2697             if ($currentgroup) {
2698                 $groupselect = "AND (d.groupid = :currentgroup OR d.groupid = -1)";
2699                 $params['currentgroup'] = $currentgroup;
2700             } else {
2701                 $groupselect = "AND d.groupid = -1";
2702             }
2703         }
2704     } else {
2705         $groupselect = "";
2706     }
2708     if (!empty($CFG->forum_enabletimedposts)) {
2709         $timedsql = "AND d.timestart < :now1 AND (d.timeend = 0 OR d.timeend > :now2)";
2710         $params['now1'] = $now;
2711         $params['now2'] = $now;
2712     } else {
2713         $timedsql = "";
2714     }
2716     $sql = "SELECT d.id, COUNT(p.id) AS unread
2717               FROM {forum_discussions} d
2718                    JOIN {forum_posts} p     ON p.discussion = d.id
2719                    LEFT JOIN {forum_read} r ON (r.postid = p.id AND r.userid = $USER->id)
2720              WHERE d.forum = {$cm->instance}
2721                    AND p.modified >= :cutoffdate AND r.id is NULL
2722                    $groupselect
2723                    $timedsql
2724           GROUP BY d.id";
2725     $params['cutoffdate'] = $cutoffdate;
2727     if ($unreads = $DB->get_records_sql($sql, $params)) {
2728         foreach ($unreads as $unread) {
2729             $unreads[$unread->id] = $unread->unread;
2730         }
2731         return $unreads;
2732     } else {
2733         return array();
2734     }
2737 /**
2738  * @global object
2739  * @global object
2740  * @global object
2741  * @uses CONEXT_MODULE
2742  * @uses VISIBLEGROUPS
2743  * @param object $cm
2744  * @return array
2745  */
2746 function forum_get_discussions_count($cm) {
2747     global $CFG, $DB, $USER;
2749     $now = round(time(), -2);
2750     $params = array($cm->instance);
2751     $groupmode    = groups_get_activity_groupmode($cm);
2752     $currentgroup = groups_get_activity_group($cm);
2754     if ($groupmode) {
2755         $modcontext = context_module::instance($cm->id);
2757         if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
2758             if ($currentgroup) {
2759                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2760                 $params[] = $currentgroup;
2761             } else {
2762                 $groupselect = "";
2763             }
2765         } else {
2766             //seprate groups without access all
2767             if ($currentgroup) {
2768                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2769                 $params[] = $currentgroup;
2770             } else {
2771                 $groupselect = "AND d.groupid = -1";
2772             }
2773         }
2774     } else {
2775         $groupselect = "";
2776     }
2778     $cutoffdate = $now - ($CFG->forum_oldpostdays*24*60*60);
2780     $timelimit = "";
2782     if (!empty($CFG->forum_enabletimedposts)) {
2784         $modcontext = context_module::instance($cm->id);
2786         if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
2787             $timelimit = " AND ((d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?))";
2788             $params[] = $now;
2789             $params[] = $now;
2790             if (isloggedin()) {
2791                 $timelimit .= " OR d.userid = ?";
2792                 $params[] = $USER->id;
2793             }
2794             $timelimit .= ")";
2795         }
2796     }
2798     $sql = "SELECT COUNT(d.id)
2799               FROM {forum_discussions} d
2800                    JOIN {forum_posts} p ON p.discussion = d.id
2801              WHERE d.forum = ? AND p.parent = 0
2802                    $groupselect $timelimit";
2804     return $DB->get_field_sql($sql, $params);
2808 /**
2809  * Get all discussions started by a particular user in a course (or group)
2810  * This function no longer used ...
2811  *
2812  * @todo Remove this function if no longer used
2813  * @global object
2814  * @global object
2815  * @param int $courseid
2816  * @param int $userid
2817  * @param int $groupid
2818  * @return array
2819  */
2820 function forum_get_user_discussions($courseid, $userid, $groupid=0) {
2821     global $CFG, $DB;
2822     $params = array($courseid, $userid);
2823     if ($groupid) {
2824         $groupselect = " AND d.groupid = ? ";
2825         $params[] = $groupid;
2826     } else  {
2827         $groupselect = "";
2828     }
2830     return $DB->get_records_sql("SELECT p.*, d.groupid, u.firstname, u.lastname, u.email, u.picture, u.imagealt,
2831                                    f.type as forumtype, f.name as forumname, f.id as forumid
2832                               FROM {forum_discussions} d,
2833                                    {forum_posts} p,
2834                                    {user} u,
2835                                    {forum} f
2836                              WHERE d.course = ?
2837                                AND p.discussion = d.id
2838                                AND p.parent = 0
2839                                AND p.userid = u.id
2840                                AND u.id = ?
2841                                AND d.forum = f.id $groupselect
2842                           ORDER BY p.created DESC", $params);
2845 /**
2846  * Get the list of potential subscribers to a forum.
2847  *
2848  * @param object $forumcontext the forum context.
2849  * @param integer $groupid the id of a group, or 0 for all groups.
2850  * @param string $fields the list of fields to return for each user. As for get_users_by_capability.
2851  * @param string $sort sort order. As for get_users_by_capability.
2852  * @return array list of users.
2853  */
2854 function forum_get_potential_subscribers($forumcontext, $groupid, $fields, $sort = '') {
2855     global $DB;
2857     // only active enrolled users or everybody on the frontpage
2858     list($esql, $params) = get_enrolled_sql($forumcontext, 'mod/forum:allowforcesubscribe', $groupid, true);
2859     if (!$sort) {
2860         list($sort, $sortparams) = users_order_by_sql('u');
2861         $params = array_merge($params, $sortparams);
2862     }
2864     $sql = "SELECT $fields
2865               FROM {user} u
2866               JOIN ($esql) je ON je.id = u.id
2867           ORDER BY $sort";
2869     return $DB->get_records_sql($sql, $params);
2872 /**
2873  * Returns list of user objects that are subscribed to this forum
2874  *
2875  * @global object
2876  * @global object
2877  * @param object $course the course
2878  * @param forum $forum the forum
2879  * @param integer $groupid group id, or 0 for all.
2880  * @param object $context the forum context, to save re-fetching it where possible.
2881  * @param string $fields requested user fields (with "u." table prefix)
2882  * @return array list of users.
2883  */
2884 function forum_subscribed_users($course, $forum, $groupid=0, $context = null, $fields = null) {
2885     global $CFG, $DB;
2887     if (empty($fields)) {
2888         $fields ="u.id,
2889                   u.username,
2890                   u.firstname,
2891                   u.lastname,
2892                   u.maildisplay,
2893                   u.mailformat,
2894                   u.maildigest,
2895                   u.imagealt,
2896                   u.email,
2897                   u.emailstop,
2898                   u.city,
2899                   u.country,
2900                   u.lastaccess,
2901                   u.lastlogin,
2902                   u.picture,
2903                   u.timezone,
2904                   u.theme,
2905                   u.lang,
2906                   u.trackforums,
2907                   u.mnethostid";
2908     }
2910     if (empty($context)) {
2911         $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id);
2912         $context = context_module::instance($cm->id);
2913     }
2915     if (forum_is_forcesubscribed($forum)) {
2916         $results = forum_get_potential_subscribers($context, $groupid, $fields, "u.email ASC");
2918     } else {
2919         // only active enrolled users or everybody on the frontpage
2920         list($esql, $params) = get_enrolled_sql($context, '', $groupid, true);
2921         $params['forumid'] = $forum->id;
2922         $results = $DB->get_records_sql("SELECT $fields
2923                                            FROM {user} u
2924                                            JOIN ($esql) je ON je.id = u.id
2925                                            JOIN {forum_subscriptions} s ON s.userid = u.id
2926                                           WHERE s.forum = :forumid
2927                                        ORDER BY u.email ASC", $params);
2928     }
2930     // Guest user should never be subscribed to a forum.
2931     unset($results[$CFG->siteguest]);
2933     return $results;
2938 // OTHER FUNCTIONS ///////////////////////////////////////////////////////////
2941 /**
2942  * @global object
2943  * @global object
2944  * @param int $courseid
2945  * @param string $type
2946  */
2947 function forum_get_course_forum($courseid, $type) {
2948 // How to set up special 1-per-course forums
2949     global $CFG, $DB, $OUTPUT;
2951     if ($forums = $DB->get_records_select("forum", "course = ? AND type = ?", array($courseid, $type), "id ASC")) {
2952         // There should always only be ONE, but with the right combination of
2953         // errors there might be more.  In this case, just return the oldest one (lowest ID).
2954         foreach ($forums as $forum) {
2955             return $forum;   // ie the first one
2956         }
2957     }
2959     // Doesn't exist, so create one now.
2960     $forum = new stdClass();
2961     $forum->course = $courseid;
2962     $forum->type = "$type";
2963     switch ($forum->type) {
2964         case "news":
2965             $forum->name  = get_string("namenews", "forum");
2966             $forum->intro = get_string("intronews", "forum");
2967             $forum->forcesubscribe = FORUM_FORCESUBSCRIBE;
2968             $forum->assessed = 0;
2969             if ($courseid == SITEID) {
2970                 $forum->name  = get_string("sitenews");
2971                 $forum->forcesubscribe = 0;
2972             }
2973             break;
2974         case "social":
2975             $forum->name  = get_string("namesocial", "forum");
2976             $forum->intro = get_string("introsocial", "forum");
2977             $forum->assessed = 0;
2978             $forum->forcesubscribe = 0;
2979             break;
2980         case "blog":
2981             $forum->name = get_string('blogforum', 'forum');
2982             $forum->intro = get_string('introblog', 'forum');
2983             $forum->assessed = 0;
2984             $forum->forcesubscribe = 0;
2985             break;
2986         default:
2987             echo $OUTPUT->notification("That forum type doesn't exist!");
2988             return false;
2989             break;
2990     }
2992     $forum->timemodified = time();
2993     $forum->id = $DB->insert_record("forum", $forum);
2995     if (! $module = $DB->get_record("modules", array("name" => "forum"))) {
2996         echo $OUTPUT->notification("Could not find forum module!!");
2997         return false;
2998     }
2999     $mod = new stdClass();
3000     $mod->course = $courseid;
3001     $mod->module = $module->id;
3002     $mod->instance = $forum->id;
3003     $mod->section = 0;
3004     include_once("$CFG->dirroot/course/lib.php");
3005     if (! $mod->coursemodule = add_course_module($mod) ) {
3006         echo $OUTPUT->notification("Could not add a new course module to the course '" . $courseid . "'");
3007         return false;
3008     }
3009     $sectionid = course_add_cm_to_section($courseid, $mod->coursemodule, 0);
3010     return $DB->get_record("forum", array("id" => "$forum->id"));
3014 /**
3015  * Given the data about a posting, builds up the HTML to display it and
3016  * returns the HTML in a string.  This is designed for sending via HTML email.
3017  *
3018  * @global object
3019  * @param object $course
3020  * @param object $cm
3021  * @param object $forum
3022  * @param object $discussion
3023  * @param object $post
3024  * @param object $userform
3025  * @param object $userto
3026  * @param bool $ownpost
3027  * @param bool $reply
3028  * @param bool $link
3029  * @param bool $rate
3030  * @param string $footer
3031  * @return string
3032  */
3033 function forum_make_mail_post($course, $cm, $forum, $discussion, $post, $userfrom, $userto,
3034                               $ownpost=false, $reply=false, $link=false, $rate=false, $footer="") {
3036     global $CFG, $OUTPUT;
3038     $modcontext = context_module::instance($cm->id);
3040     if (!isset($userto->viewfullnames[$forum->id])) {
3041         $viewfullnames = has_capability('moodle/site:viewfullnames', $modcontext, $userto->id);
3042     } else {
3043         $viewfullnames = $userto->viewfullnames[$forum->id];
3044     }
3046     // add absolute file links
3047     $post->message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php', $modcontext->id, 'mod_forum', 'post', $post->id);
3049     // format the post body
3050     $options = new stdClass();
3051     $options->para = true;
3052     $formattedtext = format_text($post->message, $post->messageformat, $options, $course->id);
3054     $output = '<table border="0" cellpadding="3" cellspacing="0" class="forumpost">';
3056     $output .= '<tr class="header"><td width="35" valign="top" class="picture left">';
3057     $output .= $OUTPUT->user_picture($userfrom, array('courseid'=>$course->id));
3058     $output .= '</td>';
3060     if ($post->parent) {
3061         $output .= '<td class="topic">';
3062     } else {
3063         $output .= '<td class="topic starter">';
3064     }
3065     $output .= '<div class="subject">'.format_string($post->subject).'</div>';
3067     $fullname = fullname($userfrom, $viewfullnames);
3068     $by = new stdClass();
3069     $by->name = '<a href="'.$CFG->wwwroot.'/user/view.php?id='.$userfrom->id.'&amp;course='.$course->id.'">'.$fullname.'</a>';
3070     $by->date = userdate($post->modified, '', $userto->timezone);
3071     $output .= '<div class="author">'.get_string('bynameondate', 'forum', $by).'</div>';
3073     $output .= '</td></tr>';
3075     $output .= '<tr><td class="left side" valign="top">';
3077     if (isset($userfrom->groups)) {
3078         $groups = $userfrom->groups[$forum->id];
3079     } else {
3080         $groups = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid);
3081     }
3083     if ($groups) {
3084         $output .= print_group_picture($groups, $course->id, false, true, true);
3085     } else {
3086         $output .= '&nbsp;';
3087     }
3089     $output .= '</td><td class="content">';
3091     $attachments = forum_print_attachments($post, $cm, 'html');
3092     if ($attachments !== '') {
3093         $output .= '<div class="attachments">';
3094         $output .= $attachments;
3095         $output .= '</div>';
3096     }
3098     $output .= $formattedtext;
3100 // Commands
3101     $commands = array();
3103     if ($post->parent) {
3104         $commands[] = '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.
3105                       $post->discussion.'&amp;parent='.$post->parent.'">'.get_string('parent', 'forum').'</a>';
3106     }
3108     if ($reply) {
3109         $commands[] = '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/post.php?reply='.$post->id.'">'.
3110                       get_string('reply', 'forum').'</a>';
3111     }
3113     $output .= '<div class="commands">';
3114     $output .= implode(' | ', $commands);
3115     $output .= '</div>';
3117 // Context link to post if required
3118     if ($link) {
3119         $output .= '<div class="link">';
3120         $output .= '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'#p'.$post->id.'">'.
3121                      get_string('postincontext', 'forum').'</a>';
3122         $output .= '</div>';
3123     }
3125     if ($footer) {
3126         $output .= '<div class="footer">'.$footer.'</div>';
3127     }
3128     $output .= '</td></tr></table>'."\n\n";
3130     return $output;
3133 /**
3134  * Print a forum post
3135  *
3136  * @global object
3137  * @global object
3138  * @uses FORUM_MODE_THREADED
3139  * @uses PORTFOLIO_FORMAT_PLAINHTML
3140  * @uses PORTFOLIO_FORMAT_FILE
3141  * @uses PORTFOLIO_FORMAT_RICHHTML
3142  * @uses PORTFOLIO_ADD_TEXT_LINK
3143  * @uses CONTEXT_MODULE
3144  * @param object $post The post to print.
3145  * @param object $discussion
3146  * @param object $forum
3147  * @param object $cm
3148  * @param object $course
3149  * @param boolean $ownpost Whether this post belongs to the current user.
3150  * @param boolean $reply Whether to print a 'reply' link at the bottom of the message.
3151  * @param boolean $link Just print a shortened version of the post as a link to the full post.
3152  * @param string $footer Extra stuff to print after the message.
3153  * @param string $highlight Space-separated list of terms to highlight.
3154  * @param int $post_read true, false or -99. If we already know whether this user
3155  *          has read this post, pass that in, otherwise, pass in -99, and this
3156  *          function will work it out.
3157  * @param boolean $dummyifcantsee When forum_user_can_see_post says that
3158  *          the current user can't see this post, if this argument is true
3159  *          (the default) then print a dummy 'you can't see this post' post.
3160  *          If false, don't output anything at all.
3161  * @param bool|null $istracked
3162  * @return void
3163  */
3164 function forum_print_post($post, $discussion, $forum, &$cm, $course, $ownpost=false, $reply=false, $link=false,
3165                           $footer="", $highlight="", $postisread=null, $dummyifcantsee=true, $istracked=null, $return=false) {
3166     global $USER, $CFG, $OUTPUT;
3168     require_once($CFG->libdir . '/filelib.php');
3170     // String cache
3171     static $str;
3173     $modcontext = context_module::instance($cm->id);
3175     $post->course = $course->id;
3176     $post->forum  = $forum->id;
3177     $post->message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php', $modcontext->id, 'mod_forum', 'post', $post->id);
3178     if (!empty($CFG->enableplagiarism)) {
3179         require_once($CFG->libdir.'/plagiarismlib.php');
3180         $post->message .= plagiarism_get_links(array('userid' => $post->userid,
3181             'content' => $post->message,
3182             'cmid' => $cm->id,
3183             'course' => $post->course,
3184             'forum' => $post->forum));
3185     }
3187     // caching
3188     if (!isset($cm->cache)) {
3189         $cm->cache = new stdClass;
3190     }
3192     if (!isset($cm->cache->caps)) {
3193         $cm->cache->caps = array();
3194         $cm->cache->caps['mod/forum:viewdiscussion']   = has_capability('mod/forum:viewdiscussion', $modcontext);
3195         $cm->cache->caps['moodle/site:viewfullnames']  = has_capability('moodle/site:viewfullnames', $modcontext);
3196         $cm->cache->caps['mod/forum:editanypost']      = has_capability('mod/forum:editanypost', $modcontext);
3197         $cm->cache->caps['mod/forum:splitdiscussions'] = has_capability('mod/forum:splitdiscussions', $modcontext);
3198         $cm->cache->caps['mod/forum:deleteownpost']    = has_capability('mod/forum:deleteownpost', $modcontext);
3199         $cm->cache->caps['mod/forum:deleteanypost']    = has_capability('mod/forum:deleteanypost', $modcontext);
3200         $cm->cache->caps['mod/forum:viewanyrating']    = has_capability('mod/forum:viewanyrating', $modcontext);
3201         $cm->cache->caps['mod/forum:exportpost']       = has_capability('mod/forum:exportpost', $modcontext);
3202         $cm->cache->caps['mod/forum:exportownpost']    = has_capability('mod/forum:exportownpost', $modcontext);
3203     }
3205     if (!isset($cm->uservisible)) {
3206         $cm->uservisible = coursemodule_visible_for_user($cm);
3207     }
3209     if ($istracked && is_null($postisread)) {
3210         $postisread = forum_tp_is_post_read($USER->id, $post);
3211     }
3213     if (!forum_user_can_see_post($forum, $discussion, $post, NULL, $cm)) {
3214         $output = '';
3215         if (!$dummyifcantsee) {
3216             if ($return) {
3217                 return $output;
3218             }
3219             echo $output;
3220             return;
3221         }
3222         $output .= html_writer::tag('a', '', array('id'=>'p'.$post->id));
3223         $output .= html_writer::start_tag('div', array('class'=>'forumpost clearfix'));
3224         $output .= html_writer::start_tag('div', array('class'=>'row header'));
3225         $output .= html_writer::tag('div', '', array('class'=>'left picture')); // Picture
3226         if ($post->parent) {
3227             $output .= html_writer::start_tag('div', array('class'=>'topic'));
3228         } else {
3229             $output .= html_writer::start_tag('div', array('class'=>'topic starter'));
3230         }
3231         $output .= html_writer::tag('div', get_string('forumsubjecthidden','forum'), array('class'=>'subject')); // Subject
3232         $output .= html_writer::tag('div', get_string('forumauthorhidden','forum'), array('class'=>'author')); // author
3233         $output .= html_writer::end_tag('div');
3234         $output .= html_writer::end_tag('div'); // row
3235         $output .= html_writer::start_tag('div', array('class'=>'row'));
3236         $output .= html_writer::tag('div', '&nbsp;', array('class'=>'left side')); // Groups
3237         $output .= html_writer::tag('div', get_string('forumbodyhidden','forum'), array('class'=>'content')); // Content
3238         $output .= html_writer::end_tag('div'); // row
3239         $output .= html_writer::end_tag('div'); // forumpost
3241         if ($return) {
3242             return $output;
3243         }
3244         echo $output;
3245         return;
3246     }
3248     if (empty($str)) {
3249         $str = new stdClass;
3250         $str->edit         = get_string('edit', 'forum');
3251         $str->delete       = get_string('delete', 'forum');
3252         $str->reply        = get_string('reply', 'forum');
3253         $str->parent       = get_string('parent', 'forum');
3254         $str->pruneheading = get_string('pruneheading', 'forum');
3255         $str->prune        = get_string('prune', 'forum');
3256         $str->displaymode     = get_user_preferences('forum_displaymode', $CFG->forum_displaymode);
3257         $str->markread     = get_string('markread', 'forum');
3258         $str->markunread   = get_string('markunread', 'forum');
3259     }
3261     $discussionlink = new moodle_url('/mod/forum/discuss.php', array('d'=>$post->discussion));
3263     // Build an object that represents the posting user
3264     $postuser = new stdClass;
3265     $postuser->id        = $post->userid;
3266     $postuser->firstname = $post->firstname;
3267     $postuser->lastname  = $post->lastname;
3268     $postuser->imagealt  = $post->imagealt;
3269     $postuser->picture   = $post->picture;
3270     $postuser->email     = $post->email;
3271     // Some handy things for later on
3272     $postuser->fullname    = fullname($postuser, $cm->cache->caps['moodle/site:viewfullnames']);
3273     $postuser->profilelink = new moodle_url('/user/view.php', array('id'=>$post->userid, 'course'=>$course->id));
3275     // Prepare the groups the posting user belongs to
3276     if (isset($cm->cache->usersgroups)) {
3277         $groups = array();
3278         if (isset($cm->cache->usersgroups[$post->userid])) {
3279             foreach ($cm->cache->usersgroups[$post->userid] as $gid) {
3280                 $groups[$gid] = $cm->cache->groups[$gid];
3281             }
3282         }
3283     } else {
3284         $groups = groups_get_all_groups($course->id, $post->userid, $cm->groupingid);
3285     }
3287     // Prepare the attachements for the post, files then images
3288     list($attachments, $attachedimages) = forum_print_attachments($post, $cm, 'separateimages');
3290     // Determine if we need to shorten this post
3291     $shortenpost = ($link && (strlen(strip_tags($post->message)) > $CFG->forum_longpost));
3294     // Prepare an array of commands
3295     $commands = array();
3297     // SPECIAL CASE: The front page can display a news item post to non-logged in users.
3298     // Don't display the mark read / unread controls in this case.
3299     if ($istracked && $CFG->forum_usermarksread && isloggedin()) {
3300         $url = new moodle_url($discussionlink, array('postid'=>$post->id, 'mark'=>'unread'));
3301         $text = $str->markunread;
3302         if (!$postisread) {
3303             $url->param('mark', 'read');
3304             $text = $str->markread;
3305         }
3306         if ($str->displaymode == FORUM_MODE_THREADED) {
3307             $url->param('parent', $post->parent);
3308         } else {
3309             $url->set_anchor('p'.$post->id);
3310         }
3311         $commands[] = array('url'=>$url, 'text'=>$text);
3312     }
3314     // Zoom in to the parent specifically
3315     if ($post->parent) {
3316         $url = new moodle_url($discussionlink);
3317         if ($str->displaymode == FORUM_MODE_THREADED) {
3318             $url->param('parent', $post->parent);
3319         } else {
3320             $url->set_anchor('p'.$post->parent);
3321         }
3322         $commands[] = array('url'=>$url, 'text'=>$str->parent);
3323     }
3325     // Hack for allow to edit news posts those are not displayed yet until they are displayed
3326     $age = time() - $post->created;
3327     if (!$post->parent && $forum->type == 'news' && $discussion->timestart > time()) {
3328         $age = 0;
3329     }
3330     if (($ownpost && $age < $CFG->maxeditingtime) || $cm->cache->caps['mod/forum:editanypost']) {
3331         $commands[] = array('url'=>new moodle_url('/mod/forum/post.php', array('edit'=>$post->id)), 'text'=>$str->edit);
3332     }
3334     if ($cm->cache->caps['mod/forum:splitdiscussions'] && $post->parent && $forum->type != 'single') {
3335         $commands[] = array('url'=>new moodle_url('/mod/forum/post.php', array('prune'=>$post->id)), 'text'=>$str->prune, 'title'=>$str->pruneheading);
3336     }
3338     if (($ownpost && $age < $CFG->maxeditingtime && $cm->cache->caps['mod/forum:deleteownpost']) || $cm->cache->caps['mod/forum:deleteanypost']) {
3339         $commands[] = array('url'=>new moodle_url('/mod/forum/post.php', array('delete'=>$post->id)), 'text'=>$str->delete);
3340     }
3342     if ($reply) {
3343         $commands[] = array('url'=>new moodle_url('/mod/forum/post.php#mformforum', array('reply'=>$post->id)), 'text'=>$str->reply);
3344     }
3346     if ($CFG->enableportfolios && ($cm->cache->caps['mod/forum:exportpost'] || ($ownpost && $cm->cache->caps['mod/forum:exportownpost']))) {
3347         $p = array('postid' => $post->id);
3348         require_once($CFG->libdir.'/portfoliolib.php');
3349         $button = new portfolio_add_button();
3350         $button->set_callback_options('forum_portfolio_caller', array('postid' => $post->id), 'mod_forum');
3351         if (empty($attachments)) {
3352             $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML);
3353         } else {
3354             $button->set_formats(PORTFOLIO_FORMAT_RICHHTML);
3355         }
3357         $porfoliohtml = $button->to_html(PORTFOLIO_ADD_TEXT_LINK);
3358         if (!empty($porfoliohtml)) {
3359             $commands[] = $porfoliohtml;
3360         }
3361     }
3362     // Finished building commands
3365     // Begin output
3367     $output  = '';
3369     if ($istracked) {
3370         if ($postisread) {
3371             $forumpostclass = ' read';
3372         } else {
3373             $forumpostclass = ' unread';
3374             $output .= html_writer::tag('a', '', array('name'=>'unread'));
3375         }
3376     } else {
3377         // ignore trackign status if not tracked or tracked param missing
3378         $forumpostclass = '';
3379     }
3381     $topicclass = '';
3382     if (empty($post->parent)) {
3383         $topicclass = ' firstpost starter';
3384     }
3386     $output .= html_writer::tag('a', '', array('id'=>'p'.$post->id));
3387     $output .= html_writer::start_tag('div', array('class'=>'forumpost clearfix'.$forumpostclass.$topicclass));
3388     $output .= html_writer::start_tag('div', array('class'=>'row header clearfix'));
3389     $output .= html_writer::start_tag('div', array('class'=>'left picture'));
3390     $output .= $OUTPUT->user_picture($postuser, array('courseid'=>$course->id));
3391     $output .= html_writer::end_tag('div');
3394     $output .= html_writer::start_tag('div', array('class'=>'topic'.$topicclass));
3396     $postsubject = $post->subject;
3397     if (empty($post->subjectnoformat)) {
3398         $postsubject = format_string($postsubject);
3399     }
3400     $output .= html_writer::tag('div', $postsubject, array('class'=>'subject'));
3402     $by = new stdClass();
3403     $by->name = html_writer::link($postuser->profilelink, $postuser->fullname);
3404     $by->date = userdate($post->modified);
3405     $output .= html_writer::tag('div', get_string('bynameondate', 'forum', $by), array('class'=>'author'));
3407     $output .= html_writer::end_tag('div'); //topic
3408     $output .= html_writer::end_tag('div'); //row
3410     $output .= html_writer::start_tag('div', array('class'=>'row maincontent clearfix'));
3411     $output .= html_writer::start_tag('div', array('class'=>'left'));
3413     $groupoutput = '';
3414     if ($groups) {
3415         $groupoutput = print_group_picture($groups, $course->id, false, true, true);
3416     }
3417     if (empty($groupoutput)) {
3418         $groupoutput = '&nbsp;';
3419     }
3420     $output .= html_writer::tag('div', $groupoutput, array('class'=>'grouppictures'));
3422     $output .= html_writer::end_tag('div'); //left side
3423     $output .= html_writer::start_tag('div', array('class'=>'no-overflow'));
3424     $output .= html_writer::start_tag('div', array('class'=>'content'));
3425     if (!empty($attachments)) {
3426         $output .= html_writer::tag('div', $attachments, array('class'=>'attachments'));
3427     }
3429     $options = new stdClass;
3430     $options->para    = false;
3431     $options->trusted = $post->messagetrust;
3432     $options->context = $modcontext;
3433     if ($shortenpost) {
3434         // Prepare shortened version
3435         $postclass    = 'shortenedpost';
3436         $postcontent  = format_text(forum_shorten_post($post->message), $post->messageformat, $options, $course->id);
3437         $postcontent .= html_writer::link($discussionlink, get_string('readtherest', 'forum'));
3438         $postcontent .= html_writer::tag('span', '('.get_string('numwords', 'moodle', count_words(strip_tags($post->message))).')...', array('class'=>'post-word-count'));
3439     } else {
3440         // Prepare whole post
3441         $postclass    = 'fullpost';
3442         $postcontent  = format_text($post->message, $post->messageformat, $options, $course->id);
3443         if (!empty($highlight)) {
3444             $postcontent = highlight($highlight, $postcontent);
3445         }
3446         $postcontent .= html_writer::tag('div', $attachedimages, array('class'=>'attachedimages'));
3447     }
3448     // Output the post content
3449     $output .= html_writer::tag('div', $postcontent, array('class'=>'posting '.$postclass));
3450     $output .= html_writer::end_tag('div'); // Content
3451     $output .= html_writer::end_tag('div'); // Content mask
3452     $output .= html_writer::end_tag('div'); // Row
3454     $output .= html_writer::start_tag('div', array('class'=>'row side'));
3455     $output .= html_writer::tag('div','&nbsp;', array('class'=>'left'));
3456     $output .= html_writer::start_tag('div', array('class'=>'options clearfix'));
3458     // Output ratings
3459     if (!empty($post->rating)) {
3460         $output .= html_writer::tag('div', $OUTPUT->render($post->rating), array('class'=>'forum-post-rating'));
3461     }
3463     // Output the commands
3464     $commandhtml = array();
3465     foreach ($commands as $command) {
3466         if (is_array($command)) {
3467             $commandhtml[] = html_writer::link($command['url'], $command['text']);
3468         } else {
3469             $commandhtml[] = $command;
3470         }
3471     }
3472     $output .= html_writer::tag('div', implode(' | ', $commandhtml), array('class'=>'commands'));
3474     // Output link to post if required
3475     if ($link) {
3476         if ($post->replies == 1) {
3477             $replystring = get_string('repliesone', 'forum', $post->replies);
3478         } else {
3479             $replystring = get_string('repliesmany', 'forum', $post->replies);
3480         }
3482         $output .= html_writer::start_tag('div', array('class'=>'link'));
3483         $output .= html_writer::link($discussionlink, get_string('discussthistopic', 'forum'));
3484         $output .= '&nbsp;('.$replystring.')';
3485         $output .= html_writer::end_tag('div'); // link
3486     }
3488     // Output footer if required
3489     if ($footer) {
3490         $output .= html_writer::tag('div', $footer, array('class'=>'footer'));
3491     }
3493     // Close remaining open divs
3494     $output .= html_writer::end_tag('div'); // content