MDL-26120_23 mod_forum: use correct object
[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 = get_context_instance(CONTEXT_MODULE, $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(get_context_instance(CONTEXT_COURSE, $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             $options = array('subdirs'=>true); // Use the same options as intro field!
104             $post->message = file_save_draft_area_files($draftid, $modcontext->id, 'mod_forum', 'post', $post->id, $options, $post->message);
105             $DB->set_field('forum_posts', 'message', $post->message, array('id'=>$post->id));
106         }
107     }
109     if ($forum->forcesubscribe == FORUM_INITIALSUBSCRIBE) {
110     /// all users should be subscribed initially
111     /// Note: forum_get_potential_subscribers should take the forum context,
112     /// but that does not exist yet, becuase the forum is only half build at this
113     /// stage. However, because the forum is brand new, we know that there are
114     /// no role assignments or overrides in the forum context, so using the
115     /// course context gives the same list of users.
116         $users = forum_get_potential_subscribers($modcontext, 0, 'u.id, u.email', '');
117         foreach ($users as $user) {
118             forum_subscribe($user->id, $forum->id);
119         }
120     }
122     forum_grade_item_update($forum);
124     return $forum->id;
128 /**
129  * Given an object containing all the necessary data,
130  * (defined by the form in mod_form.php) this function
131  * will update an existing instance with new data.
132  *
133  * @global object
134  * @param object $forum forum instance (with magic quotes)
135  * @return bool success
136  */
137 function forum_update_instance($forum, $mform) {
138     global $DB, $OUTPUT, $USER;
140     $forum->timemodified = time();
141     $forum->id           = $forum->instance;
143     if (empty($forum->assessed)) {
144         $forum->assessed = 0;
145     }
147     if (empty($forum->ratingtime) or empty($forum->assessed)) {
148         $forum->assesstimestart  = 0;
149         $forum->assesstimefinish = 0;
150     }
152     $oldforum = $DB->get_record('forum', array('id'=>$forum->id));
154     // MDL-3942 - if the aggregation type or scale (i.e. max grade) changes then recalculate the grades for the entire forum
155     // if  scale changes - do we need to recheck the ratings, if ratings higher than scale how do we want to respond?
156     // 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
157     if (($oldforum->assessed<>$forum->assessed) or ($oldforum->scale<>$forum->scale)) {
158         forum_update_grades($forum); // recalculate grades for the forum
159     }
161     if ($forum->type == 'single') {  // Update related discussion and post.
162         $discussions = $DB->get_records('forum_discussions', array('forum'=>$forum->id), 'timemodified ASC');
163         if (!empty($discussions)) {
164             if (count($discussions) > 1) {
165                 echo $OUTPUT->notification(get_string('warnformorepost', 'forum'));
166             }
167             $discussion = array_pop($discussions);
168         } else {
169             // try to recover by creating initial discussion - MDL-16262
170             $discussion = new stdClass();
171             $discussion->course          = $forum->course;
172             $discussion->forum           = $forum->id;
173             $discussion->name            = $forum->name;
174             $discussion->assessed        = $forum->assessed;
175             $discussion->message         = $forum->intro;
176             $discussion->messageformat   = $forum->introformat;
177             $discussion->messagetrust    = true;
178             $discussion->mailnow         = false;
179             $discussion->groupid         = -1;
181             $message = '';
183             forum_add_discussion($discussion, null, $message);
185             if (! $discussion = $DB->get_record('forum_discussions', array('forum'=>$forum->id))) {
186                 print_error('cannotadd', 'forum');
187             }
188         }
189         if (! $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost))) {
190             print_error('cannotfindfirstpost', 'forum');
191         }
193         $cm         = get_coursemodule_from_instance('forum', $forum->id);
194         $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id, MUST_EXIST);
196         $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost), '*', MUST_EXIST);
197         $post->subject       = $forum->name;
198         $post->message       = $forum->intro;
199         $post->messageformat = $forum->introformat;
200         $post->messagetrust  = trusttext_trusted($modcontext);
201         $post->modified      = $forum->timemodified;
202         $post->userid        = $USER->id;    // MDL-18599, so that current teacher can take ownership of activities.
204         if ($mform and $draftid = file_get_submitted_draft_itemid('introeditor')) {
205             // Ugly hack - we need to copy the files somehow.
206             $options = array('subdirs'=>true); // Use the same options as intro field!
207             $post->message = file_save_draft_area_files($draftid, $modcontext->id, 'mod_forum', 'post', $post->id, $options, $post->message);
208         }
210         $DB->update_record('forum_posts', $post);
211         $discussion->name = $forum->name;
212         $DB->update_record('forum_discussions', $discussion);
213     }
215     $DB->update_record('forum', $forum);
217     $modcontext = get_context_instance(CONTEXT_MODULE, $forum->coursemodule);
218     if (($forum->forcesubscribe == FORUM_INITIALSUBSCRIBE) && ($oldforum->forcesubscribe <> $forum->forcesubscribe)) {
219         $users = forum_get_potential_subscribers($modcontext, 0, 'u.id, u.email', '');
220         foreach ($users as $user) {
221             forum_subscribe($user->id, $forum->id);
222         }
223     }
225     forum_grade_item_update($forum);
227     return true;
231 /**
232  * Given an ID of an instance of this module,
233  * this function will permanently delete the instance
234  * and any data that depends on it.
235  *
236  * @global object
237  * @param int $id forum instance id
238  * @return bool success
239  */
240 function forum_delete_instance($id) {
241     global $DB;
243     if (!$forum = $DB->get_record('forum', array('id'=>$id))) {
244         return false;
245     }
246     if (!$cm = get_coursemodule_from_instance('forum', $forum->id)) {
247         return false;
248     }
249     if (!$course = $DB->get_record('course', array('id'=>$cm->course))) {
250         return false;
251     }
253     $context = get_context_instance(CONTEXT_MODULE, $cm->id);
255     // now get rid of all files
256     $fs = get_file_storage();
257     $fs->delete_area_files($context->id);
259     $result = true;
261     if ($discussions = $DB->get_records('forum_discussions', array('forum'=>$forum->id))) {
262         foreach ($discussions as $discussion) {
263             if (!forum_delete_discussion($discussion, true, $course, $cm, $forum)) {
264                 $result = false;
265             }
266         }
267     }
269     if (!$DB->delete_records('forum_subscriptions', array('forum'=>$forum->id))) {
270         $result = false;
271     }
273     forum_tp_delete_read_records(-1, -1, -1, $forum->id);
275     if (!$DB->delete_records('forum', array('id'=>$forum->id))) {
276         $result = false;
277     }
279     forum_grade_item_delete($forum);
281     return $result;
285 /**
286  * Indicates API features that the forum supports.
287  *
288  * @uses FEATURE_GROUPS
289  * @uses FEATURE_GROUPINGS
290  * @uses FEATURE_GROUPMEMBERSONLY
291  * @uses FEATURE_MOD_INTRO
292  * @uses FEATURE_COMPLETION_TRACKS_VIEWS
293  * @uses FEATURE_COMPLETION_HAS_RULES
294  * @uses FEATURE_GRADE_HAS_GRADE
295  * @uses FEATURE_GRADE_OUTCOMES
296  * @param string $feature
297  * @return mixed True if yes (some features may use other values)
298  */
299 function forum_supports($feature) {
300     switch($feature) {
301         case FEATURE_GROUPS:                  return true;
302         case FEATURE_GROUPINGS:               return true;
303         case FEATURE_GROUPMEMBERSONLY:        return true;
304         case FEATURE_MOD_INTRO:               return true;
305         case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
306         case FEATURE_COMPLETION_HAS_RULES:    return true;
307         case FEATURE_GRADE_HAS_GRADE:         return true;
308         case FEATURE_GRADE_OUTCOMES:          return true;
309         case FEATURE_RATE:                    return true;
310         case FEATURE_BACKUP_MOODLE2:          return true;
311         case FEATURE_SHOW_DESCRIPTION:        return true;
313         default: return null;
314     }
318 /**
319  * Obtains the automatic completion state for this forum based on any conditions
320  * in forum settings.
321  *
322  * @global object
323  * @global object
324  * @param object $course Course
325  * @param object $cm Course-module
326  * @param int $userid User ID
327  * @param bool $type Type of comparison (or/and; can be used as return value if no conditions)
328  * @return bool True if completed, false if not. (If no conditions, then return
329  *   value depends on comparison type)
330  */
331 function forum_get_completion_state($course,$cm,$userid,$type) {
332     global $CFG,$DB;
334     // Get forum details
335     if (!($forum=$DB->get_record('forum',array('id'=>$cm->instance)))) {
336         throw new Exception("Can't find forum {$cm->instance}");
337     }
339     $result=$type; // Default return value
341     $postcountparams=array('userid'=>$userid,'forumid'=>$forum->id);
342     $postcountsql="
343 SELECT
344     COUNT(1)
345 FROM
346     {forum_posts} fp
347     INNER JOIN {forum_discussions} fd ON fp.discussion=fd.id
348 WHERE
349     fp.userid=:userid AND fd.forum=:forumid";
351     if ($forum->completiondiscussions) {
352         $value = $forum->completiondiscussions <=
353                  $DB->count_records('forum_discussions',array('forum'=>$forum->id,'userid'=>$userid));
354         if ($type == COMPLETION_AND) {
355             $result = $result && $value;
356         } else {
357             $result = $result || $value;
358         }
359     }
360     if ($forum->completionreplies) {
361         $value = $forum->completionreplies <=
362                  $DB->get_field_sql( $postcountsql.' AND fp.parent<>0',$postcountparams);
363         if ($type==COMPLETION_AND) {
364             $result = $result && $value;
365         } else {
366             $result = $result || $value;
367         }
368     }
369     if ($forum->completionposts) {
370         $value = $forum->completionposts <= $DB->get_field_sql($postcountsql,$postcountparams);
371         if ($type == COMPLETION_AND) {
372             $result = $result && $value;
373         } else {
374             $result = $result || $value;
375         }
376     }
378     return $result;
381 /**
382  * Create a message-id string to use in the custom headers of forum notification emails
383  *
384  * message-id is used by email clients to identify emails and to nest conversations
385  *
386  * @param int $postid The ID of the forum post we are notifying the user about
387  * @param int $usertoid The ID of the user being notified
388  * @param string $hostname The server's hostname
389  * @return string A unique message-id
390  */
391 function forum_get_email_message_id($postid, $usertoid, $hostname) {
392     return '<'.hash('sha256',$postid.'to'.$usertoid).'@'.$hostname.'>';
395 /**
396  * Removes properties from user record that are not necessary
397  * for sending post notifications.
398  * @param stdClass $user
399  * @return void, $user parameter is modified
400  */
401 function forum_cron_minimise_user_record(stdClass $user) {
403     // We store large amount of users in one huge array,
404     // make sure we do not store info there we do not actually need
405     // in mail generation code or messaging.
407     unset($user->institution);
408     unset($user->department);
409     unset($user->address);
410     unset($user->city);
411     unset($user->url);
412     unset($user->currentlogin);
413     unset($user->description);
414     unset($user->descriptionformat);
417 /**
418  * Function to be run periodically according to the moodle cron
419  * Finds all posts that have yet to be mailed out, and mails them
420  * out to all subscribers
421  *
422  * @global object
423  * @global object
424  * @global object
425  * @uses CONTEXT_MODULE
426  * @uses CONTEXT_COURSE
427  * @uses SITEID
428  * @uses FORMAT_PLAIN
429  * @return void
430  */
431 function forum_cron() {
432     global $CFG, $USER, $DB;
434     $site = get_site();
436     // All users that are subscribed to any post that needs sending,
437     // please increase $CFG->extramemorylimit on large sites that
438     // send notifications to a large number of users.
439     $users = array();
440     $userscount = 0; // Cached user counter - count($users) in PHP is horribly slow!!!
442     // status arrays
443     $mailcount  = array();
444     $errorcount = array();
446     // caches
447     $discussions     = array();
448     $forums          = array();
449     $courses         = array();
450     $coursemodules   = array();
451     $subscribedusers = array();
454     // Posts older than 2 days will not be mailed.  This is to avoid the problem where
455     // cron has not been running for a long time, and then suddenly people are flooded
456     // with mail from the past few weeks or months
457     $timenow   = time();
458     $endtime   = $timenow - $CFG->maxeditingtime;
459     $starttime = $endtime - 48 * 3600;   // Two days earlier
461     if ($posts = forum_get_unmailed_posts($starttime, $endtime, $timenow)) {
462         // Mark them all now as being mailed.  It's unlikely but possible there
463         // might be an error later so that a post is NOT actually mailed out,
464         // but since mail isn't crucial, we can accept this risk.  Doing it now
465         // prevents the risk of duplicated mails, which is a worse problem.
467         if (!forum_mark_old_posts_as_mailed($endtime)) {
468             mtrace('Errors occurred while trying to mark some posts as being mailed.');
469             return false;  // Don't continue trying to mail them, in case we are in a cron loop
470         }
472         // checking post validity, and adding users to loop through later
473         foreach ($posts as $pid => $post) {
475             $discussionid = $post->discussion;
476             if (!isset($discussions[$discussionid])) {
477                 if ($discussion = $DB->get_record('forum_discussions', array('id'=> $post->discussion))) {
478                     $discussions[$discussionid] = $discussion;
479                 } else {
480                     mtrace('Could not find discussion '.$discussionid);
481                     unset($posts[$pid]);
482                     continue;
483                 }
484             }
485             $forumid = $discussions[$discussionid]->forum;
486             if (!isset($forums[$forumid])) {
487                 if ($forum = $DB->get_record('forum', array('id' => $forumid))) {
488                     $forums[$forumid] = $forum;
489                 } else {
490                     mtrace('Could not find forum '.$forumid);
491                     unset($posts[$pid]);
492                     continue;
493                 }
494             }
495             $courseid = $forums[$forumid]->course;
496             if (!isset($courses[$courseid])) {
497                 if ($course = $DB->get_record('course', array('id' => $courseid))) {
498                     $courses[$courseid] = $course;
499                 } else {
500                     mtrace('Could not find course '.$courseid);
501                     unset($posts[$pid]);
502                     continue;
503                 }
504             }
505             if (!isset($coursemodules[$forumid])) {
506                 if ($cm = get_coursemodule_from_instance('forum', $forumid, $courseid)) {
507                     $coursemodules[$forumid] = $cm;
508                 } else {
509                     mtrace('Could not find course module for forum '.$forumid);
510                     unset($posts[$pid]);
511                     continue;
512                 }
513             }
516             // caching subscribed users of each forum
517             if (!isset($subscribedusers[$forumid])) {
518                 $modcontext = get_context_instance(CONTEXT_MODULE, $coursemodules[$forumid]->id);
519                 if ($subusers = forum_subscribed_users($courses[$courseid], $forums[$forumid], 0, $modcontext, "u.*")) {
520                     foreach ($subusers as $postuser) {
521                         // this user is subscribed to this forum
522                         $subscribedusers[$forumid][$postuser->id] = $postuser->id;
523                         $userscount++;
524                         if ($userscount > FORUM_CRON_USER_CACHE) {
525                             // Store minimal user info.
526                             $minuser = new stdClass();
527                             $minuser->id = $postuser->id;
528                             $users[$postuser->id] = $minuser;
529                         } else {
530                             // Cache full user record.
531                             forum_cron_minimise_user_record($postuser);
532                             $users[$postuser->id] = $postuser;
533                         }
534                     }
535                     // Release memory.
536                     unset($subusers);
537                     unset($postuser);
538                 }
539             }
541             $mailcount[$pid] = 0;
542             $errorcount[$pid] = 0;
543         }
544     }
546     if ($users && $posts) {
548         $urlinfo = parse_url($CFG->wwwroot);
549         $hostname = $urlinfo['host'];
551         foreach ($users as $userto) {
553             @set_time_limit(120); // terminate if processing of any account takes longer than 2 minutes
555             mtrace('Processing user '.$userto->id);
557             // Init user caches - we keep the cache for one cycle only,
558             // otherwise it could consume too much memory.
559             if (isset($userto->username)) {
560                 $userto = clone($userto);
561             } else {
562                 $userto = $DB->get_record('user', array('id' => $userto->id));
563                 forum_cron_minimise_user_record($userto);
564             }
565             $userto->viewfullnames = array();
566             $userto->canpost       = array();
567             $userto->markposts     = array();
569             // set this so that the capabilities are cached, and environment matches receiving user
570             cron_setup_user($userto);
572             // reset the caches
573             foreach ($coursemodules as $forumid=>$unused) {
574                 $coursemodules[$forumid]->cache       = new stdClass();
575                 $coursemodules[$forumid]->cache->caps = array();
576                 unset($coursemodules[$forumid]->uservisible);
577             }
579             foreach ($posts as $pid => $post) {
581                 // Set up the environment for the post, discussion, forum, course
582                 $discussion = $discussions[$post->discussion];
583                 $forum      = $forums[$discussion->forum];
584                 $course     = $courses[$forum->course];
585                 $cm         =& $coursemodules[$forum->id];
587                 // Do some checks  to see if we can bail out now
588                 // Only active enrolled users are in the list of subscribers
589                 if (!isset($subscribedusers[$forum->id][$userto->id])) {
590                     continue; // user does not subscribe to this forum
591                 }
593                 // Don't send email if the forum is Q&A and the user has not posted
594                 // Initial topics are still mailed
595                 if ($forum->type == 'qanda' && !forum_get_user_posted_time($discussion->id, $userto->id) && $pid != $discussion->firstpost) {
596                     mtrace('Did not email '.$userto->id.' because user has not posted in discussion');
597                     continue;
598                 }
600                 // Get info about the sending user
601                 if (array_key_exists($post->userid, $users)) { // we might know him/her already
602                     $userfrom = $users[$post->userid];
603                     if (!isset($userfrom->idnumber)) {
604                         // Minimalised user info, fetch full record.
605                         $userfrom = $DB->get_record('user', array('id' => $userfrom->id));
606                         forum_cron_minimise_user_record($userfrom);
607                     }
609                 } else if ($userfrom = $DB->get_record('user', array('id' => $post->userid))) {
610                     forum_cron_minimise_user_record($userfrom);
611                     // Fetch only once if possible, we can add it to user list, it will be skipped anyway.
612                     if ($userscount <= FORUM_CRON_USER_CACHE) {
613                         $userscount++;
614                         $users[$userfrom->id] = $userfrom;
615                     }
617                 } else {
618                     mtrace('Could not find user '.$post->userid);
619                     continue;
620                 }
622                 //if we want to check that userto and userfrom are not the same person this is probably the spot to do it
624                 // setup global $COURSE properly - needed for roles and languages
625                 cron_setup_user($userto, $course);
627                 // Fill caches
628                 if (!isset($userto->viewfullnames[$forum->id])) {
629                     $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
630                     $userto->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $modcontext);
631                 }
632                 if (!isset($userto->canpost[$discussion->id])) {
633                     $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
634                     $userto->canpost[$discussion->id] = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
635                 }
636                 if (!isset($userfrom->groups[$forum->id])) {
637                     if (!isset($userfrom->groups)) {
638                         $userfrom->groups = array();
639                         if (isset($users[$userfrom->id])) {
640                             $users[$userfrom->id]->groups = array();
641                         }
642                     }
643                     $userfrom->groups[$forum->id] = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid);
644                     if (isset($users[$userfrom->id])) {
645                         $users[$userfrom->id]->groups[$forum->id] = $userfrom->groups[$forum->id];
646                     }
647                 }
649                 // Make sure groups allow this user to see this email
650                 if ($discussion->groupid > 0 and $groupmode = groups_get_activity_groupmode($cm, $course)) {   // Groups are being used
651                     if (!groups_group_exists($discussion->groupid)) { // Can't find group
652                         continue;                           // Be safe and don't send it to anyone
653                     }
655                     if (!groups_is_member($discussion->groupid) and !has_capability('moodle/site:accessallgroups', $modcontext)) {
656                         // do not send posts from other groups when in SEPARATEGROUPS or VISIBLEGROUPS
657                         continue;
658                     }
659                 }
661                 // Make sure we're allowed to see it...
662                 if (!forum_user_can_see_post($forum, $discussion, $post, NULL, $cm)) {
663                     mtrace('user '.$userto->id. ' can not see '.$post->id);
664                     continue;
665                 }
667                 // OK so we need to send the email.
669                 // Does the user want this post in a digest?  If so postpone it for now.
670                 if ($userto->maildigest > 0) {
671                     // This user wants the mails to be in digest form
672                     $queue = new stdClass();
673                     $queue->userid       = $userto->id;
674                     $queue->discussionid = $discussion->id;
675                     $queue->postid       = $post->id;
676                     $queue->timemodified = $post->created;
677                     $DB->insert_record('forum_queue', $queue);
678                     continue;
679                 }
682                 // Prepare to actually send the post now, and build up the content
684                 $cleanforumname = str_replace('"', "'", strip_tags(format_string($forum->name)));
686                 $userfrom->customheaders = array (  // Headers to make emails easier to track
687                            'Precedence: Bulk',
688                            'List-Id: "'.$cleanforumname.'" <moodleforum'.$forum->id.'@'.$hostname.'>',
689                            'List-Help: '.$CFG->wwwroot.'/mod/forum/view.php?f='.$forum->id,
690                            'Message-ID: '.forum_get_email_message_id($post->id, $userto->id, $hostname),
691                            'X-Course-Id: '.$course->id,
692                            'X-Course-Name: '.format_string($course->fullname, true)
693                 );
695                 if ($post->parent) {  // This post is a reply, so add headers for threading (see MDL-22551)
696                     $userfrom->customheaders[] = 'In-Reply-To: '.forum_get_email_message_id($post->parent, $userto->id, $hostname);
697                     $userfrom->customheaders[] = 'References: '.forum_get_email_message_id($post->parent, $userto->id, $hostname);
698                 }
700                 $shortname = format_string($course->shortname, true, array('context' => get_context_instance(CONTEXT_COURSE, $course->id)));
702                 $postsubject = html_to_text("$shortname: ".format_string($post->subject, true));
703                 $posttext = forum_make_mail_text($course, $cm, $forum, $discussion, $post, $userfrom, $userto);
704                 $posthtml = forum_make_mail_html($course, $cm, $forum, $discussion, $post, $userfrom, $userto);
706                 // Send the post now!
708                 mtrace('Sending ', '');
710                 $eventdata = new stdClass();
711                 $eventdata->component        = 'mod_forum';
712                 $eventdata->name             = 'posts';
713                 $eventdata->userfrom         = $userfrom;
714                 $eventdata->userto           = $userto;
715                 $eventdata->subject          = $postsubject;
716                 $eventdata->fullmessage      = $posttext;
717                 $eventdata->fullmessageformat = FORMAT_PLAIN;
718                 $eventdata->fullmessagehtml  = $posthtml;
719                 $eventdata->notification = 1;
721                 // If forum_replytouser is not set then send mail using the noreplyaddress.
722                 if (empty($CFG->forum_replytouser)) {
723                     // Clone userfrom as it is referenced by $users.
724                     $cloneduserfrom = clone($userfrom);
725                     $cloneduserfrom->email = $CFG->noreplyaddress;
726                     $eventdata->userfrom = $cloneduserfrom;
727                 }
729                 $smallmessagestrings = new stdClass();
730                 $smallmessagestrings->user = fullname($userfrom);
731                 $smallmessagestrings->forumname = "$shortname: ".format_string($forum->name,true).": ".$discussion->name;
732                 $smallmessagestrings->message = $post->message;
733                 //make sure strings are in message recipients language
734                 $eventdata->smallmessage = get_string_manager()->get_string('smallmessage', 'forum', $smallmessagestrings, $userto->lang);
736                 $eventdata->contexturl = "{$CFG->wwwroot}/mod/forum/discuss.php?d={$discussion->id}#p{$post->id}";
737                 $eventdata->contexturlname = $discussion->name;
739                 $mailresult = message_send($eventdata);
740                 if (!$mailresult){
741                     mtrace("Error: mod/forum/lib.php forum_cron(): Could not send out mail for id $post->id to user $userto->id".
742                          " ($userto->email) .. not trying again.");
743                     add_to_log($course->id, 'forum', 'mail error', "discuss.php?d=$discussion->id#p$post->id",
744                                substr(format_string($post->subject,true),0,30), $cm->id, $userto->id);
745                     $errorcount[$post->id]++;
746                 } else {
747                     $mailcount[$post->id]++;
749                 // Mark post as read if forum_usermarksread is set off
750                     if (!$CFG->forum_usermarksread) {
751                         $userto->markposts[$post->id] = $post->id;
752                     }
753                 }
755                 mtrace('post '.$post->id. ': '.$post->subject);
756             }
758             // mark processed posts as read
759             forum_tp_mark_posts_read($userto, $userto->markposts);
760             unset($userto);
761         }
762     }
764     if ($posts) {
765         foreach ($posts as $post) {
766             mtrace($mailcount[$post->id]." users were sent post $post->id, '$post->subject'");
767             if ($errorcount[$post->id]) {
768                 $DB->set_field("forum_posts", "mailed", "2", array("id" => "$post->id"));
769             }
770         }
771     }
773     // release some memory
774     unset($subscribedusers);
775     unset($mailcount);
776     unset($errorcount);
778     cron_setup_user();
780     $sitetimezone = $CFG->timezone;
782     // Now see if there are any digest mails waiting to be sent, and if we should send them
784     mtrace('Starting digest processing...');
786     @set_time_limit(300); // terminate if not able to fetch all digests in 5 minutes
788     if (!isset($CFG->digestmailtimelast)) {    // To catch the first time
789         set_config('digestmailtimelast', 0);
790     }
792     $timenow = time();
793     $digesttime = usergetmidnight($timenow, $sitetimezone) + ($CFG->digestmailtime * 3600);
795     // Delete any really old ones (normally there shouldn't be any)
796     $weekago = $timenow - (7 * 24 * 3600);
797     $DB->delete_records_select('forum_queue', "timemodified < ?", array($weekago));
798     mtrace ('Cleaned old digest records');
800     if ($CFG->digestmailtimelast < $digesttime and $timenow > $digesttime) {
802         mtrace('Sending forum digests: '.userdate($timenow, '', $sitetimezone));
804         $digestposts_rs = $DB->get_recordset_select('forum_queue', "timemodified < ?", array($digesttime));
806         if ($digestposts_rs->valid()) {
808             // We have work to do
809             $usermailcount = 0;
811             //caches - reuse the those filled before too
812             $discussionposts = array();
813             $userdiscussions = array();
815             foreach ($digestposts_rs as $digestpost) {
816                 if (!isset($posts[$digestpost->postid])) {
817                     if ($post = $DB->get_record('forum_posts', array('id' => $digestpost->postid))) {
818                         $posts[$digestpost->postid] = $post;
819                     } else {
820                         continue;
821                     }
822                 }
823                 $discussionid = $digestpost->discussionid;
824                 if (!isset($discussions[$discussionid])) {
825                     if ($discussion = $DB->get_record('forum_discussions', array('id' => $discussionid))) {
826                         $discussions[$discussionid] = $discussion;
827                     } else {
828                         continue;
829                     }
830                 }
831                 $forumid = $discussions[$discussionid]->forum;
832                 if (!isset($forums[$forumid])) {
833                     if ($forum = $DB->get_record('forum', array('id' => $forumid))) {
834                         $forums[$forumid] = $forum;
835                     } else {
836                         continue;
837                     }
838                 }
840                 $courseid = $forums[$forumid]->course;
841                 if (!isset($courses[$courseid])) {
842                     if ($course = $DB->get_record('course', array('id' => $courseid))) {
843                         $courses[$courseid] = $course;
844                     } else {
845                         continue;
846                     }
847                 }
849                 if (!isset($coursemodules[$forumid])) {
850                     if ($cm = get_coursemodule_from_instance('forum', $forumid, $courseid)) {
851                         $coursemodules[$forumid] = $cm;
852                     } else {
853                         continue;
854                     }
855                 }
856                 $userdiscussions[$digestpost->userid][$digestpost->discussionid] = $digestpost->discussionid;
857                 $discussionposts[$digestpost->discussionid][$digestpost->postid] = $digestpost->postid;
858             }
859             $digestposts_rs->close(); /// Finished iteration, let's close the resultset
861             // Data collected, start sending out emails to each user
862             foreach ($userdiscussions as $userid => $thesediscussions) {
864                 @set_time_limit(120); // terminate if processing of any account takes longer than 2 minutes
866                 cron_setup_user();
868                 mtrace(get_string('processingdigest', 'forum', $userid), '... ');
870                 // First of all delete all the queue entries for this user
871                 $DB->delete_records_select('forum_queue', "userid = ? AND timemodified < ?", array($userid, $digesttime));
873                 // Init user caches - we keep the cache for one cycle only,
874                 // otherwise it would unnecessarily consume memory.
875                 if (array_key_exists($userid, $users) and isset($users[$userid]->username)) {
876                     $userto = clone($users[$userid]);
877                 } else {
878                     $userto = $DB->get_record('user', array('id' => $userid));
879                     forum_cron_minimise_user_record($userto);
880                 }
881                 $userto->viewfullnames = array();
882                 $userto->canpost       = array();
883                 $userto->markposts     = array();
885                 // Override the language and timezone of the "current" user, so that
886                 // mail is customised for the receiver.
887                 cron_setup_user($userto);
889                 $postsubject = get_string('digestmailsubject', 'forum', format_string($site->shortname, true));
891                 $headerdata = new stdClass();
892                 $headerdata->sitename = format_string($site->fullname, true);
893                 $headerdata->userprefs = $CFG->wwwroot.'/user/edit.php?id='.$userid.'&amp;course='.$site->id;
895                 $posttext = get_string('digestmailheader', 'forum', $headerdata)."\n\n";
896                 $headerdata->userprefs = '<a target="_blank" href="'.$headerdata->userprefs.'">'.get_string('digestmailprefs', 'forum').'</a>';
898                 $posthtml = "<head>";
899 /*                foreach ($CFG->stylesheets as $stylesheet) {
900                     //TODO: MDL-21120
901                     $posthtml .= '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'" />'."\n";
902                 }*/
903                 $posthtml .= "</head>\n<body id=\"email\">\n";
904                 $posthtml .= '<p>'.get_string('digestmailheader', 'forum', $headerdata).'</p><br /><hr size="1" noshade="noshade" />';
906                 foreach ($thesediscussions as $discussionid) {
908                     @set_time_limit(120);   // to be reset for each post
910                     $discussion = $discussions[$discussionid];
911                     $forum      = $forums[$discussion->forum];
912                     $course     = $courses[$forum->course];
913                     $cm         = $coursemodules[$forum->id];
915                     //override language
916                     cron_setup_user($userto, $course);
918                     // Fill caches
919                     if (!isset($userto->viewfullnames[$forum->id])) {
920                         $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
921                         $userto->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $modcontext);
922                     }
923                     if (!isset($userto->canpost[$discussion->id])) {
924                         $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
925                         $userto->canpost[$discussion->id] = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
926                     }
928                     $strforums      = get_string('forums', 'forum');
929                     $canunsubscribe = ! forum_is_forcesubscribed($forum);
930                     $canreply       = $userto->canpost[$discussion->id];
931                     $shortname = format_string($course->shortname, true, array('context' => get_context_instance(CONTEXT_COURSE, $course->id)));
933                     $posttext .= "\n \n";
934                     $posttext .= '=====================================================================';
935                     $posttext .= "\n \n";
936                     $posttext .= "$shortname -> $strforums -> ".format_string($forum->name,true);
937                     if ($discussion->name != $forum->name) {
938                         $posttext  .= " -> ".format_string($discussion->name,true);
939                     }
940                     $posttext .= "\n";
942                     $posthtml .= "<p><font face=\"sans-serif\">".
943                     "<a target=\"_blank\" href=\"$CFG->wwwroot/course/view.php?id=$course->id\">$shortname</a> -> ".
944                     "<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/index.php?id=$course->id\">$strforums</a> -> ".
945                     "<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/view.php?f=$forum->id\">".format_string($forum->name,true)."</a>";
946                     if ($discussion->name == $forum->name) {
947                         $posthtml .= "</font></p>";
948                     } else {
949                         $posthtml .= " -> <a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/discuss.php?d=$discussion->id\">".format_string($discussion->name,true)."</a></font></p>";
950                     }
951                     $posthtml .= '<p>';
953                     $postsarray = $discussionposts[$discussionid];
954                     sort($postsarray);
956                     foreach ($postsarray as $postid) {
957                         $post = $posts[$postid];
959                         if (array_key_exists($post->userid, $users)) { // we might know him/her already
960                             $userfrom = $users[$post->userid];
961                             if (!isset($userfrom->idnumber)) {
962                                 $userfrom = $DB->get_record('user', array('id' => $userfrom->id));
963                                 forum_cron_minimise_user_record($userfrom);
964                             }
966                         } else if ($userfrom = $DB->get_record('user', array('id' => $post->userid))) {
967                             forum_cron_minimise_user_record($userfrom);
968                             if ($userscount <= FORUM_CRON_USER_CACHE) {
969                                 $userscount++;
970                                 $users[$userfrom->id] = $userfrom;
971                             }
973                         } else {
974                             mtrace('Could not find user '.$post->userid);
975                             continue;
976                         }
978                         if (!isset($userfrom->groups[$forum->id])) {
979                             if (!isset($userfrom->groups)) {
980                                 $userfrom->groups = array();
981                                 if (isset($users[$userfrom->id])) {
982                                     $users[$userfrom->id]->groups = array();
983                                 }
984                             }
985                             $userfrom->groups[$forum->id] = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid);
986                             if (isset($users[$userfrom->id])) {
987                                 $users[$userfrom->id]->groups[$forum->id] = $userfrom->groups[$forum->id];
988                             }
989                         }
991                         $userfrom->customheaders = array ("Precedence: Bulk");
993                         if ($userto->maildigest == 2) {
994                             // Subjects only
995                             $by = new stdClass();
996                             $by->name = fullname($userfrom);
997                             $by->date = userdate($post->modified);
998                             $posttext .= "\n".format_string($post->subject,true).' '.get_string("bynameondate", "forum", $by);
999                             $posttext .= "\n---------------------------------------------------------------------";
1001                             $by->name = "<a target=\"_blank\" href=\"$CFG->wwwroot/user/view.php?id=$userfrom->id&amp;course=$course->id\">$by->name</a>";
1002                             $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>';
1004                         } else {
1005                             // The full treatment
1006                             $posttext .= forum_make_mail_text($course, $cm, $forum, $discussion, $post, $userfrom, $userto, true);
1007                             $posthtml .= forum_make_mail_post($course, $cm, $forum, $discussion, $post, $userfrom, $userto, false, $canreply, true, false);
1009                         // Create an array of postid's for this user to mark as read.
1010                             if (!$CFG->forum_usermarksread) {
1011                                 $userto->markposts[$post->id] = $post->id;
1012                             }
1013                         }
1014                     }
1015                     if ($canunsubscribe) {
1016                         $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>";
1017                     } else {
1018                         $posthtml .= "\n<div class='mdl-right'><font size=\"1\">".get_string("everyoneissubscribed", "forum")."</font></div>";
1019                     }
1020                     $posthtml .= '<hr size="1" noshade="noshade" /></p>';
1021                 }
1022                 $posthtml .= '</body>';
1024                 if (empty($userto->mailformat) || $userto->mailformat != 1) {
1025                     // This user DOESN'T want to receive HTML
1026                     $posthtml = '';
1027                 }
1029                 $attachment = $attachname='';
1030                 // Directly email forum digests rather than sending them via messaging, use the
1031                 // site shortname as 'from name', the noreply address will be used by email_to_user.
1032                 $mailresult = email_to_user($userto, $site->shortname, $postsubject, $posttext, $posthtml, $attachment, $attachname);
1034                 if (!$mailresult) {
1035                     mtrace("ERROR!");
1036                     echo "Error: mod/forum/cron.php: Could not send out digest mail to user $userto->id ($userto->email)... not trying again.\n";
1037                     add_to_log($course->id, 'forum', 'mail digest error', '', '', $cm->id, $userto->id);
1038                 } else {
1039                     mtrace("success.");
1040                     $usermailcount++;
1042                     // Mark post as read if forum_usermarksread is set off
1043                     forum_tp_mark_posts_read($userto, $userto->markposts);
1044                 }
1045             }
1046         }
1047     /// We have finishied all digest emails, update $CFG->digestmailtimelast
1048         set_config('digestmailtimelast', $timenow);
1049     }
1051     cron_setup_user();
1053     if (!empty($usermailcount)) {
1054         mtrace(get_string('digestsentusers', 'forum', $usermailcount));
1055     }
1057     if (!empty($CFG->forum_lastreadclean)) {
1058         $timenow = time();
1059         if ($CFG->forum_lastreadclean + (24*3600) < $timenow) {
1060             set_config('forum_lastreadclean', $timenow);
1061             mtrace('Removing old forum read tracking info...');
1062             forum_tp_clean_read_records();
1063         }
1064     } else {
1065         set_config('forum_lastreadclean', time());
1066     }
1069     return true;
1072 /**
1073  * Builds and returns the body of the email notification in plain text.
1074  *
1075  * @global object
1076  * @global object
1077  * @uses CONTEXT_MODULE
1078  * @param object $course
1079  * @param object $cm
1080  * @param object $forum
1081  * @param object $discussion
1082  * @param object $post
1083  * @param object $userfrom
1084  * @param object $userto
1085  * @param boolean $bare
1086  * @return string The email body in plain text format.
1087  */
1088 function forum_make_mail_text($course, $cm, $forum, $discussion, $post, $userfrom, $userto, $bare = false) {
1089     global $CFG, $USER;
1091     $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
1093     if (!isset($userto->viewfullnames[$forum->id])) {
1094         $viewfullnames = has_capability('moodle/site:viewfullnames', $modcontext, $userto->id);
1095     } else {
1096         $viewfullnames = $userto->viewfullnames[$forum->id];
1097     }
1099     if (!isset($userto->canpost[$discussion->id])) {
1100         $canreply = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
1101     } else {
1102         $canreply = $userto->canpost[$discussion->id];
1103     }
1105     $by = New stdClass;
1106     $by->name = fullname($userfrom, $viewfullnames);
1107     $by->date = userdate($post->modified, "", $userto->timezone);
1109     $strbynameondate = get_string('bynameondate', 'forum', $by);
1111     $strforums = get_string('forums', 'forum');
1113     $canunsubscribe = ! forum_is_forcesubscribed($forum);
1115     $posttext = '';
1117     if (!$bare) {
1118         $shortname = format_string($course->shortname, true, array('context' => get_context_instance(CONTEXT_COURSE, $course->id)));
1119         $posttext  = "$shortname -> $strforums -> ".format_string($forum->name,true);
1121         if ($discussion->name != $forum->name) {
1122             $posttext  .= " -> ".format_string($discussion->name,true);
1123         }
1124     }
1126     // add absolute file links
1127     $post->message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php', $modcontext->id, 'mod_forum', 'post', $post->id);
1129     $posttext .= "\n---------------------------------------------------------------------\n";
1130     $posttext .= format_string($post->subject,true);
1131     if ($bare) {
1132         $posttext .= " ($CFG->wwwroot/mod/forum/discuss.php?d=$discussion->id#p$post->id)";
1133     }
1134     $posttext .= "\n".$strbynameondate."\n";
1135     $posttext .= "---------------------------------------------------------------------\n";
1136     $posttext .= format_text_email($post->message, $post->messageformat);
1137     $posttext .= "\n\n";
1138     $posttext .= forum_print_attachments($post, $cm, "text");
1140     if (!$bare && $canreply) {
1141         $posttext .= "---------------------------------------------------------------------\n";
1142         $posttext .= get_string("postmailinfo", "forum", $shortname)."\n";
1143         $posttext .= "$CFG->wwwroot/mod/forum/post.php?reply=$post->id\n";
1144     }
1145     if (!$bare && $canunsubscribe) {
1146         $posttext .= "\n---------------------------------------------------------------------\n";
1147         $posttext .= get_string("unsubscribe", "forum");
1148         $posttext .= ": $CFG->wwwroot/mod/forum/subscribe.php?id=$forum->id\n";
1149     }
1151     return $posttext;
1154 /**
1155  * Builds and returns the body of the email notification in html format.
1156  *
1157  * @global object
1158  * @param object $course
1159  * @param object $cm
1160  * @param object $forum
1161  * @param object $discussion
1162  * @param object $post
1163  * @param object $userfrom
1164  * @param object $userto
1165  * @return string The email text in HTML format
1166  */
1167 function forum_make_mail_html($course, $cm, $forum, $discussion, $post, $userfrom, $userto) {
1168     global $CFG;
1170     if ($userto->mailformat != 1) {  // Needs to be HTML
1171         return '';
1172     }
1174     if (!isset($userto->canpost[$discussion->id])) {
1175         $canreply = forum_user_can_post($forum, $discussion, $userto, $cm, $course);
1176     } else {
1177         $canreply = $userto->canpost[$discussion->id];
1178     }
1180     $strforums = get_string('forums', 'forum');
1181     $canunsubscribe = ! forum_is_forcesubscribed($forum);
1182     $shortname = format_string($course->shortname, true, array('context' => get_context_instance(CONTEXT_COURSE, $course->id)));
1184     $posthtml = '<head>';
1185 /*    foreach ($CFG->stylesheets as $stylesheet) {
1186         //TODO: MDL-21120
1187         $posthtml .= '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'" />'."\n";
1188     }*/
1189     $posthtml .= '</head>';
1190     $posthtml .= "\n<body id=\"email\">\n\n";
1192     $posthtml .= '<div class="navbar">'.
1193     '<a target="_blank" href="'.$CFG->wwwroot.'/course/view.php?id='.$course->id.'">'.$shortname.'</a> &raquo; '.
1194     '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/index.php?id='.$course->id.'">'.$strforums.'</a> &raquo; '.
1195     '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/view.php?f='.$forum->id.'">'.format_string($forum->name,true).'</a>';
1196     if ($discussion->name == $forum->name) {
1197         $posthtml .= '</div>';
1198     } else {
1199         $posthtml .= ' &raquo; <a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$discussion->id.'">'.
1200                      format_string($discussion->name,true).'</a></div>';
1201     }
1202     $posthtml .= forum_make_mail_post($course, $cm, $forum, $discussion, $post, $userfrom, $userto, false, $canreply, true, false);
1204     if ($canunsubscribe) {
1205         $posthtml .= '<hr /><div class="mdl-align unsubscribelink">
1206                       <a href="'.$CFG->wwwroot.'/mod/forum/subscribe.php?id='.$forum->id.'">'.get_string('unsubscribe', 'forum').'</a>&nbsp;
1207                       <a href="'.$CFG->wwwroot.'/mod/forum/unsubscribeall.php">'.get_string('unsubscribeall', 'forum').'</a></div>';
1208     }
1210     $posthtml .= '</body>';
1212     return $posthtml;
1216 /**
1217  *
1218  * @param object $course
1219  * @param object $user
1220  * @param object $mod TODO this is not used in this function, refactor
1221  * @param object $forum
1222  * @return object A standard object with 2 variables: info (number of posts for this user) and time (last modified)
1223  */
1224 function forum_user_outline($course, $user, $mod, $forum) {
1225     global $CFG;
1226     require_once("$CFG->libdir/gradelib.php");
1227     $grades = grade_get_grades($course->id, 'mod', 'forum', $forum->id, $user->id);
1228     if (empty($grades->items[0]->grades)) {
1229         $grade = false;
1230     } else {
1231         $grade = reset($grades->items[0]->grades);
1232     }
1234     $count = forum_count_user_posts($forum->id, $user->id);
1236     if ($count && $count->postcount > 0) {
1237         $result = new stdClass();
1238         $result->info = get_string("numposts", "forum", $count->postcount);
1239         $result->time = $count->lastpost;
1240         if ($grade) {
1241             $result->info .= ', ' . get_string('grade') . ': ' . $grade->str_long_grade;
1242         }
1243         return $result;
1244     } else if ($grade) {
1245         $result = new stdClass();
1246         $result->info = get_string('grade') . ': ' . $grade->str_long_grade;
1248         //datesubmitted == time created. dategraded == time modified or time overridden
1249         //if grade was last modified by the user themselves use date graded. Otherwise use date submitted
1250         //TODO: move this copied & pasted code somewhere in the grades API. See MDL-26704
1251         if ($grade->usermodified == $user->id || empty($grade->datesubmitted)) {
1252             $result->time = $grade->dategraded;
1253         } else {
1254             $result->time = $grade->datesubmitted;
1255         }
1257         return $result;
1258     }
1259     return NULL;
1263 /**
1264  * @global object
1265  * @global object
1266  * @param object $coure
1267  * @param object $user
1268  * @param object $mod
1269  * @param object $forum
1270  */
1271 function forum_user_complete($course, $user, $mod, $forum) {
1272     global $CFG,$USER, $OUTPUT;
1273     require_once("$CFG->libdir/gradelib.php");
1275     $grades = grade_get_grades($course->id, 'mod', 'forum', $forum->id, $user->id);
1276     if (!empty($grades->items[0]->grades)) {
1277         $grade = reset($grades->items[0]->grades);
1278         echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade);
1279         if ($grade->str_feedback) {
1280             echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
1281         }
1282     }
1284     if ($posts = forum_get_user_posts($forum->id, $user->id)) {
1286         if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $course->id)) {
1287             print_error('invalidcoursemodule');
1288         }
1289         $discussions = forum_get_user_involved_discussions($forum->id, $user->id);
1291         foreach ($posts as $post) {
1292             if (!isset($discussions[$post->discussion])) {
1293                 continue;
1294             }
1295             $discussion = $discussions[$post->discussion];
1297             forum_print_post($post, $discussion, $forum, $cm, $course, false, false, false);
1298         }
1299     } else {
1300         echo "<p>".get_string("noposts", "forum")."</p>";
1301     }
1309 /**
1310  * @global object
1311  * @global object
1312  * @global object
1313  * @param array $courses
1314  * @param array $htmlarray
1315  */
1316 function forum_print_overview($courses,&$htmlarray) {
1317     global $USER, $CFG, $DB, $SESSION;
1319     if (empty($courses) || !is_array($courses) || count($courses) == 0) {
1320         return array();
1321     }
1323     if (!$forums = get_all_instances_in_courses('forum',$courses)) {
1324         return;
1325     }
1327     // Courses to search for new posts
1328     $coursessqls = array();
1329     $params = array();
1330     foreach ($courses as $course) {
1332         // If the user has never entered into the course all posts are pending
1333         if ($course->lastaccess == 0) {
1334             $coursessqls[] = '(f.course = ?)';
1335             $params[] = $course->id;
1337         // Only posts created after the course last access
1338         } else {
1339             $coursessqls[] = '(f.course = ? AND p.created > ?)';
1340             $params[] = $course->id;
1341             $params[] = $course->lastaccess;
1342         }
1343     }
1344     $params[] = $USER->id;
1345     $coursessql = implode(' OR ', $coursessqls);
1347     $sql = "SELECT f.id, COUNT(*) as count "
1348                 .'FROM {forum} f '
1349                 .'JOIN {forum_discussions} d ON d.forum  = f.id '
1350                 .'JOIN {forum_posts} p ON p.discussion = d.id '
1351                 ."WHERE ($coursessql) "
1352                 .'AND p.userid != ? '
1353                 .'GROUP BY f.id';
1355     if (!$new = $DB->get_records_sql($sql, $params)) {
1356         $new = array(); // avoid warnings
1357     }
1359     // also get all forum tracking stuff ONCE.
1360     $trackingforums = array();
1361     foreach ($forums as $forum) {
1362         if (forum_tp_can_track_forums($forum)) {
1363             $trackingforums[$forum->id] = $forum;
1364         }
1365     }
1367     if (count($trackingforums) > 0) {
1368         $cutoffdate = isset($CFG->forum_oldpostdays) ? (time() - ($CFG->forum_oldpostdays*24*60*60)) : 0;
1369         $sql = 'SELECT d.forum,d.course,COUNT(p.id) AS count '.
1370             ' FROM {forum_posts} p '.
1371             ' JOIN {forum_discussions} d ON p.discussion = d.id '.
1372             ' LEFT JOIN {forum_read} r ON r.postid = p.id AND r.userid = ? WHERE (';
1373         $params = array($USER->id);
1375         foreach ($trackingforums as $track) {
1376             $sql .= '(d.forum = ? AND (d.groupid = -1 OR d.groupid = 0 OR d.groupid = ?)) OR ';
1377             $params[] = $track->id;
1378             if (isset($SESSION->currentgroup[$track->course])) {
1379                 $groupid =  $SESSION->currentgroup[$track->course];
1380             } else {
1381                 // get first groupid
1382                 $groupids = groups_get_all_groups($track->course, $USER->id);
1383                 if ($groupids) {
1384                     reset($groupids);
1385                     $groupid = key($groupids);
1386                     $SESSION->currentgroup[$track->course] = $groupid;
1387                 } else {
1388                     $groupid = 0;
1389                 }
1390                 unset($groupids);
1391             }
1392             $params[] = $groupid;
1393         }
1394         $sql = substr($sql,0,-3); // take off the last OR
1395         $sql .= ') AND p.modified >= ? AND r.id is NULL GROUP BY d.forum,d.course';
1396         $params[] = $cutoffdate;
1398         if (!$unread = $DB->get_records_sql($sql, $params)) {
1399             $unread = array();
1400         }
1401     } else {
1402         $unread = array();
1403     }
1405     if (empty($unread) and empty($new)) {
1406         return;
1407     }
1409     $strforum = get_string('modulename','forum');
1411     foreach ($forums as $forum) {
1412         $str = '';
1413         $count = 0;
1414         $thisunread = 0;
1415         $showunread = false;
1416         // either we have something from logs, or trackposts, or nothing.
1417         if (array_key_exists($forum->id, $new) && !empty($new[$forum->id])) {
1418             $count = $new[$forum->id]->count;
1419         }
1420         if (array_key_exists($forum->id,$unread)) {
1421             $thisunread = $unread[$forum->id]->count;
1422             $showunread = true;
1423         }
1424         if ($count > 0 || $thisunread > 0) {
1425             $str .= '<div class="overview forum"><div class="name">'.$strforum.': <a title="'.$strforum.'" href="'.$CFG->wwwroot.'/mod/forum/view.php?f='.$forum->id.'">'.
1426                 $forum->name.'</a></div>';
1427             $str .= '<div class="info"><span class="postsincelogin">';
1428             $str .= get_string('overviewnumpostssince', 'forum', $count)."</span>";
1429             if (!empty($showunread)) {
1430                 $str .= '<div class="unreadposts">'.get_string('overviewnumunread', 'forum', $thisunread).'</div>';
1431             }
1432             $str .= '</div></div>';
1433         }
1434         if (!empty($str)) {
1435             if (!array_key_exists($forum->course,$htmlarray)) {
1436                 $htmlarray[$forum->course] = array();
1437             }
1438             if (!array_key_exists('forum',$htmlarray[$forum->course])) {
1439                 $htmlarray[$forum->course]['forum'] = ''; // initialize, avoid warnings
1440             }
1441             $htmlarray[$forum->course]['forum'] .= $str;
1442         }
1443     }
1446 /**
1447  * Given a course and a date, prints a summary of all the new
1448  * messages posted in the course since that date
1449  *
1450  * @global object
1451  * @global object
1452  * @global object
1453  * @uses CONTEXT_MODULE
1454  * @uses VISIBLEGROUPS
1455  * @param object $course
1456  * @param bool $viewfullnames capability
1457  * @param int $timestart
1458  * @return bool success
1459  */
1460 function forum_print_recent_activity($course, $viewfullnames, $timestart) {
1461     global $CFG, $USER, $DB, $OUTPUT;
1463     // do not use log table if possible, it may be huge and is expensive to join with other tables
1465     if (!$posts = $DB->get_records_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid,
1466                                               d.timestart, d.timeend, d.userid AS duserid,
1467                                               u.firstname, u.lastname, u.email, u.picture
1468                                          FROM {forum_posts} p
1469                                               JOIN {forum_discussions} d ON d.id = p.discussion
1470                                               JOIN {forum} f             ON f.id = d.forum
1471                                               JOIN {user} u              ON u.id = p.userid
1472                                         WHERE p.created > ? AND f.course = ?
1473                                      ORDER BY p.id ASC", array($timestart, $course->id))) { // order by initial posting date
1474          return false;
1475     }
1477     $modinfo = get_fast_modinfo($course);
1479     $groupmodes = array();
1480     $cms    = array();
1482     $strftimerecent = get_string('strftimerecent');
1484     $printposts = array();
1485     foreach ($posts as $post) {
1486         if (!isset($modinfo->instances['forum'][$post->forum])) {
1487             // not visible
1488             continue;
1489         }
1490         $cm = $modinfo->instances['forum'][$post->forum];
1491         if (!$cm->uservisible) {
1492             continue;
1493         }
1494         $context = get_context_instance(CONTEXT_MODULE, $cm->id);
1496         if (!has_capability('mod/forum:viewdiscussion', $context)) {
1497             continue;
1498         }
1500         if (!empty($CFG->forum_enabletimedposts) and $USER->id != $post->duserid
1501           and (($post->timestart > 0 and $post->timestart > time()) or ($post->timeend > 0 and $post->timeend < time()))) {
1502             if (!has_capability('mod/forum:viewhiddentimedposts', $context)) {
1503                 continue;
1504             }
1505         }
1507         $groupmode = groups_get_activity_groupmode($cm, $course);
1509         if ($groupmode) {
1510             if ($post->groupid == -1 or $groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $context)) {
1511                 // oki (Open discussions have groupid -1)
1512             } else {
1513                 // separate mode
1514                 if (isguestuser()) {
1515                     // shortcut
1516                     continue;
1517                 }
1519                 if (is_null($modinfo->groups)) {
1520                     $modinfo->groups = groups_get_user_groups($course->id); // load all my groups and cache it in modinfo
1521                 }
1523                 if (!array_key_exists($post->groupid, $modinfo->groups[0])) {
1524                     continue;
1525                 }
1526             }
1527         }
1529         $printposts[] = $post;
1530     }
1531     unset($posts);
1533     if (!$printposts) {
1534         return false;
1535     }
1537     echo $OUTPUT->heading(get_string('newforumposts', 'forum').':', 3);
1538     echo "\n<ul class='unlist'>\n";
1540     foreach ($printposts as $post) {
1541         $subjectclass = empty($post->parent) ? ' bold' : '';
1543         echo '<li><div class="head">'.
1544                '<div class="date">'.userdate($post->modified, $strftimerecent).'</div>'.
1545                '<div class="name">'.fullname($post, $viewfullnames).'</div>'.
1546              '</div>';
1547         echo '<div class="info'.$subjectclass.'">';
1548         if (empty($post->parent)) {
1549             echo '"<a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'">';
1550         } else {
1551             echo '"<a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'&amp;parent='.$post->parent.'#p'.$post->id.'">';
1552         }
1553         $post->subject = break_up_long_words(format_string($post->subject, true));
1554         echo $post->subject;
1555         echo "</a>\"</div></li>\n";
1556     }
1558     echo "</ul>\n";
1560     return true;
1563 /**
1564  * Return grade for given user or all users.
1565  *
1566  * @global object
1567  * @global object
1568  * @param object $forum
1569  * @param int $userid optional user id, 0 means all users
1570  * @return array array of grades, false if none
1571  */
1572 function forum_get_user_grades($forum, $userid = 0) {
1573     global $CFG;
1575     require_once($CFG->dirroot.'/rating/lib.php');
1577     $ratingoptions = new stdClass;
1578     $ratingoptions->component = 'mod_forum';
1579     $ratingoptions->ratingarea = 'post';
1581     //need these to work backwards to get a context id. Is there a better way to get contextid from a module instance?
1582     $ratingoptions->modulename = 'forum';
1583     $ratingoptions->moduleid   = $forum->id;
1584     $ratingoptions->userid = $userid;
1585     $ratingoptions->aggregationmethod = $forum->assessed;
1586     $ratingoptions->scaleid = $forum->scale;
1587     $ratingoptions->itemtable = 'forum_posts';
1588     $ratingoptions->itemtableusercolumn = 'userid';
1590     $rm = new rating_manager();
1591     return $rm->get_user_grades($ratingoptions);
1594 /**
1595  * Update activity grades
1596  *
1597  * @category grade
1598  * @param object $forum
1599  * @param int $userid specific user only, 0 means all
1600  * @param boolean $nullifnone return null if grade does not exist
1601  * @return void
1602  */
1603 function forum_update_grades($forum, $userid=0, $nullifnone=true) {
1604     global $CFG, $DB;
1605     require_once($CFG->libdir.'/gradelib.php');
1607     if (!$forum->assessed) {
1608         forum_grade_item_update($forum);
1610     } else if ($grades = forum_get_user_grades($forum, $userid)) {
1611         forum_grade_item_update($forum, $grades);
1613     } else if ($userid and $nullifnone) {
1614         $grade = new stdClass();
1615         $grade->userid   = $userid;
1616         $grade->rawgrade = NULL;
1617         forum_grade_item_update($forum, $grade);
1619     } else {
1620         forum_grade_item_update($forum);
1621     }
1624 /**
1625  * Update all grades in gradebook.
1626  * @global object
1627  */
1628 function forum_upgrade_grades() {
1629     global $DB;
1631     $sql = "SELECT COUNT('x')
1632               FROM {forum} f, {course_modules} cm, {modules} m
1633              WHERE m.name='forum' AND m.id=cm.module AND cm.instance=f.id";
1634     $count = $DB->count_records_sql($sql);
1636     $sql = "SELECT f.*, cm.idnumber AS cmidnumber, f.course AS courseid
1637               FROM {forum} f, {course_modules} cm, {modules} m
1638              WHERE m.name='forum' AND m.id=cm.module AND cm.instance=f.id";
1639     $rs = $DB->get_recordset_sql($sql);
1640     if ($rs->valid()) {
1641         $pbar = new progress_bar('forumupgradegrades', 500, true);
1642         $i=0;
1643         foreach ($rs as $forum) {
1644             $i++;
1645             upgrade_set_timeout(60*5); // set up timeout, may also abort execution
1646             forum_update_grades($forum, 0, false);
1647             $pbar->update($i, $count, "Updating Forum grades ($i/$count).");
1648         }
1649     }
1650     $rs->close();
1653 /**
1654  * Create/update grade item for given forum
1655  *
1656  * @category grade
1657  * @uses GRADE_TYPE_NONE
1658  * @uses GRADE_TYPE_VALUE
1659  * @uses GRADE_TYPE_SCALE
1660  * @param stdClass $forum Forum object with extra cmidnumber
1661  * @param mixed $grades Optional array/object of grade(s); 'reset' means reset grades in gradebook
1662  * @return int 0 if ok
1663  */
1664 function forum_grade_item_update($forum, $grades=NULL) {
1665     global $CFG;
1666     if (!function_exists('grade_update')) { //workaround for buggy PHP versions
1667         require_once($CFG->libdir.'/gradelib.php');
1668     }
1670     $params = array('itemname'=>$forum->name, 'idnumber'=>$forum->cmidnumber);
1672     if (!$forum->assessed or $forum->scale == 0) {
1673         $params['gradetype'] = GRADE_TYPE_NONE;
1675     } else if ($forum->scale > 0) {
1676         $params['gradetype'] = GRADE_TYPE_VALUE;
1677         $params['grademax']  = $forum->scale;
1678         $params['grademin']  = 0;
1680     } else if ($forum->scale < 0) {
1681         $params['gradetype'] = GRADE_TYPE_SCALE;
1682         $params['scaleid']   = -$forum->scale;
1683     }
1685     if ($grades  === 'reset') {
1686         $params['reset'] = true;
1687         $grades = NULL;
1688     }
1690     return grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, $grades, $params);
1693 /**
1694  * Delete grade item for given forum
1695  *
1696  * @category grade
1697  * @param stdClass $forum Forum object
1698  * @return grade_item
1699  */
1700 function forum_grade_item_delete($forum) {
1701     global $CFG;
1702     require_once($CFG->libdir.'/gradelib.php');
1704     return grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, NULL, array('deleted'=>1));
1708 /**
1709  * This function returns if a scale is being used by one forum
1710  *
1711  * @global object
1712  * @param int $forumid
1713  * @param int $scaleid negative number
1714  * @return bool
1715  */
1716 function forum_scale_used ($forumid,$scaleid) {
1717     global $DB;
1718     $return = false;
1720     $rec = $DB->get_record("forum",array("id" => "$forumid","scale" => "-$scaleid"));
1722     if (!empty($rec) && !empty($scaleid)) {
1723         $return = true;
1724     }
1726     return $return;
1729 /**
1730  * Checks if scale is being used by any instance of forum
1731  *
1732  * This is used to find out if scale used anywhere
1733  *
1734  * @global object
1735  * @param $scaleid int
1736  * @return boolean True if the scale is used by any forum
1737  */
1738 function forum_scale_used_anywhere($scaleid) {
1739     global $DB;
1740     if ($scaleid and $DB->record_exists('forum', array('scale' => -$scaleid))) {
1741         return true;
1742     } else {
1743         return false;
1744     }
1747 // SQL FUNCTIONS ///////////////////////////////////////////////////////////
1749 /**
1750  * Gets a post with all info ready for forum_print_post
1751  * Most of these joins are just to get the forum id
1752  *
1753  * @global object
1754  * @global object
1755  * @param int $postid
1756  * @return mixed array of posts or false
1757  */
1758 function forum_get_post_full($postid) {
1759     global $CFG, $DB;
1761     return $DB->get_record_sql("SELECT p.*, d.forum, u.firstname, u.lastname, u.email, u.picture, u.imagealt
1762                              FROM {forum_posts} p
1763                                   JOIN {forum_discussions} d ON p.discussion = d.id
1764                                   LEFT JOIN {user} u ON p.userid = u.id
1765                             WHERE p.id = ?", array($postid));
1768 /**
1769  * Gets posts with all info ready for forum_print_post
1770  * We pass forumid in because we always know it so no need to make a
1771  * complicated join to find it out.
1772  *
1773  * @global object
1774  * @global object
1775  * @return mixed array of posts or false
1776  */
1777 function forum_get_discussion_posts($discussion, $sort, $forumid) {
1778     global $CFG, $DB;
1780     return $DB->get_records_sql("SELECT p.*, $forumid AS forum, u.firstname, u.lastname, u.email, u.picture, u.imagealt
1781                               FROM {forum_posts} p
1782                          LEFT JOIN {user} u ON p.userid = u.id
1783                              WHERE p.discussion = ?
1784                                AND p.parent > 0 $sort", array($discussion));
1787 /**
1788  * Gets all posts in discussion including top parent.
1789  *
1790  * @global object
1791  * @global object
1792  * @global object
1793  * @param int $discussionid
1794  * @param string $sort
1795  * @param bool $tracking does user track the forum?
1796  * @return array of posts
1797  */
1798 function forum_get_all_discussion_posts($discussionid, $sort, $tracking=false) {
1799     global $CFG, $DB, $USER;
1801     $tr_sel  = "";
1802     $tr_join = "";
1803     $params = array();
1805     if ($tracking) {
1806         $now = time();
1807         $cutoffdate = $now - ($CFG->forum_oldpostdays * 24 * 3600);
1808         $tr_sel  = ", fr.id AS postread";
1809         $tr_join = "LEFT JOIN {forum_read} fr ON (fr.postid = p.id AND fr.userid = ?)";
1810         $params[] = $USER->id;
1811     }
1813     $params[] = $discussionid;
1814     if (!$posts = $DB->get_records_sql("SELECT p.*, u.firstname, u.lastname, u.email, u.picture, u.imagealt $tr_sel
1815                                      FROM {forum_posts} p
1816                                           LEFT JOIN {user} u ON p.userid = u.id
1817                                           $tr_join
1818                                     WHERE p.discussion = ?
1819                                  ORDER BY $sort", $params)) {
1820         return array();
1821     }
1823     foreach ($posts as $pid=>$p) {
1824         if ($tracking) {
1825             if (forum_tp_is_post_old($p)) {
1826                  $posts[$pid]->postread = true;
1827             }
1828         }
1829         if (!$p->parent) {
1830             continue;
1831         }
1832         if (!isset($posts[$p->parent])) {
1833             continue; // parent does not exist??
1834         }
1835         if (!isset($posts[$p->parent]->children)) {
1836             $posts[$p->parent]->children = array();
1837         }
1838         $posts[$p->parent]->children[$pid] =& $posts[$pid];
1839     }
1841     return $posts;
1844 /**
1845  * Gets posts with all info ready for forum_print_post
1846  * We pass forumid in because we always know it so no need to make a
1847  * complicated join to find it out.
1848  *
1849  * @global object
1850  * @global object
1851  * @param int $parent
1852  * @param int $forumid
1853  * @return array
1854  */
1855 function forum_get_child_posts($parent, $forumid) {
1856     global $CFG, $DB;
1858     return $DB->get_records_sql("SELECT p.*, $forumid AS forum, u.firstname, u.lastname, u.email, u.picture, u.imagealt
1859                               FROM {forum_posts} p
1860                          LEFT JOIN {user} u ON p.userid = u.id
1861                              WHERE p.parent = ?
1862                           ORDER BY p.created ASC", array($parent));
1865 /**
1866  * An array of forum objects that the user is allowed to read/search through.
1867  *
1868  * @global object
1869  * @global object
1870  * @global object
1871  * @param int $userid
1872  * @param int $courseid if 0, we look for forums throughout the whole site.
1873  * @return array of forum objects, or false if no matches
1874  *         Forum objects have the following attributes:
1875  *         id, type, course, cmid, cmvisible, cmgroupmode, accessallgroups,
1876  *         viewhiddentimedposts
1877  */
1878 function forum_get_readable_forums($userid, $courseid=0) {
1880     global $CFG, $DB, $USER;
1881     require_once($CFG->dirroot.'/course/lib.php');
1883     if (!$forummod = $DB->get_record('modules', array('name' => 'forum'))) {
1884         print_error('notinstalled', 'forum');
1885     }
1887     if ($courseid) {
1888         $courses = $DB->get_records('course', array('id' => $courseid));
1889     } else {
1890         // If no course is specified, then the user can see SITE + his courses.
1891         $courses1 = $DB->get_records('course', array('id' => SITEID));
1892         $courses2 = enrol_get_users_courses($userid, true, array('modinfo'));
1893         $courses = array_merge($courses1, $courses2);
1894     }
1895     if (!$courses) {
1896         return array();
1897     }
1899     $readableforums = array();
1901     foreach ($courses as $course) {
1903         $modinfo = get_fast_modinfo($course);
1904         if (is_null($modinfo->groups)) {
1905             $modinfo->groups = groups_get_user_groups($course->id, $userid);
1906         }
1908         if (empty($modinfo->instances['forum'])) {
1909             // hmm, no forums?
1910             continue;
1911         }
1913         $courseforums = $DB->get_records('forum', array('course' => $course->id));
1915         foreach ($modinfo->instances['forum'] as $forumid => $cm) {
1916             if (!$cm->uservisible or !isset($courseforums[$forumid])) {
1917                 continue;
1918             }
1919             $context = get_context_instance(CONTEXT_MODULE, $cm->id);
1920             $forum = $courseforums[$forumid];
1921             $forum->context = $context;
1922             $forum->cm = $cm;
1924             if (!has_capability('mod/forum:viewdiscussion', $context)) {
1925                 continue;
1926             }
1928          /// group access
1929             if (groups_get_activity_groupmode($cm, $course) == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) {
1930                 if (is_null($modinfo->groups)) {
1931                     $modinfo->groups = groups_get_user_groups($course->id, $USER->id);
1932                 }
1933                 if (isset($modinfo->groups[$cm->groupingid])) {
1934                     $forum->onlygroups = $modinfo->groups[$cm->groupingid];
1935                     $forum->onlygroups[] = -1;
1936                 } else {
1937                     $forum->onlygroups = array(-1);
1938                 }
1939             }
1941         /// hidden timed discussions
1942             $forum->viewhiddentimedposts = true;
1943             if (!empty($CFG->forum_enabletimedposts)) {
1944                 if (!has_capability('mod/forum:viewhiddentimedposts', $context)) {
1945                     $forum->viewhiddentimedposts = false;
1946                 }
1947             }
1949         /// qanda access
1950             if ($forum->type == 'qanda'
1951                     && !has_capability('mod/forum:viewqandawithoutposting', $context)) {
1953                 // We need to check whether the user has posted in the qanda forum.
1954                 $forum->onlydiscussions = array();  // Holds discussion ids for the discussions
1955                                                     // the user is allowed to see in this forum.
1956                 if ($discussionspostedin = forum_discussions_user_has_posted_in($forum->id, $USER->id)) {
1957                     foreach ($discussionspostedin as $d) {
1958                         $forum->onlydiscussions[] = $d->id;
1959                     }
1960                 }
1961             }
1963             $readableforums[$forum->id] = $forum;
1964         }
1966         unset($modinfo);
1968     } // End foreach $courses
1970     return $readableforums;
1973 /**
1974  * Returns a list of posts found using an array of search terms.
1975  *
1976  * @global object
1977  * @global object
1978  * @global object
1979  * @param array $searchterms array of search terms, e.g. word +word -word
1980  * @param int $courseid if 0, we search through the whole site
1981  * @param int $limitfrom
1982  * @param int $limitnum
1983  * @param int &$totalcount
1984  * @param string $extrasql
1985  * @return array|bool Array of posts found or false
1986  */
1987 function forum_search_posts($searchterms, $courseid=0, $limitfrom=0, $limitnum=50,
1988                             &$totalcount, $extrasql='') {
1989     global $CFG, $DB, $USER;
1990     require_once($CFG->libdir.'/searchlib.php');
1992     $forums = forum_get_readable_forums($USER->id, $courseid);
1994     if (count($forums) == 0) {
1995         $totalcount = 0;
1996         return false;
1997     }
1999     $now = round(time(), -2); // db friendly
2001     $fullaccess = array();
2002     $where = array();
2003     $params = array();
2005     foreach ($forums as $forumid => $forum) {
2006         $select = array();
2008         if (!$forum->viewhiddentimedposts) {
2009             $select[] = "(d.userid = :userid{$forumid} OR (d.timestart < :timestart{$forumid} AND (d.timeend = 0 OR d.timeend > :timeend{$forumid})))";
2010             $params = array_merge($params, array('userid'.$forumid=>$USER->id, 'timestart'.$forumid=>$now, 'timeend'.$forumid=>$now));
2011         }
2013         $cm = $forum->cm;
2014         $context = $forum->context;
2016         if ($forum->type == 'qanda'
2017             && !has_capability('mod/forum:viewqandawithoutposting', $context)) {
2018             if (!empty($forum->onlydiscussions)) {
2019                 list($discussionid_sql, $discussionid_params) = $DB->get_in_or_equal($forum->onlydiscussions, SQL_PARAMS_NAMED, 'qanda'.$forumid.'_');
2020                 $params = array_merge($params, $discussionid_params);
2021                 $select[] = "(d.id $discussionid_sql OR p.parent = 0)";
2022             } else {
2023                 $select[] = "p.parent = 0";
2024             }
2025         }
2027         if (!empty($forum->onlygroups)) {
2028             list($groupid_sql, $groupid_params) = $DB->get_in_or_equal($forum->onlygroups, SQL_PARAMS_NAMED, 'grps'.$forumid.'_');
2029             $params = array_merge($params, $groupid_params);
2030             $select[] = "d.groupid $groupid_sql";
2031         }
2033         if ($select) {
2034             $selects = implode(" AND ", $select);
2035             $where[] = "(d.forum = :forum{$forumid} AND $selects)";
2036             $params['forum'.$forumid] = $forumid;
2037         } else {
2038             $fullaccess[] = $forumid;
2039         }
2040     }
2042     if ($fullaccess) {
2043         list($fullid_sql, $fullid_params) = $DB->get_in_or_equal($fullaccess, SQL_PARAMS_NAMED, 'fula');
2044         $params = array_merge($params, $fullid_params);
2045         $where[] = "(d.forum $fullid_sql)";
2046     }
2048     $selectdiscussion = "(".implode(" OR ", $where).")";
2050     $messagesearch = '';
2051     $searchstring = '';
2053     // Need to concat these back together for parser to work.
2054     foreach($searchterms as $searchterm){
2055         if ($searchstring != '') {
2056             $searchstring .= ' ';
2057         }
2058         $searchstring .= $searchterm;
2059     }
2061     // We need to allow quoted strings for the search. The quotes *should* be stripped
2062     // by the parser, but this should be examined carefully for security implications.
2063     $searchstring = str_replace("\\\"","\"",$searchstring);
2064     $parser = new search_parser();
2065     $lexer = new search_lexer($parser);
2067     if ($lexer->parse($searchstring)) {
2068         $parsearray = $parser->get_parsed_array();
2069     // Experimental feature under 1.8! MDL-8830
2070     // Use alternative text searches if defined
2071     // This feature only works under mysql until properly implemented for other DBs
2072     // Requires manual creation of text index for forum_posts before enabling it:
2073     // CREATE FULLTEXT INDEX foru_post_tix ON [prefix]forum_posts (subject, message)
2074     // Experimental feature under 1.8! MDL-8830
2075         if (!empty($CFG->forum_usetextsearches)) {
2076             list($messagesearch, $msparams) = search_generate_text_SQL($parsearray, 'p.message', 'p.subject',
2077                                                  'p.userid', 'u.id', 'u.firstname',
2078                                                  'u.lastname', 'p.modified', 'd.forum');
2079         } else {
2080             list($messagesearch, $msparams) = search_generate_SQL($parsearray, 'p.message', 'p.subject',
2081                                                  'p.userid', 'u.id', 'u.firstname',
2082                                                  'u.lastname', 'p.modified', 'd.forum');
2083         }
2084         $params = array_merge($params, $msparams);
2085     }
2087     $fromsql = "{forum_posts} p,
2088                   {forum_discussions} d,
2089                   {user} u";
2091     $selectsql = " $messagesearch
2092                AND p.discussion = d.id
2093                AND p.userid = u.id
2094                AND $selectdiscussion
2095                    $extrasql";
2097     $countsql = "SELECT COUNT(*)
2098                    FROM $fromsql
2099                   WHERE $selectsql";
2101     $searchsql = "SELECT p.*,
2102                          d.forum,
2103                          u.firstname,
2104                          u.lastname,
2105                          u.email,
2106                          u.picture,
2107                          u.imagealt
2108                     FROM $fromsql
2109                    WHERE $selectsql
2110                 ORDER BY p.modified DESC";
2112     $totalcount = $DB->count_records_sql($countsql, $params);
2114     return $DB->get_records_sql($searchsql, $params, $limitfrom, $limitnum);
2117 /**
2118  * Returns a list of ratings for a particular post - sorted.
2119  *
2120  * TODO: Check if this function is actually used anywhere.
2121  * Up until the fix for MDL-27471 this function wasn't even returning.
2122  *
2123  * @param stdClass $context
2124  * @param int $postid
2125  * @param string $sort
2126  * @return array Array of ratings or false
2127  */
2128 function forum_get_ratings($context, $postid, $sort = "u.firstname ASC") {
2129     $options = new stdClass;
2130     $options->context = $context;
2131     $options->component = 'mod_forum';
2132     $options->ratingarea = 'post';
2133     $options->itemid = $postid;
2134     $options->sort = "ORDER BY $sort";
2136     $rm = new rating_manager();
2137     return $rm->get_all_ratings_for_item($options);
2140 /**
2141  * Returns a list of all new posts that have not been mailed yet
2142  *
2143  * @param int $starttime posts created after this time
2144  * @param int $endtime posts created before this
2145  * @param int $now used for timed discussions only
2146  * @return array
2147  */
2148 function forum_get_unmailed_posts($starttime, $endtime, $now=null) {
2149     global $CFG, $DB;
2151     $params = array($starttime, $endtime);
2152     if (!empty($CFG->forum_enabletimedposts)) {
2153         if (empty($now)) {
2154             $now = time();
2155         }
2156         $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2157         $params[] = $now;
2158         $params[] = $now;
2159     } else {
2160         $timedsql = "";
2161     }
2163     return $DB->get_records_sql("SELECT p.*, d.course, d.forum
2164                               FROM {forum_posts} p
2165                                    JOIN {forum_discussions} d ON d.id = p.discussion
2166                              WHERE p.mailed = 0
2167                                    AND p.created >= ?
2168                                    AND (p.created < ? OR p.mailnow = 1)
2169                                    $timedsql
2170                           ORDER BY p.modified ASC", $params);
2173 /**
2174  * Marks posts before a certain time as being mailed already
2175  *
2176  * @global object
2177  * @global object
2178  * @param int $endtime
2179  * @param int $now Defaults to time()
2180  * @return bool
2181  */
2182 function forum_mark_old_posts_as_mailed($endtime, $now=null) {
2183     global $CFG, $DB;
2184     if (empty($now)) {
2185         $now = time();
2186     }
2188     if (empty($CFG->forum_enabletimedposts)) {
2189         return $DB->execute("UPDATE {forum_posts}
2190                                SET mailed = '1'
2191                              WHERE (created < ? OR mailnow = 1)
2192                                    AND mailed = 0", array($endtime));
2194     } else {
2195         return $DB->execute("UPDATE {forum_posts}
2196                                SET mailed = '1'
2197                              WHERE discussion NOT IN (SELECT d.id
2198                                                         FROM {forum_discussions} d
2199                                                        WHERE d.timestart > ?)
2200                                    AND (created < ? OR mailnow = 1)
2201                                    AND mailed = 0", array($now, $endtime));
2202     }
2205 /**
2206  * Get all the posts for a user in a forum suitable for forum_print_post
2207  *
2208  * @global object
2209  * @global object
2210  * @uses CONTEXT_MODULE
2211  * @return array
2212  */
2213 function forum_get_user_posts($forumid, $userid) {
2214     global $CFG, $DB;
2216     $timedsql = "";
2217     $params = array($forumid, $userid);
2219     if (!empty($CFG->forum_enabletimedposts)) {
2220         $cm = get_coursemodule_from_instance('forum', $forumid);
2221         if (!has_capability('mod/forum:viewhiddentimedposts' , get_context_instance(CONTEXT_MODULE, $cm->id))) {
2222             $now = time();
2223             $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2224             $params[] = $now;
2225             $params[] = $now;
2226         }
2227     }
2229     return $DB->get_records_sql("SELECT p.*, d.forum, u.firstname, u.lastname, u.email, u.picture, u.imagealt
2230                               FROM {forum} f
2231                                    JOIN {forum_discussions} d ON d.forum = f.id
2232                                    JOIN {forum_posts} p       ON p.discussion = d.id
2233                                    JOIN {user} u              ON u.id = p.userid
2234                              WHERE f.id = ?
2235                                    AND p.userid = ?
2236                                    $timedsql
2237                           ORDER BY p.modified ASC", $params);
2240 /**
2241  * Get all the discussions user participated in
2242  *
2243  * @global object
2244  * @global object
2245  * @uses CONTEXT_MODULE
2246  * @param int $forumid
2247  * @param int $userid
2248  * @return array Array or false
2249  */
2250 function forum_get_user_involved_discussions($forumid, $userid) {
2251     global $CFG, $DB;
2253     $timedsql = "";
2254     $params = array($forumid, $userid);
2255     if (!empty($CFG->forum_enabletimedposts)) {
2256         $cm = get_coursemodule_from_instance('forum', $forumid);
2257         if (!has_capability('mod/forum:viewhiddentimedposts' , get_context_instance(CONTEXT_MODULE, $cm->id))) {
2258             $now = time();
2259             $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2260             $params[] = $now;
2261             $params[] = $now;
2262         }
2263     }
2265     return $DB->get_records_sql("SELECT DISTINCT d.*
2266                               FROM {forum} f
2267                                    JOIN {forum_discussions} d ON d.forum = f.id
2268                                    JOIN {forum_posts} p       ON p.discussion = d.id
2269                              WHERE f.id = ?
2270                                    AND p.userid = ?
2271                                    $timedsql", $params);
2274 /**
2275  * Get all the posts for a user in a forum suitable for forum_print_post
2276  *
2277  * @global object
2278  * @global object
2279  * @param int $forumid
2280  * @param int $userid
2281  * @return array of counts or false
2282  */
2283 function forum_count_user_posts($forumid, $userid) {
2284     global $CFG, $DB;
2286     $timedsql = "";
2287     $params = array($forumid, $userid);
2288     if (!empty($CFG->forum_enabletimedposts)) {
2289         $cm = get_coursemodule_from_instance('forum', $forumid);
2290         if (!has_capability('mod/forum:viewhiddentimedposts' , get_context_instance(CONTEXT_MODULE, $cm->id))) {
2291             $now = time();
2292             $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2293             $params[] = $now;
2294             $params[] = $now;
2295         }
2296     }
2298     return $DB->get_record_sql("SELECT COUNT(p.id) AS postcount, MAX(p.modified) AS lastpost
2299                              FROM {forum} f
2300                                   JOIN {forum_discussions} d ON d.forum = f.id
2301                                   JOIN {forum_posts} p       ON p.discussion = d.id
2302                                   JOIN {user} u              ON u.id = p.userid
2303                             WHERE f.id = ?
2304                                   AND p.userid = ?
2305                                   $timedsql", $params);
2308 /**
2309  * Given a log entry, return the forum post details for it.
2310  *
2311  * @global object
2312  * @global object
2313  * @param object $log
2314  * @return array|null
2315  */
2316 function forum_get_post_from_log($log) {
2317     global $CFG, $DB;
2319     if ($log->action == "add post") {
2321         return $DB->get_record_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid,
2322                                            u.firstname, u.lastname, u.email, u.picture
2323                                  FROM {forum_discussions} d,
2324                                       {forum_posts} p,
2325                                       {forum} f,
2326                                       {user} u
2327                                 WHERE p.id = ?
2328                                   AND d.id = p.discussion
2329                                   AND p.userid = u.id
2330                                   AND u.deleted <> '1'
2331                                   AND f.id = d.forum", array($log->info));
2334     } else if ($log->action == "add discussion") {
2336         return $DB->get_record_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid,
2337                                            u.firstname, u.lastname, u.email, u.picture
2338                                  FROM {forum_discussions} d,
2339                                       {forum_posts} p,
2340                                       {forum} f,
2341                                       {user} u
2342                                 WHERE d.id = ?
2343                                   AND d.firstpost = p.id
2344                                   AND p.userid = u.id
2345                                   AND u.deleted <> '1'
2346                                   AND f.id = d.forum", array($log->info));
2347     }
2348     return NULL;
2351 /**
2352  * Given a discussion id, return the first post from the discussion
2353  *
2354  * @global object
2355  * @global object
2356  * @param int $dicsussionid
2357  * @return array
2358  */
2359 function forum_get_firstpost_from_discussion($discussionid) {
2360     global $CFG, $DB;
2362     return $DB->get_record_sql("SELECT p.*
2363                              FROM {forum_discussions} d,
2364                                   {forum_posts} p
2365                             WHERE d.id = ?
2366                               AND d.firstpost = p.id ", array($discussionid));
2369 /**
2370  * Returns an array of counts of replies to each discussion
2371  *
2372  * @global object
2373  * @global object
2374  * @param int $forumid
2375  * @param string $forumsort
2376  * @param int $limit
2377  * @param int $page
2378  * @param int $perpage
2379  * @return array
2380  */
2381 function forum_count_discussion_replies($forumid, $forumsort="", $limit=-1, $page=-1, $perpage=0) {
2382     global $CFG, $DB;
2384     if ($limit > 0) {
2385         $limitfrom = 0;
2386         $limitnum  = $limit;
2387     } else if ($page != -1) {
2388         $limitfrom = $page*$perpage;
2389         $limitnum  = $perpage;
2390     } else {
2391         $limitfrom = 0;
2392         $limitnum  = 0;
2393     }
2395     if ($forumsort == "") {
2396         $orderby = "";
2397         $groupby = "";
2399     } else {
2400         $orderby = "ORDER BY $forumsort";
2401         $groupby = ", ".strtolower($forumsort);
2402         $groupby = str_replace('desc', '', $groupby);
2403         $groupby = str_replace('asc', '', $groupby);
2404     }
2406     if (($limitfrom == 0 and $limitnum == 0) or $forumsort == "") {
2407         $sql = "SELECT p.discussion, COUNT(p.id) AS replies, MAX(p.id) AS lastpostid
2408                   FROM {forum_posts} p
2409                        JOIN {forum_discussions} d ON p.discussion = d.id
2410                  WHERE p.parent > 0 AND d.forum = ?
2411               GROUP BY p.discussion";
2412         return $DB->get_records_sql($sql, array($forumid));
2414     } else {
2415         $sql = "SELECT p.discussion, (COUNT(p.id) - 1) AS replies, MAX(p.id) AS lastpostid
2416                   FROM {forum_posts} p
2417                        JOIN {forum_discussions} d ON p.discussion = d.id
2418                  WHERE d.forum = ?
2419               GROUP BY p.discussion $groupby
2420               $orderby";
2421         return $DB->get_records_sql("SELECT * FROM ($sql) sq", array($forumid), $limitfrom, $limitnum);
2422     }
2425 /**
2426  * @global object
2427  * @global object
2428  * @global object
2429  * @staticvar array $cache
2430  * @param object $forum
2431  * @param object $cm
2432  * @param object $course
2433  * @return mixed
2434  */
2435 function forum_count_discussions($forum, $cm, $course) {
2436     global $CFG, $DB, $USER;
2438     static $cache = array();
2440     $now = round(time(), -2); // db cache friendliness
2442     $params = array($course->id);
2444     if (!isset($cache[$course->id])) {
2445         if (!empty($CFG->forum_enabletimedposts)) {
2446             $timedsql = "AND d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?)";
2447             $params[] = $now;
2448             $params[] = $now;
2449         } else {
2450             $timedsql = "";
2451         }
2453         $sql = "SELECT f.id, COUNT(d.id) as dcount
2454                   FROM {forum} f
2455                        JOIN {forum_discussions} d ON d.forum = f.id
2456                  WHERE f.course = ?
2457                        $timedsql
2458               GROUP BY f.id";
2460         if ($counts = $DB->get_records_sql($sql, $params)) {
2461             foreach ($counts as $count) {
2462                 $counts[$count->id] = $count->dcount;
2463             }
2464             $cache[$course->id] = $counts;
2465         } else {
2466             $cache[$course->id] = array();
2467         }
2468     }
2470     if (empty($cache[$course->id][$forum->id])) {
2471         return 0;
2472     }
2474     $groupmode = groups_get_activity_groupmode($cm, $course);
2476     if ($groupmode != SEPARATEGROUPS) {
2477         return $cache[$course->id][$forum->id];
2478     }
2480     if (has_capability('moodle/site:accessallgroups', get_context_instance(CONTEXT_MODULE, $cm->id))) {
2481         return $cache[$course->id][$forum->id];
2482     }
2484     require_once($CFG->dirroot.'/course/lib.php');
2486     $modinfo = get_fast_modinfo($course);
2487     if (is_null($modinfo->groups)) {
2488         $modinfo->groups = groups_get_user_groups($course->id, $USER->id);
2489     }
2491     if (array_key_exists($cm->groupingid, $modinfo->groups)) {
2492         $mygroups = $modinfo->groups[$cm->groupingid];
2493     } else {
2494         $mygroups = false; // Will be set below
2495     }
2497     // add all groups posts
2498     if (empty($mygroups)) {
2499         $mygroups = array(-1=>-1);
2500     } else {
2501         $mygroups[-1] = -1;
2502     }
2504     list($mygroups_sql, $params) = $DB->get_in_or_equal($mygroups);
2505     $params[] = $forum->id;
2507     if (!empty($CFG->forum_enabletimedposts)) {
2508         $timedsql = "AND d.timestart < $now AND (d.timeend = 0 OR d.timeend > $now)";
2509         $params[] = $now;
2510         $params[] = $now;
2511     } else {
2512         $timedsql = "";
2513     }
2515     $sql = "SELECT COUNT(d.id)
2516               FROM {forum_discussions} d
2517              WHERE d.groupid $mygroups_sql AND d.forum = ?
2518                    $timedsql";
2520     return $DB->get_field_sql($sql, $params);
2523 /**
2524  * How many posts by other users are unrated by a given user in the given discussion?
2525  *
2526  * TODO: Is this function still used anywhere?
2527  *
2528  * @param int $discussionid
2529  * @param int $userid
2530  * @return mixed
2531  */
2532 function forum_count_unrated_posts($discussionid, $userid) {
2533     global $CFG, $DB;
2535     $sql = "SELECT COUNT(*) as num
2536               FROM {forum_posts}
2537              WHERE parent > 0
2538                AND discussion = :discussionid
2539                AND userid <> :userid";
2540     $params = array('discussionid' => $discussionid, 'userid' => $userid);
2541     $posts = $DB->get_record_sql($sql, $params);
2542     if ($posts) {
2543         $sql = "SELECT count(*) as num
2544                   FROM {forum_posts} p,
2545                        {rating} r
2546                  WHERE p.discussion = :discussionid AND
2547                        p.id = r.itemid AND
2548                        r.userid = userid AND
2549                        r.component = 'mod_forum' AND
2550                        r.ratingarea = 'post'";
2551         $rated = $DB->get_record_sql($sql, $params);
2552         if ($rated) {
2553             if ($posts->num > $rated->num) {
2554                 return $posts->num - $rated->num;
2555             } else {
2556                 return 0;    // Just in case there was a counting error
2557             }
2558         } else {
2559             return $posts->num;
2560         }
2561     } else {
2562         return 0;
2563     }
2566 /**
2567  * Get all discussions in a forum
2568  *
2569  * @global object
2570  * @global object
2571  * @global object
2572  * @uses CONTEXT_MODULE
2573  * @uses VISIBLEGROUPS
2574  * @param object $cm
2575  * @param string $forumsort
2576  * @param bool $fullpost
2577  * @param int $unused
2578  * @param int $limit
2579  * @param bool $userlastmodified
2580  * @param int $page
2581  * @param int $perpage
2582  * @return array
2583  */
2584 function forum_get_discussions($cm, $forumsort="d.timemodified DESC", $fullpost=true, $unused=-1, $limit=-1, $userlastmodified=false, $page=-1, $perpage=0) {
2585     global $CFG, $DB, $USER;
2587     $timelimit = '';
2589     $now = round(time(), -2);
2590     $params = array($cm->instance);
2592     $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
2594     if (!has_capability('mod/forum:viewdiscussion', $modcontext)) { /// User must have perms to view discussions
2595         return array();
2596     }
2598     if (!empty($CFG->forum_enabletimedposts)) { /// Users must fulfill timed posts
2600         if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
2601             $timelimit = " AND ((d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?))";
2602             $params[] = $now;
2603             $params[] = $now;
2604             if (isloggedin()) {
2605                 $timelimit .= " OR d.userid = ?";
2606                 $params[] = $USER->id;
2607             }
2608             $timelimit .= ")";
2609         }
2610     }
2612     if ($limit > 0) {
2613         $limitfrom = 0;
2614         $limitnum  = $limit;
2615     } else if ($page != -1) {
2616         $limitfrom = $page*$perpage;
2617         $limitnum  = $perpage;
2618     } else {
2619         $limitfrom = 0;
2620         $limitnum  = 0;
2621     }
2623     $groupmode    = groups_get_activity_groupmode($cm);
2624     $currentgroup = groups_get_activity_group($cm);
2626     if ($groupmode) {
2627         if (empty($modcontext)) {
2628             $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
2629         }
2631         if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
2632             if ($currentgroup) {
2633                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2634                 $params[] = $currentgroup;
2635             } else {
2636                 $groupselect = "";
2637             }
2639         } else {
2640             //seprate groups without access all
2641             if ($currentgroup) {
2642                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2643                 $params[] = $currentgroup;
2644             } else {
2645                 $groupselect = "AND d.groupid = -1";
2646             }
2647         }
2648     } else {
2649         $groupselect = "";
2650     }
2653     if (empty($forumsort)) {
2654         $forumsort = "d.timemodified DESC";
2655     }
2656     if (empty($fullpost)) {
2657         $postdata = "p.id,p.subject,p.modified,p.discussion,p.userid";
2658     } else {
2659         $postdata = "p.*";
2660     }
2662     if (empty($userlastmodified)) {  // We don't need to know this
2663         $umfields = "";
2664         $umtable  = "";
2665     } else {
2666         $umfields = ", um.firstname AS umfirstname, um.lastname AS umlastname";
2667         $umtable  = " LEFT JOIN {user} um ON (d.usermodified = um.id)";
2668     }
2670     $sql = "SELECT $postdata, d.name, d.timemodified, d.usermodified, d.groupid, d.timestart, d.timeend,
2671                    u.firstname, u.lastname, u.email, u.picture, u.imagealt $umfields
2672               FROM {forum_discussions} d
2673                    JOIN {forum_posts} p ON p.discussion = d.id
2674                    JOIN {user} u ON p.userid = u.id
2675                    $umtable
2676              WHERE d.forum = ? AND p.parent = 0
2677                    $timelimit $groupselect
2678           ORDER BY $forumsort";
2679     return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
2682 /**
2683  *
2684  * @global object
2685  * @global object
2686  * @global object
2687  * @uses CONTEXT_MODULE
2688  * @uses VISIBLEGROUPS
2689  * @param object $cm
2690  * @return array
2691  */
2692 function forum_get_discussions_unread($cm) {
2693     global $CFG, $DB, $USER;
2695     $now = round(time(), -2);
2696     $cutoffdate = $now - ($CFG->forum_oldpostdays*24*60*60);
2698     $params = array();
2699     $groupmode    = groups_get_activity_groupmode($cm);
2700     $currentgroup = groups_get_activity_group($cm);
2702     if ($groupmode) {
2703         $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
2705         if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
2706             if ($currentgroup) {
2707                 $groupselect = "AND (d.groupid = :currentgroup OR d.groupid = -1)";
2708                 $params['currentgroup'] = $currentgroup;
2709             } else {
2710                 $groupselect = "";
2711             }
2713         } else {
2714             //separate groups without access all
2715             if ($currentgroup) {
2716                 $groupselect = "AND (d.groupid = :currentgroup OR d.groupid = -1)";
2717                 $params['currentgroup'] = $currentgroup;
2718             } else {
2719                 $groupselect = "AND d.groupid = -1";
2720             }
2721         }
2722     } else {
2723         $groupselect = "";
2724     }
2726     if (!empty($CFG->forum_enabletimedposts)) {
2727         $timedsql = "AND d.timestart < :now1 AND (d.timeend = 0 OR d.timeend > :now2)";
2728         $params['now1'] = $now;
2729         $params['now2'] = $now;
2730     } else {
2731         $timedsql = "";
2732     }
2734     $sql = "SELECT d.id, COUNT(p.id) AS unread
2735               FROM {forum_discussions} d
2736                    JOIN {forum_posts} p     ON p.discussion = d.id
2737                    LEFT JOIN {forum_read} r ON (r.postid = p.id AND r.userid = $USER->id)
2738              WHERE d.forum = {$cm->instance}
2739                    AND p.modified >= :cutoffdate AND r.id is NULL
2740                    $groupselect
2741                    $timedsql
2742           GROUP BY d.id";
2743     $params['cutoffdate'] = $cutoffdate;
2745     if ($unreads = $DB->get_records_sql($sql, $params)) {
2746         foreach ($unreads as $unread) {
2747             $unreads[$unread->id] = $unread->unread;
2748         }
2749         return $unreads;
2750     } else {
2751         return array();
2752     }
2755 /**
2756  * @global object
2757  * @global object
2758  * @global object
2759  * @uses CONEXT_MODULE
2760  * @uses VISIBLEGROUPS
2761  * @param object $cm
2762  * @return array
2763  */
2764 function forum_get_discussions_count($cm) {
2765     global $CFG, $DB, $USER;
2767     $now = round(time(), -2);
2768     $params = array($cm->instance);
2769     $groupmode    = groups_get_activity_groupmode($cm);
2770     $currentgroup = groups_get_activity_group($cm);
2772     if ($groupmode) {
2773         $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
2775         if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
2776             if ($currentgroup) {
2777                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2778                 $params[] = $currentgroup;
2779             } else {
2780                 $groupselect = "";
2781             }
2783         } else {
2784             //seprate groups without access all
2785             if ($currentgroup) {
2786                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2787                 $params[] = $currentgroup;
2788             } else {
2789                 $groupselect = "AND d.groupid = -1";
2790             }
2791         }
2792     } else {
2793         $groupselect = "";
2794     }
2796     $cutoffdate = $now - ($CFG->forum_oldpostdays*24*60*60);
2798     $timelimit = "";
2800     if (!empty($CFG->forum_enabletimedposts)) {
2802         $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
2804         if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
2805             $timelimit = " AND ((d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?))";
2806             $params[] = $now;
2807             $params[] = $now;
2808             if (isloggedin()) {
2809                 $timelimit .= " OR d.userid = ?";
2810                 $params[] = $USER->id;
2811             }
2812             $timelimit .= ")";
2813         }
2814     }
2816     $sql = "SELECT COUNT(d.id)
2817               FROM {forum_discussions} d
2818                    JOIN {forum_posts} p ON p.discussion = d.id
2819              WHERE d.forum = ? AND p.parent = 0
2820                    $groupselect $timelimit";
2822     return $DB->get_field_sql($sql, $params);
2826 /**
2827  * Get all discussions started by a particular user in a course (or group)
2828  * This function no longer used ...
2829  *
2830  * @todo Remove this function if no longer used
2831  * @global object
2832  * @global object
2833  * @param int $courseid
2834  * @param int $userid
2835  * @param int $groupid
2836  * @return array
2837  */
2838 function forum_get_user_discussions($courseid, $userid, $groupid=0) {
2839     global $CFG, $DB;
2840     $params = array($courseid, $userid);
2841     if ($groupid) {
2842         $groupselect = " AND d.groupid = ? ";
2843         $params[] = $groupid;
2844     } else  {
2845         $groupselect = "";
2846     }
2848     return $DB->get_records_sql("SELECT p.*, d.groupid, u.firstname, u.lastname, u.email, u.picture, u.imagealt,
2849                                    f.type as forumtype, f.name as forumname, f.id as forumid
2850                               FROM {forum_discussions} d,
2851                                    {forum_posts} p,
2852                                    {user} u,
2853                                    {forum} f
2854                              WHERE d.course = ?
2855                                AND p.discussion = d.id
2856                                AND p.parent = 0
2857                                AND p.userid = u.id
2858                                AND u.id = ?
2859                                AND d.forum = f.id $groupselect
2860                           ORDER BY p.created DESC", $params);
2863 /**
2864  * Get the list of potential subscribers to a forum.
2865  *
2866  * @param object $forumcontext the forum context.
2867  * @param integer $groupid the id of a group, or 0 for all groups.
2868  * @param string $fields the list of fields to return for each user. As for get_users_by_capability.
2869  * @param string $sort sort order. As for get_users_by_capability.
2870  * @return array list of users.
2871  */
2872 function forum_get_potential_subscribers($forumcontext, $groupid, $fields, $sort) {
2873     global $DB;
2875     // only active enrolled users or everybody on the frontpage
2876     list($esql, $params) = get_enrolled_sql($forumcontext, 'mod/forum:allowforcesubscribe', $groupid, true);
2878     $sql = "SELECT $fields
2879               FROM {user} u
2880               JOIN ($esql) je ON je.id = u.id";
2881     if ($sort) {
2882         $sql = "$sql ORDER BY $sort";
2883     } else {
2884         $sql = "$sql ORDER BY u.lastname ASC, u.firstname ASC";
2885     }
2887     return $DB->get_records_sql($sql, $params);
2890 /**
2891  * Returns list of user objects that are subscribed to this forum
2892  *
2893  * @global object
2894  * @global object
2895  * @param object $course the course
2896  * @param forum $forum the forum
2897  * @param integer $groupid group id, or 0 for all.
2898  * @param object $context the forum context, to save re-fetching it where possible.
2899  * @param string $fields requested user fields (with "u." table prefix)
2900  * @return array list of users.
2901  */
2902 function forum_subscribed_users($course, $forum, $groupid=0, $context = null, $fields = null) {
2903     global $CFG, $DB;
2905     if (empty($fields)) {
2906         $fields ="u.id,
2907                   u.username,
2908                   u.firstname,
2909                   u.lastname,
2910                   u.maildisplay,
2911                   u.mailformat,
2912                   u.maildigest,
2913                   u.imagealt,
2914                   u.email,
2915                   u.emailstop,
2916                   u.city,
2917                   u.country,
2918                   u.lastaccess,
2919                   u.lastlogin,
2920                   u.picture,
2921                   u.timezone,
2922                   u.theme,
2923                   u.lang,
2924                   u.trackforums,
2925                   u.mnethostid";
2926     }
2928     if (empty($context)) {
2929         $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id);
2930         $context = get_context_instance(CONTEXT_MODULE, $cm->id);
2931     }
2933     if (forum_is_forcesubscribed($forum)) {
2934         $results = forum_get_potential_subscribers($context, $groupid, $fields, "u.email ASC");
2936     } else {
2937         // only active enrolled users or everybody on the frontpage
2938         list($esql, $params) = get_enrolled_sql($context, '', $groupid, true);
2939         $params['forumid'] = $forum->id;
2940         $results = $DB->get_records_sql("SELECT $fields
2941                                            FROM {user} u
2942                                            JOIN ($esql) je ON je.id = u.id
2943                                            JOIN {forum_subscriptions} s ON s.userid = u.id
2944                                           WHERE s.forum = :forumid
2945                                        ORDER BY u.email ASC", $params);
2946     }
2948     // Guest user should never be subscribed to a forum.
2949     unset($results[$CFG->siteguest]);
2951     return $results;
2956 // OTHER FUNCTIONS ///////////////////////////////////////////////////////////
2959 /**
2960  * @global object
2961  * @global object
2962  * @param int $courseid
2963  * @param string $type
2964  */
2965 function forum_get_course_forum($courseid, $type) {
2966 // How to set up special 1-per-course forums
2967     global $CFG, $DB, $OUTPUT, $USER;
2969     if ($forums = $DB->get_records_select("forum", "course = ? AND type = ?", array($courseid, $type), "id ASC")) {
2970         // There should always only be ONE, but with the right combination of
2971         // errors there might be more.  In this case, just return the oldest one (lowest ID).
2972         foreach ($forums as $forum) {
2973             return $forum;   // ie the first one
2974         }
2975     }
2977     // Doesn't exist, so create one now.
2978     $forum = new stdClass();
2979     $forum->course = $courseid;
2980     $forum->type = "$type";
2981     if (!empty($USER->htmleditor)) {
2982         $forum->introformat = $USER->htmleditor;
2983     }
2984     switch ($forum->type) {
2985         case "news":
2986             $forum->name  = get_string("namenews", "forum");
2987             $forum->intro = get_string("intronews", "forum");
2988             $forum->forcesubscribe = FORUM_FORCESUBSCRIBE;
2989             $forum->assessed = 0;
2990             if ($courseid == SITEID) {
2991                 $forum->name  = get_string("sitenews");
2992                 $forum->forcesubscribe = 0;
2993             }
2994             break;
2995         case "social":
2996             $forum->name  = get_string("namesocial", "forum");
2997             $forum->intro = get_string("introsocial", "forum");
2998             $forum->assessed = 0;
2999             $forum->forcesubscribe = 0;
3000             break;
3001         case "blog":
3002             $forum->name = get_string('blogforum', 'forum');
3003             $forum->intro = get_string('introblog', 'forum');
3004             $forum->assessed = 0;
3005             $forum->forcesubscribe = 0;
3006             break;
3007         default:
3008             echo $OUTPUT->notification("That forum type doesn't exist!");
3009             return false;
3010             break;
3011     }
3013     $forum->timemodified = time();
3014     $forum->id = $DB->insert_record("forum", $forum);
3016     if (! $module = $DB->get_record("modules", array("name" => "forum"))) {
3017         echo $OUTPUT->notification("Could not find forum module!!");
3018         return false;
3019     }
3020     $mod = new stdClass();
3021     $mod->course = $courseid;
3022     $mod->module = $module->id;
3023     $mod->instance = $forum->id;
3024     $mod->section = 0;
3025     if (! $mod->coursemodule = add_course_module($mod) ) {   // assumes course/lib.php is loaded
3026         echo $OUTPUT->notification("Could not add a new course module to the course '" . $courseid . "'");
3027         return false;
3028     }
3029     if (! $sectionid = add_mod_to_section($mod) ) {   // assumes course/lib.php is loaded
3030         echo $OUTPUT->notification("Could not add the new course module to that section");
3031         return false;
3032     }
3033     $DB->set_field("course_modules", "section", $sectionid, array("id" => $mod->coursemodule));
3035     include_once("$CFG->dirroot/course/lib.php");
3036     rebuild_course_cache($courseid);
3038     return $DB->get_record("forum", array("id" => "$forum->id"));
3042 /**
3043  * Given the data about a posting, builds up the HTML to display it and
3044  * returns the HTML in a string.  This is designed for sending via HTML email.
3045  *
3046  * @global object
3047  * @param object $course
3048  * @param object $cm
3049  * @param object $forum
3050  * @param object $discussion
3051  * @param object $post
3052  * @param object $userform
3053  * @param object $userto
3054  * @param bool $ownpost
3055  * @param bool $reply
3056  * @param bool $link
3057  * @param bool $rate
3058  * @param string $footer
3059  * @return string
3060  */
3061 function forum_make_mail_post($course, $cm, $forum, $discussion, $post, $userfrom, $userto,
3062                               $ownpost=false, $reply=false, $link=false, $rate=false, $footer="") {
3064     global $CFG, $OUTPUT;
3066     $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
3068     if (!isset($userto->viewfullnames[$forum->id])) {
3069         $viewfullnames = has_capability('moodle/site:viewfullnames', $modcontext, $userto->id);
3070     } else {
3071         $viewfullnames = $userto->viewfullnames[$forum->id];
3072     }
3074     // add absolute file links
3075     $post->message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php', $modcontext->id, 'mod_forum', 'post', $post->id);
3077     // format the post body
3078     $options = new stdClass();
3079     $options->para = true;
3080     $formattedtext = format_text($post->message, $post->messageformat, $options, $course->id);
3082     $output = '<table border="0" cellpadding="3" cellspacing="0" class="forumpost">';
3084     $output .= '<tr class="header"><td width="35" valign="top" class="picture left">';
3085     $output .= $OUTPUT->user_picture($userfrom, array('courseid'=>$course->id));
3086     $output .= '</td>';
3088     if ($post->parent) {
3089         $output .= '<td class="topic">';
3090     } else {
3091         $output .= '<td class="topic starter">';
3092     }
3093     $output .= '<div class="subject">'.format_string($post->subject).'</div>';
3095     $fullname = fullname($userfrom, $viewfullnames);
3096     $by = new stdClass();
3097     $by->name = '<a href="'.$CFG->wwwroot.'/user/view.php?id='.$userfrom->id.'&amp;course='.$course->id.'">'.$fullname.'</a>';
3098     $by->date = userdate($post->modified, '', $userto->timezone);
3099     $output .= '<div class="author">'.get_string('bynameondate', 'forum', $by).'</div>';
3101     $output .= '</td></tr>';
3103     $output .= '<tr><td class="left side" valign="top">';
3105     if (isset($userfrom->groups)) {
3106         $groups = $userfrom->groups[$forum->id];
3107     } else {
3108         $groups = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid);
3109     }
3111     if ($groups) {
3112         $output .= print_group_picture($groups, $course->id, false, true, true);
3113     } else {
3114         $output .= '&nbsp;';
3115     }
3117     $output .= '</td><td class="content">';
3119     $attachments = forum_print_attachments($post, $cm, 'html');
3120     if ($attachments !== '') {
3121         $output .= '<div class="attachments">';
3122         $output .= $attachments;
3123         $output .= '</div>';
3124     }
3126     $output .= $formattedtext;
3128 // Commands
3129     $commands = array();
3131     if ($post->parent) {
3132         $commands[] = '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.
3133                       $post->discussion.'&amp;parent='.$post->parent.'">'.get_string('parent', 'forum').'</a>';
3134     }
3136     if ($reply) {
3137         $commands[] = '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/post.php?reply='.$post->id.'">'.
3138                       get_string('reply', 'forum').'</a>';
3139     }
3141     $output .= '<div class="commands">';
3142     $output .= implode(' | ', $commands);
3143     $output .= '</div>';
3145 // Context link to post if required
3146     if ($link) {
3147         $output .= '<div class="link">';
3148         $output .= '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'#p'.$post->id.'">'.
3149                      get_string('postincontext', 'forum').'</a>';
3150         $output .= '</div>';
3151     }
3153     if ($footer) {
3154         $output .= '<div class="footer">'.$footer.'</div>';
3155     }
3156     $output .= '</td></tr></table>'."\n\n";
3158     return $output;
3161 /**
3162  * Print a forum post
3163  *
3164  * @global object
3165  * @global object
3166  * @uses FORUM_MODE_THREADED
3167  * @uses PORTFOLIO_FORMAT_PLAINHTML
3168  * @uses PORTFOLIO_FORMAT_FILE
3169  * @uses PORTFOLIO_FORMAT_RICHHTML
3170  * @uses PORTFOLIO_ADD_TEXT_LINK
3171  * @uses CONTEXT_MODULE
3172  * @param object $post The post to print.
3173  * @param object $discussion
3174  * @param object $forum
3175  * @param object $cm
3176  * @param object $course
3177  * @param boolean $ownpost Whether this post belongs to the current user.
3178  * @param boolean $reply Whether to print a 'reply' link at the bottom of the message.
3179  * @param boolean $link Just print a shortened version of the post as a link to the full post.
3180  * @param string $footer Extra stuff to print after the message.
3181  * @param string $highlight Space-separated list of terms to highlight.
3182  * @param int $post_read true, false or -99. If we already know whether this user
3183  *          has read this post, pass that in, otherwise, pass in -99, and this
3184  *          function will work it out.
3185  * @param boolean $dummyifcantsee When forum_user_can_see_post says that
3186  *          the current user can't see this post, if this argument is true
3187  *          (the default) then print a dummy 'you can't see this post' post.
3188  *          If false, don't output anything at all.
3189  * @param bool|null $istracked
3190  * @return void
3191  */
3192 function forum_print_post($post, $discussion, $forum, &$cm, $course, $ownpost=false, $reply=false, $link=false,
3193                           $footer="", $highlight="", $postisread=null, $dummyifcantsee=true, $istracked=null, $return=false) {
3194     global $USER, $CFG, $OUTPUT;
3196     require_once($CFG->libdir . '/filelib.php');
3198     // String cache
3199     static $str;
3201     $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
3203     $post->course = $course->id;
3204     $post->forum  = $forum->id;
3205     $post->message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php', $modcontext->id, 'mod_forum', 'post', $post->id);
3207     // caching
3208     if (!isset($cm->cache)) {
3209         $cm->cache = new stdClass;
3210     }
3212     if (!isset($cm->cache->caps)) {
3213         $cm->cache->caps = array();
3214         $cm->cache->caps['mod/forum:viewdiscussion']   = has_capability('mod/forum:viewdiscussion', $modcontext);
3215         $cm->cache->caps['moodle/site:viewfullnames']  = has_capability('moodle/site:viewfullnames', $modcontext);
3216         $cm->cache->caps['mod/forum:editanypost']      = has_capability('mod/forum:editanypost', $modcontext);
3217         $cm->cache->caps['mod/forum:splitdiscussions'] = has_capability('mod/forum:splitdiscussions', $modcontext);
3218         $cm->cache->caps['mod/forum:deleteownpost']    = has_capability('mod/forum:deleteownpost', $modcontext);
3219         $cm->cache->caps['mod/forum:deleteanypost']    = has_capability('mod/forum:deleteanypost', $modcontext);
3220         $cm->cache->caps['mod/forum:viewanyrating']    = has_capability('mod/forum:viewanyrating', $modcontext);
3221         $cm->cache->caps['mod/forum:exportpost']       = has_capability('mod/forum:exportpost', $modcontext);
3222         $cm->cache->caps['mod/forum:exportownpost']    = has_capability('mod/forum:exportownpost', $modcontext);
3223     }
3225     if (!isset($cm->uservisible)) {
3226         $cm->uservisible = coursemodule_visible_for_user($cm);
3227     }
3229     if ($istracked && is_null($postisread)) {
3230         $postisread = forum_tp_is_post_read($USER->id, $post);
3231     }
3233     if (!forum_user_can_see_post($forum, $discussion, $post, NULL, $cm)) {
3234         $output = '';
3235         if (!$dummyifcantsee) {
3236             if ($return) {
3237                 return $output;
3238             }
3239             echo $output;
3240             return;
3241         }
3242         $output .= html_writer::tag('a', '', array('id'=>'p'.$post->id));
3243         $output .= html_writer::start_tag('div', array('class'=>'forumpost clearfix'));
3244         $output .= html_writer::start_tag('div', array('class'=>'row header'));
3245         $output .= html_writer::tag('div', '', array('class'=>'left picture')); // Picture
3246         if ($post->parent) {
3247             $output .= html_writer::start_tag('div', array('class'=>'topic'));
3248         } else {
3249             $output .= html_writer::start_tag('div', array('class'=>'topic starter'));
3250         }
3251         $output .= html_writer::tag('div', get_string('forumsubjecthidden','forum'), array('class'=>'subject')); // Subject
3252         $output .= html_writer::tag('div', get_string('forumauthorhidden','forum'), array('class'=>'author')); // author
3253         $output .= html_writer::end_tag('div');
3254         $output .= html_writer::end_tag('div'); // row
3255         $output .= html_writer::start_tag('div', array('class'=>'row'));
3256         $output .= html_writer::tag('div', '&nbsp;', array('class'=>'left side')); // Groups
3257         $output .= html_writer::tag('div', get_string('forumbodyhidden','forum'), array('class'=>'content')); // Content
3258         $output .= html_writer::end_tag('div'); // row
3259         $output .= html_writer::end_tag('div'); // forumpost
3261         if ($return) {
3262             return $output;
3263         }
3264         echo $output;
3265         return;
3266     }
3268     if (empty($str)) {
3269         $str = new stdClass;
3270         $str->edit         = get_string('edit', 'forum');
3271         $str->delete       = get_string('delete', 'forum');
3272         $str->reply        = get_string('reply', 'forum');
3273         $str->parent       = get_string('parent', 'forum');
3274         $str->pruneheading = get_string('pruneheading', 'forum');
3275         $str->prune        = get_string('prune', 'forum');
3276         $str->displaymode     = get_user_preferences('forum_displaymode', $CFG->forum_displaymode);
3277         $str->markread     = get_string('markread', 'forum');
3278         $str->markunread   = get_string('markunread', 'forum');
3279     }
3281     $discussionlink = new moodle_url('/mod/forum/discuss.php', array('d'=>$post->discussion));
3283     // Build an object that represents the posting user
3284     $postuser = new stdClass;
3285     $postuser->id        = $post->userid;
3286     $postuser->firstname = $post->firstname;
3287     $postuser->lastname  = $post->lastname;
3288     $postuser->imagealt  = $post->imagealt;
3289     $postuser->picture   = $post->picture;
3290     $postuser->email     = $post->email;
3291     // Some handy things for later on
3292     $postuser->fullname    = fullname($postuser, $cm->cache->caps['moodle/site:viewfullnames']);
3293     $postuser->profilelink = new moodle_url('/user/view.php', array('id'=>$post->userid, 'course'=>$course->id));
3295     // Prepare the groups the posting user belongs to
3296     if (isset($cm->cache->usersgroups)) {
3297         $groups = array();
3298         if (isset($cm->cache->usersgroups[$post->userid])) {
3299             foreach ($cm->cache->usersgroups[$post->userid] as $gid) {
3300                 $groups[$gid] = $cm->cache->groups[$gid];
3301             }
3302         }
3303     } else {
3304         $groups = groups_get_all_groups($course->id, $post->userid, $cm->groupingid);
3305     }
3307     // Prepare the attachements for the post, files then images
3308     list($attachments, $attachedimages) = forum_print_attachments($post, $cm, 'separateimages');
3310     // Determine if we need to shorten this post
3311     $shortenpost = ($link && (strlen(strip_tags($post->message)) > $CFG->forum_longpost));
3314     // Prepare an array of commands
3315     $commands = array();
3317     // SPECIAL CASE: The front page can display a news item post to non-logged in users.
3318     // Don't display the mark read / unread controls in this case.
3319     if ($istracked && $CFG->forum_usermarksread && isloggedin()) {
3320         $url = new moodle_url($discussionlink, array('postid'=>$post->id, 'mark'=>'unread'));
3321         $text = $str->markunread;
3322         if (!$postisread) {
3323             $url->param('mark', 'read');
3324             $text = $str->markread;
3325         }
3326         if ($str->displaymode == FORUM_MODE_THREADED) {
3327             $url->param('parent', $post->parent);
3328         } else {
3329             $url->set_anchor('p'.$post->id);
3330         }
3331         $commands[] = array('url'=>$url, 'text'=>$text);
3332     }
3334     // Zoom in to the parent specifically
3335     if ($post->parent) {
3336         $url = new moodle_url($discussionlink);
3337         if ($str->displaymode == FORUM_MODE_THREADED) {
3338             $url->param('parent', $post->parent);
3339         } else {
3340             $url->set_anchor('p'.$post->parent);
3341         }
3342         $commands[] = array('url'=>$url, 'text'=>$str->parent);
3343     }
3345     // Hack for allow to edit news posts those are not displayed yet until they are displayed
3346     $age = time() - $post->created;
3347     if (!$post->parent && $forum->type == 'news' && $discussion->timestart > time()) {
3348         $age = 0;
3349     }
3351     if ($forum->type == 'single' and $discussion->firstpost == $post->id) {
3352         if (has_capability('moodle/course:manageactivities', $modcontext)) {
3353             // The first post in single simple is the forum description.
3354             $commands[] = array('url'=>new moodle_url('/course/modedit.php', array('update'=>$cm->id, 'sesskey'=>sesskey(), 'return'=>1)), 'text'=>$str->edit);
3355         }
3356     } else if (($ownpost && $age < $CFG->maxeditingtime) || $cm->cache->caps['mod/forum:editanypost']) {
3357         $commands[] = array('url'=>new moodle_url('/mod/forum/post.php', array('edit'=>$post->id)), 'text'=>$str->edit);
3358     }
3360     if ($cm->cache->caps['mod/forum:splitdiscussions'] && $post->parent && $forum->type != 'single') {
3361         $commands[] = array('url'=>new moodle_url('/mod/forum/post.php', array('prune'=>$post->id)), 'text'=>$str->prune, 'title'=>$str->pruneheading);
3362     }
3364     if ($forum->type == 'single' and $discussion->firstpost == $post->id) {
3365         // Do not allow deleting of first post in single simple type.
3366     } else if (($ownpost && $age < $CFG->maxeditingtime && $cm->cache->caps['mod/forum:deleteownpost']) || $cm->cache->caps['mod/forum:deleteanypost']) {
3367         $commands[] = array('url'=>new moodle_url('/mod/forum/post.php', array('delete'=>$post->id)), 'text'=>$str->delete);
3368     }
3370     if ($reply) {
3371         $commands[] = array('url'=>new moodle_url('/mod/forum/post.php#mform1', array('reply'=>$post->id)), 'text'=>$str->reply);
3372     }
3374     if ($CFG->enableportfolios && ($cm->cache->caps['mod/forum:exportpost'] || ($ownpost && $cm->cache->caps['mod/forum:exportownpost']))) {
3375         $p = array('postid' => $post->id);
3376         require_once($CFG->libdir.'/portfoliolib.php');
3377         $button = new portfolio_add_button();
3378         $button->set_callback_options('forum_portfolio_caller', array('postid' => $post->id), '/mod/forum/locallib.php');
3379         if (empty($attachments)) {
3380             $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML);
3381         } else {
3382             $button->set_formats(PORTFOLIO_FORMAT_RICHHTML);
3383         }
3385         $porfoliohtml = $button->to_html(PORTFOLIO_ADD_TEXT_LINK);
3386         if (!empty($porfoliohtml)) {
3387             $commands[] = $porfoliohtml;
3388         }
3389     }
3390     // Finished building commands
3393     // Begin output
3395     $output  = '';
3397     if ($istracked) {
3398         if ($postisread) {
3399             $forumpostclass = ' read';
3400         } else {
3401             $forumpostclass = ' unread';
3402             $output .= html_writer::tag('a', '', array('name'=>'unread'));
3403         }
3404     } else {
3405         // ignore trackign status if not tracked or tracked param missing
3406         $forumpostclass = '';
3407     }
3409     $topicclass = '';
3410     if (empty($post->parent)) {
3411         $topicclass = ' firstpost starter';
3412     }
3414     $output .= html_writer::tag('a', '', array('id'=>'p'.$post->id));
3415     $output .= html_writer::start_tag('div', array('class'=>'forumpost clearfix'.$forumpostclass.$topicclass));
3416     $output .= html_writer::start_tag('div', array('class'=>'row header clearfix'));
3417     $output .= html_writer::start_tag('div', array('class'=>'left picture'));
3418     $output .= $OUTPUT->user_picture($postuser, array('courseid'=>$course->id));
3419     $output .= html_writer::end_tag('div');
3422     $output .= html_writer::start_tag('div', array('class'=>'topic'.$topicclass));
3424     $postsubject = $post->subject;
3425     if (empty($post->subjectnoformat)) {
3426         $postsubject = format_string($postsubject);
3427     }
3428     $output .= html_writer::tag('div', $postsubject, array('class'=>'subject'));
3430     $by = new stdClass();
3431     $by->name = html_writer::link($postuser->profilelink, $postuser->fullname);
3432     $by->date = userdate($post->modified);
3433     $output .= html_writer::tag('div', get_string('bynameondate', 'forum', $by), array('class'=>'author'));
3435     $output .= html_writer::end_tag('div'); //topic
3436     $output .= html_writer::end_tag('div'); //row
3438     $output .= html_writer::start_tag('div', array('class'=>'row maincontent clearfix'));
3439     $output .= html_writer::start_tag('div', array('class'=>'left'));
3441     $groupoutput = '';
3442     if ($groups) {
3443         $groupoutput = print_group_picture($groups, $course->id, false, true, true);
3444     }
3445     if (empty($groupoutput)) {
3446         $groupoutput = '&nbsp;';
3447     }
3448     $output .= html_writer::tag('div', $groupoutput, array('class'=>'grouppictures'));
3450     $output .= html_writer::end_tag('div'); //left side
3451     $output .= html_writer::start_tag('div', array('class'=>'no-overflow'));
3452     $output .= html_writer::start_tag('div', array('class'=>'content'));
3453     if (!empty($attachments)) {
3454         $output .= html_writer::tag('div', $attachments, array('class'=>'attachments'));
3455     }
3457     $options = new stdClass;
3458     $options->para    = false;
3459     $options->trusted = $post->messagetrust;
3460     $options->context = $modcontext;
3461     if ($shortenpost) {
3462         // Prepare shortened version
3463         $postclass    = 'shortenedpost';
3464         $postcontent  = format_text(forum_shorten_post($post->message), $post->messageformat, $options, $course->id);
3465         $postcontent .= html_writer::link($discussionlink, get_string('readtherest', 'forum'));
3466         $postcontent .= html_writer::tag('span', '('.get_string('numwords', 'moodle', count_words(strip_tags($post->message))).')...', array('class'=>'post-word-count'));
3467     } else {
3468         // Prepare whole post
3469         $postclass    = 'fullpost';
3470         $postcontent  = format_text($post->message, $post->messageformat, $options, $course->id);
3471         if (!empty($highlight)) {
3472             $postcontent = highlight($highlight, $postcontent);
3473         }
3474         $postcontent .= html_writer::tag('div', $attachedimages, array('class'=>'attachedimages'));
3475     }
3476     // Output the post content
3477     $output .= html_writer::tag('div', $postcontent, array('class'=>'posting '.$postclass));
3478     $output .= html_writer::end_tag('div'); // Content
3479     $output .= html_writer::end_tag('div'); // Content mask
3480     $output .= html_writer::end_tag('div'); // Row
3482     $output .= html_writer::start_tag('div', array('class'=>'row side'));
3483     $output .= html_writer::tag('div','&nbsp;', array('class'=>'left'));
3484     $output .= html_writer::start_tag('div', array('class'=>'options clearfix'));
3486     // Output ratings
3487     if (!empty($post->rating)) {
3488         $output .= html_writer::tag('div', $OUTPUT->render($post->rating), array('class'=>'forum-post-rating'));