MDL-40062 mod_forum: subscription events
[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');
31 /// CONSTANTS ///////////////////////////////////////////////////////////
33 define('FORUM_MODE_FLATOLDEST', 1);
34 define('FORUM_MODE_FLATNEWEST', -1);
35 define('FORUM_MODE_THREADED', 2);
36 define('FORUM_MODE_NESTED', 3);
38 define('FORUM_CHOOSESUBSCRIBE', 0);
39 define('FORUM_FORCESUBSCRIBE', 1);
40 define('FORUM_INITIALSUBSCRIBE', 2);
41 define('FORUM_DISALLOWSUBSCRIBE',3);
43 /**
44  * FORUM_TRACKING_OFF - Tracking is not available for this forum.
45  */
46 define('FORUM_TRACKING_OFF', 0);
48 /**
49  * FORUM_TRACKING_OPTIONAL - Tracking is based on user preference.
50  */
51 define('FORUM_TRACKING_OPTIONAL', 1);
53 /**
54  * FORUM_TRACKING_FORCED - Tracking is on, regardless of user setting.
55  * Treated as FORUM_TRACKING_OPTIONAL if $CFG->forum_allowforcedreadtracking is off.
56  */
57 define('FORUM_TRACKING_FORCED', 2);
59 /**
60  * FORUM_TRACKING_ON - deprecated alias for FORUM_TRACKING_FORCED.
61  * @deprecated since 2.6
62  */
63 define('FORUM_TRACKING_ON', 2);
65 define('FORUM_MAILED_PENDING', 0);
66 define('FORUM_MAILED_SUCCESS', 1);
67 define('FORUM_MAILED_ERROR', 2);
69 if (!defined('FORUM_CRON_USER_CACHE')) {
70     /** Defines how many full user records are cached in forum cron. */
71     define('FORUM_CRON_USER_CACHE', 5000);
72 }
74 /// STANDARD FUNCTIONS ///////////////////////////////////////////////////////////
76 /**
77  * Given an object containing all the necessary data,
78  * (defined by the form in mod_form.php) this function
79  * will create a new instance and return the id number
80  * of the new instance.
81  *
82  * @param stdClass $forum add forum instance
83  * @param mod_forum_mod_form $mform
84  * @return int intance id
85  */
86 function forum_add_instance($forum, $mform = null) {
87     global $CFG, $DB;
89     $forum->timemodified = time();
91     if (empty($forum->assessed)) {
92         $forum->assessed = 0;
93     }
95     if (empty($forum->ratingtime) or empty($forum->assessed)) {
96         $forum->assesstimestart  = 0;
97         $forum->assesstimefinish = 0;
98     }
100     $forum->id = $DB->insert_record('forum', $forum);
101     $modcontext = context_module::instance($forum->coursemodule);
103     if ($forum->type == 'single') {  // Create related discussion.
104         $discussion = new stdClass();
105         $discussion->course        = $forum->course;
106         $discussion->forum         = $forum->id;
107         $discussion->name          = $forum->name;
108         $discussion->assessed      = $forum->assessed;
109         $discussion->message       = $forum->intro;
110         $discussion->messageformat = $forum->introformat;
111         $discussion->messagetrust  = trusttext_trusted(context_course::instance($forum->course));
112         $discussion->mailnow       = false;
113         $discussion->groupid       = -1;
115         $message = '';
117         $discussion->id = forum_add_discussion($discussion, null, $message);
119         if ($mform and $draftid = file_get_submitted_draft_itemid('introeditor')) {
120             // Ugly hack - we need to copy the files somehow.
121             $discussion = $DB->get_record('forum_discussions', array('id'=>$discussion->id), '*', MUST_EXIST);
122             $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost), '*', MUST_EXIST);
124             $options = array('subdirs'=>true); // Use the same options as intro field!
125             $post->message = file_save_draft_area_files($draftid, $modcontext->id, 'mod_forum', 'post', $post->id, $options, $post->message);
126             $DB->set_field('forum_posts', 'message', $post->message, array('id'=>$post->id));
127         }
128     }
130     if ($forum->forcesubscribe == FORUM_INITIALSUBSCRIBE) {
131         $users = forum_get_potential_subscribers($modcontext, 0, 'u.id, u.email');
132         foreach ($users as $user) {
133             forum_subscribe($user->id, $forum->id);
134         }
135     }
137     forum_grade_item_update($forum);
139     return $forum->id;
143 /**
144  * Given an object containing all the necessary data,
145  * (defined by the form in mod_form.php) this function
146  * will update an existing instance with new data.
147  *
148  * @global object
149  * @param object $forum forum instance (with magic quotes)
150  * @return bool success
151  */
152 function forum_update_instance($forum, $mform) {
153     global $DB, $OUTPUT, $USER;
155     $forum->timemodified = time();
156     $forum->id           = $forum->instance;
158     if (empty($forum->assessed)) {
159         $forum->assessed = 0;
160     }
162     if (empty($forum->ratingtime) or empty($forum->assessed)) {
163         $forum->assesstimestart  = 0;
164         $forum->assesstimefinish = 0;
165     }
167     $oldforum = $DB->get_record('forum', array('id'=>$forum->id));
169     // MDL-3942 - if the aggregation type or scale (i.e. max grade) changes then recalculate the grades for the entire forum
170     // if  scale changes - do we need to recheck the ratings, if ratings higher than scale how do we want to respond?
171     // 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
172     if (($oldforum->assessed<>$forum->assessed) or ($oldforum->scale<>$forum->scale)) {
173         forum_update_grades($forum); // recalculate grades for the forum
174     }
176     if ($forum->type == 'single') {  // Update related discussion and post.
177         $discussions = $DB->get_records('forum_discussions', array('forum'=>$forum->id), 'timemodified ASC');
178         if (!empty($discussions)) {
179             if (count($discussions) > 1) {
180                 echo $OUTPUT->notification(get_string('warnformorepost', 'forum'));
181             }
182             $discussion = array_pop($discussions);
183         } else {
184             // try to recover by creating initial discussion - MDL-16262
185             $discussion = new stdClass();
186             $discussion->course          = $forum->course;
187             $discussion->forum           = $forum->id;
188             $discussion->name            = $forum->name;
189             $discussion->assessed        = $forum->assessed;
190             $discussion->message         = $forum->intro;
191             $discussion->messageformat   = $forum->introformat;
192             $discussion->messagetrust    = true;
193             $discussion->mailnow         = false;
194             $discussion->groupid         = -1;
196             $message = '';
198             forum_add_discussion($discussion, null, $message);
200             if (! $discussion = $DB->get_record('forum_discussions', array('forum'=>$forum->id))) {
201                 print_error('cannotadd', 'forum');
202             }
203         }
204         if (! $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost))) {
205             print_error('cannotfindfirstpost', 'forum');
206         }
208         $cm         = get_coursemodule_from_instance('forum', $forum->id);
209         $modcontext = context_module::instance($cm->id, MUST_EXIST);
211         $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost), '*', MUST_EXIST);
212         $post->subject       = $forum->name;
213         $post->message       = $forum->intro;
214         $post->messageformat = $forum->introformat;
215         $post->messagetrust  = trusttext_trusted($modcontext);
216         $post->modified      = $forum->timemodified;
217         $post->userid        = $USER->id;    // MDL-18599, so that current teacher can take ownership of activities.
219         if ($mform and $draftid = file_get_submitted_draft_itemid('introeditor')) {
220             // Ugly hack - we need to copy the files somehow.
221             $options = array('subdirs'=>true); // Use the same options as intro field!
222             $post->message = file_save_draft_area_files($draftid, $modcontext->id, 'mod_forum', 'post', $post->id, $options, $post->message);
223         }
225         $DB->update_record('forum_posts', $post);
226         $discussion->name = $forum->name;
227         $DB->update_record('forum_discussions', $discussion);
228     }
230     $DB->update_record('forum', $forum);
232     $modcontext = context_module::instance($forum->coursemodule);
233     if (($forum->forcesubscribe == FORUM_INITIALSUBSCRIBE) && ($oldforum->forcesubscribe <> $forum->forcesubscribe)) {
234         $users = forum_get_potential_subscribers($modcontext, 0, 'u.id, u.email', '');
235         foreach ($users as $user) {
236             forum_subscribe($user->id, $forum->id);
237         }
238     }
240     forum_grade_item_update($forum);
242     return true;
246 /**
247  * Given an ID of an instance of this module,
248  * this function will permanently delete the instance
249  * and any data that depends on it.
250  *
251  * @global object
252  * @param int $id forum instance id
253  * @return bool success
254  */
255 function forum_delete_instance($id) {
256     global $DB;
258     if (!$forum = $DB->get_record('forum', array('id'=>$id))) {
259         return false;
260     }
261     if (!$cm = get_coursemodule_from_instance('forum', $forum->id)) {
262         return false;
263     }
264     if (!$course = $DB->get_record('course', array('id'=>$cm->course))) {
265         return false;
266     }
268     $context = context_module::instance($cm->id);
270     // now get rid of all files
271     $fs = get_file_storage();
272     $fs->delete_area_files($context->id);
274     $result = true;
276     if ($discussions = $DB->get_records('forum_discussions', array('forum'=>$forum->id))) {
277         foreach ($discussions as $discussion) {
278             if (!forum_delete_discussion($discussion, true, $course, $cm, $forum)) {
279                 $result = false;
280             }
281         }
282     }
284     if (!$DB->delete_records('forum_digests', array('forum' => $forum->id))) {
285         $result = false;
286     }
288     if (!$DB->delete_records('forum_subscriptions', array('forum'=>$forum->id))) {
289         $result = false;
290     }
292     forum_tp_delete_read_records(-1, -1, -1, $forum->id);
294     if (!$DB->delete_records('forum', array('id'=>$forum->id))) {
295         $result = false;
296     }
298     forum_grade_item_delete($forum);
300     return $result;
304 /**
305  * Indicates API features that the forum supports.
306  *
307  * @uses FEATURE_GROUPS
308  * @uses FEATURE_GROUPINGS
309  * @uses FEATURE_GROUPMEMBERSONLY
310  * @uses FEATURE_MOD_INTRO
311  * @uses FEATURE_COMPLETION_TRACKS_VIEWS
312  * @uses FEATURE_COMPLETION_HAS_RULES
313  * @uses FEATURE_GRADE_HAS_GRADE
314  * @uses FEATURE_GRADE_OUTCOMES
315  * @param string $feature
316  * @return mixed True if yes (some features may use other values)
317  */
318 function forum_supports($feature) {
319     switch($feature) {
320         case FEATURE_GROUPS:                  return true;
321         case FEATURE_GROUPINGS:               return true;
322         case FEATURE_GROUPMEMBERSONLY:        return true;
323         case FEATURE_MOD_INTRO:               return true;
324         case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
325         case FEATURE_COMPLETION_HAS_RULES:    return true;
326         case FEATURE_GRADE_HAS_GRADE:         return true;
327         case FEATURE_GRADE_OUTCOMES:          return true;
328         case FEATURE_RATE:                    return true;
329         case FEATURE_BACKUP_MOODLE2:          return true;
330         case FEATURE_SHOW_DESCRIPTION:        return true;
331         case FEATURE_PLAGIARISM:              return true;
333         default: return null;
334     }
338 /**
339  * Obtains the automatic completion state for this forum based on any conditions
340  * in forum settings.
341  *
342  * @global object
343  * @global object
344  * @param object $course Course
345  * @param object $cm Course-module
346  * @param int $userid User ID
347  * @param bool $type Type of comparison (or/and; can be used as return value if no conditions)
348  * @return bool True if completed, false if not. (If no conditions, then return
349  *   value depends on comparison type)
350  */
351 function forum_get_completion_state($course,$cm,$userid,$type) {
352     global $CFG,$DB;
354     // Get forum details
355     if (!($forum=$DB->get_record('forum',array('id'=>$cm->instance)))) {
356         throw new Exception("Can't find forum {$cm->instance}");
357     }
359     $result=$type; // Default return value
361     $postcountparams=array('userid'=>$userid,'forumid'=>$forum->id);
362     $postcountsql="
363 SELECT
364     COUNT(1)
365 FROM
366     {forum_posts} fp
367     INNER JOIN {forum_discussions} fd ON fp.discussion=fd.id
368 WHERE
369     fp.userid=:userid AND fd.forum=:forumid";
371     if ($forum->completiondiscussions) {
372         $value = $forum->completiondiscussions <=
373                  $DB->count_records('forum_discussions',array('forum'=>$forum->id,'userid'=>$userid));
374         if ($type == COMPLETION_AND) {
375             $result = $result && $value;
376         } else {
377             $result = $result || $value;
378         }
379     }
380     if ($forum->completionreplies) {
381         $value = $forum->completionreplies <=
382                  $DB->get_field_sql( $postcountsql.' AND fp.parent<>0',$postcountparams);
383         if ($type==COMPLETION_AND) {
384             $result = $result && $value;
385         } else {
386             $result = $result || $value;
387         }
388     }
389     if ($forum->completionposts) {
390         $value = $forum->completionposts <= $DB->get_field_sql($postcountsql,$postcountparams);
391         if ($type == COMPLETION_AND) {
392             $result = $result && $value;
393         } else {
394             $result = $result || $value;
395         }
396     }
398     return $result;
401 /**
402  * Create a message-id string to use in the custom headers of forum notification emails
403  *
404  * message-id is used by email clients to identify emails and to nest conversations
405  *
406  * @param int $postid The ID of the forum post we are notifying the user about
407  * @param int $usertoid The ID of the user being notified
408  * @param string $hostname The server's hostname
409  * @return string A unique message-id
410  */
411 function forum_get_email_message_id($postid, $usertoid, $hostname) {
412     return '<'.hash('sha256',$postid.'to'.$usertoid).'@'.$hostname.'>';
415 /**
416  * Removes properties from user record that are not necessary
417  * for sending post notifications.
418  * @param stdClass $user
419  * @return void, $user parameter is modified
420  */
421 function forum_cron_minimise_user_record(stdClass $user) {
423     // We store large amount of users in one huge array,
424     // make sure we do not store info there we do not actually need
425     // in mail generation code or messaging.
427     unset($user->institution);
428     unset($user->department);
429     unset($user->address);
430     unset($user->city);
431     unset($user->url);
432     unset($user->currentlogin);
433     unset($user->description);
434     unset($user->descriptionformat);
437 /**
438  * Function to be run periodically according to the moodle cron
439  * Finds all posts that have yet to be mailed out, and mails them
440  * out to all subscribers
441  *
442  * @global object
443  * @global object
444  * @global object
445  * @uses CONTEXT_MODULE
446  * @uses CONTEXT_COURSE
447  * @uses SITEID
448  * @uses FORMAT_PLAIN
449  * @return void
450  */
451 function forum_cron() {
452     global $CFG, $USER, $DB;
454     $site = get_site();
456     // All users that are subscribed to any post that needs sending,
457     // please increase $CFG->extramemorylimit on large sites that
458     // send notifications to a large number of users.
459     $users = array();
460     $userscount = 0; // Cached user counter - count($users) in PHP is horribly slow!!!
462     // status arrays
463     $mailcount  = array();
464     $errorcount = array();
466     // caches
467     $discussions     = array();
468     $forums          = array();
469     $courses         = array();
470     $coursemodules   = array();
471     $subscribedusers = array();
474     // Posts older than 2 days will not be mailed.  This is to avoid the problem where
475     // cron has not been running for a long time, and then suddenly people are flooded
476     // with mail from the past few weeks or months
477     $timenow   = time();
478     $endtime   = $timenow - $CFG->maxeditingtime;
479     $starttime = $endtime - 48 * 3600;   // Two days earlier
481     // Get the list of forum subscriptions for per-user per-forum maildigest settings.
482     $digestsset = $DB->get_recordset('forum_digests', null, '', 'id, userid, forum, maildigest');
483     $digests = array();
484     foreach ($digestsset as $thisrow) {
485         if (!isset($digests[$thisrow->forum])) {
486             $digests[$thisrow->forum] = array();
487         }
488         $digests[$thisrow->forum][$thisrow->userid] = $thisrow->maildigest;
489     }
490     $digestsset->close();
492     if ($posts = forum_get_unmailed_posts($starttime, $endtime, $timenow)) {
493         // Mark them all now as being mailed.  It's unlikely but possible there
494         // might be an error later so that a post is NOT actually mailed out,
495         // but since mail isn't crucial, we can accept this risk.  Doing it now
496         // prevents the risk of duplicated mails, which is a worse problem.
498         if (!forum_mark_old_posts_as_mailed($endtime)) {
499             mtrace('Errors occurred while trying to mark some posts as being mailed.');
500             return false;  // Don't continue trying to mail them, in case we are in a cron loop
501         }
503         // checking post validity, and adding users to loop through later
504         foreach ($posts as $pid => $post) {
506             $discussionid = $post->discussion;
507             if (!isset($discussions[$discussionid])) {
508                 if ($discussion = $DB->get_record('forum_discussions', array('id'=> $post->discussion))) {
509                     $discussions[$discussionid] = $discussion;
510                 } else {
511                     mtrace('Could not find discussion '.$discussionid);
512                     unset($posts[$pid]);
513                     continue;
514                 }
515             }
516             $forumid = $discussions[$discussionid]->forum;
517             if (!isset($forums[$forumid])) {
518                 if ($forum = $DB->get_record('forum', array('id' => $forumid))) {
519                     $forums[$forumid] = $forum;
520                 } else {
521                     mtrace('Could not find forum '.$forumid);
522                     unset($posts[$pid]);
523                     continue;
524                 }
525             }
526             $courseid = $forums[$forumid]->course;
527             if (!isset($courses[$courseid])) {
528                 if ($course = $DB->get_record('course', array('id' => $courseid))) {
529                     $courses[$courseid] = $course;
530                 } else {
531                     mtrace('Could not find course '.$courseid);
532                     unset($posts[$pid]);
533                     continue;
534                 }
535             }
536             if (!isset($coursemodules[$forumid])) {
537                 if ($cm = get_coursemodule_from_instance('forum', $forumid, $courseid)) {
538                     $coursemodules[$forumid] = $cm;
539                 } else {
540                     mtrace('Could not find course module for forum '.$forumid);
541                     unset($posts[$pid]);
542                     continue;
543                 }
544             }
547             // caching subscribed users of each forum
548             if (!isset($subscribedusers[$forumid])) {
549                 $modcontext = context_module::instance($coursemodules[$forumid]->id);
550                 if ($subusers = forum_subscribed_users($courses[$courseid], $forums[$forumid], 0, $modcontext, "u.*")) {
551                     foreach ($subusers as $postuser) {
552                         // this user is subscribed to this forum
553                         $subscribedusers[$forumid][$postuser->id] = $postuser->id;
554                         $userscount++;
555                         if ($userscount > FORUM_CRON_USER_CACHE) {
556                             // Store minimal user info.
557                             $minuser = new stdClass();
558                             $minuser->id = $postuser->id;
559                             $users[$postuser->id] = $minuser;
560                         } else {
561                             // Cache full user record.
562                             forum_cron_minimise_user_record($postuser);
563                             $users[$postuser->id] = $postuser;
564                         }
565                     }
566                     // Release memory.
567                     unset($subusers);
568                     unset($postuser);
569                 }
570             }
572             $mailcount[$pid] = 0;
573             $errorcount[$pid] = 0;
574         }
575     }
577     if ($users && $posts) {
579         $urlinfo = parse_url($CFG->wwwroot);
580         $hostname = $urlinfo['host'];
582         foreach ($users as $userto) {
584             core_php_time_limit::raise(120); // terminate if processing of any account takes longer than 2 minutes
586             mtrace('Processing user '.$userto->id);
588             // Init user caches - we keep the cache for one cycle only,
589             // otherwise it could consume too much memory.
590             if (isset($userto->username)) {
591                 $userto = clone($userto);
592             } else {
593                 $userto = $DB->get_record('user', array('id' => $userto->id));
594                 forum_cron_minimise_user_record($userto);
595             }
596             $userto->viewfullnames = array();
597             $userto->canpost       = array();
598             $userto->markposts     = array();
600             // set this so that the capabilities are cached, and environment matches receiving user
601             cron_setup_user($userto);
603             // reset the caches
604             foreach ($coursemodules as $forumid=>$unused) {
605                 $coursemodules[$forumid]->cache       = new stdClass();
606                 $coursemodules[$forumid]->cache->caps = array();
607                 unset($coursemodules[$forumid]->uservisible);
608             }
610             foreach ($posts as $pid => $post) {
612                 // Set up the environment for the post, discussion, forum, course
613                 $discussion = $discussions[$post->discussion];
614                 $forum      = $forums[$discussion->forum];
615                 $course     = $courses[$forum->course];
616                 $cm         =& $coursemodules[$forum->id];
618                 // Do some checks  to see if we can bail out now
619                 // Only active enrolled users are in the list of subscribers
620                 if (!isset($subscribedusers[$forum->id][$userto->id])) {
621                     continue; // user does not subscribe to this forum
622                 }
624                 // Don't send email if the forum is Q&A and the user has not posted
625                 // Initial topics are still mailed
626                 if ($forum->type == 'qanda' && !forum_get_user_posted_time($discussion->id, $userto->id) && $pid != $discussion->firstpost) {
627                     mtrace('Did not email '.$userto->id.' because user has not posted in discussion');
628                     continue;
629                 }
631                 // Get info about the sending user
632                 if (array_key_exists($post->userid, $users)) { // we might know him/her already
633                     $userfrom = $users[$post->userid];
634                     if (!isset($userfrom->idnumber)) {
635                         // Minimalised user info, fetch full record.
636                         $userfrom = $DB->get_record('user', array('id' => $userfrom->id));
637                         forum_cron_minimise_user_record($userfrom);
638                     }
640                 } else if ($userfrom = $DB->get_record('user', array('id' => $post->userid))) {
641                     forum_cron_minimise_user_record($userfrom);
642                     // Fetch only once if possible, we can add it to user list, it will be skipped anyway.
643                     if ($userscount <= FORUM_CRON_USER_CACHE) {
644                         $userscount++;
645                         $users[$userfrom->id] = $userfrom;
646                     }
648                 } else {
649                     mtrace('Could not find user '.$post->userid);
650                     continue;
651                 }
653                 //if we want to check that userto and userfrom are not the same person this is probably the spot to do it
655                 // setup global $COURSE properly - needed for roles and languages
656                 cron_setup_user($userto, $course);
658                 // Fill caches
659                 if (!isset($userto->viewfullnames[$forum->id])) {
660                     $modcontext = context_module::instance($cm->id);
661                     $userto->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $modcontext);
662                 }
663                 if (!isset($userto->canpost[$discussion->id])) {
664                     $modcontext = context_module::instance($cm->id);
665                     $userto->canpost[$discussion->id] = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
666                 }
667                 if (!isset($userfrom->groups[$forum->id])) {
668                     if (!isset($userfrom->groups)) {
669                         $userfrom->groups = array();
670                         if (isset($users[$userfrom->id])) {
671                             $users[$userfrom->id]->groups = array();
672                         }
673                     }
674                     $userfrom->groups[$forum->id] = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid);
675                     if (isset($users[$userfrom->id])) {
676                         $users[$userfrom->id]->groups[$forum->id] = $userfrom->groups[$forum->id];
677                     }
678                 }
680                 // Make sure groups allow this user to see this email
681                 if ($discussion->groupid > 0 and $groupmode = groups_get_activity_groupmode($cm, $course)) {   // Groups are being used
682                     if (!groups_group_exists($discussion->groupid)) { // Can't find group
683                         continue;                           // Be safe and don't send it to anyone
684                     }
686                     if (!groups_is_member($discussion->groupid) and !has_capability('moodle/site:accessallgroups', $modcontext)) {
687                         // do not send posts from other groups when in SEPARATEGROUPS or VISIBLEGROUPS
688                         continue;
689                     }
690                 }
692                 // Make sure we're allowed to see it...
693                 if (!forum_user_can_see_post($forum, $discussion, $post, NULL, $cm)) {
694                     mtrace('user '.$userto->id. ' can not see '.$post->id);
695                     continue;
696                 }
698                 // OK so we need to send the email.
700                 // Does the user want this post in a digest?  If so postpone it for now.
701                 $maildigest = forum_get_user_maildigest_bulk($digests, $userto, $forum->id);
703                 if ($maildigest > 0) {
704                     // This user wants the mails to be in digest form
705                     $queue = new stdClass();
706                     $queue->userid       = $userto->id;
707                     $queue->discussionid = $discussion->id;
708                     $queue->postid       = $post->id;
709                     $queue->timemodified = $post->created;
710                     $DB->insert_record('forum_queue', $queue);
711                     continue;
712                 }
715                 // Prepare to actually send the post now, and build up the content
717                 $cleanforumname = str_replace('"', "'", strip_tags(format_string($forum->name)));
719                 $userfrom->customheaders = array (  // Headers to make emails easier to track
720                            'Precedence: Bulk',
721                            'List-Id: "'.$cleanforumname.'" <moodleforum'.$forum->id.'@'.$hostname.'>',
722                            'List-Help: '.$CFG->wwwroot.'/mod/forum/view.php?f='.$forum->id,
723                            'Message-ID: '.forum_get_email_message_id($post->id, $userto->id, $hostname),
724                            'X-Course-Id: '.$course->id,
725                            'X-Course-Name: '.format_string($course->fullname, true)
726                 );
728                 if ($post->parent) {  // This post is a reply, so add headers for threading (see MDL-22551)
729                     $userfrom->customheaders[] = 'In-Reply-To: '.forum_get_email_message_id($post->parent, $userto->id, $hostname);
730                     $userfrom->customheaders[] = 'References: '.forum_get_email_message_id($post->parent, $userto->id, $hostname);
731                 }
733                 $shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
735                 $postsubject = html_to_text("$shortname: ".format_string($post->subject, true));
736                 $posttext = forum_make_mail_text($course, $cm, $forum, $discussion, $post, $userfrom, $userto);
737                 $posthtml = forum_make_mail_html($course, $cm, $forum, $discussion, $post, $userfrom, $userto);
739                 // Send the post now!
741                 mtrace('Sending ', '');
743                 $eventdata = new stdClass();
744                 $eventdata->component        = 'mod_forum';
745                 $eventdata->name             = 'posts';
746                 $eventdata->userfrom         = $userfrom;
747                 $eventdata->userto           = $userto;
748                 $eventdata->subject          = $postsubject;
749                 $eventdata->fullmessage      = $posttext;
750                 $eventdata->fullmessageformat = FORMAT_PLAIN;
751                 $eventdata->fullmessagehtml  = $posthtml;
752                 $eventdata->notification = 1;
754                 // If forum_replytouser is not set then send mail using the noreplyaddress.
755                 if (empty($CFG->forum_replytouser)) {
756                     // Clone userfrom as it is referenced by $users.
757                     $cloneduserfrom = clone($userfrom);
758                     $cloneduserfrom->email = $CFG->noreplyaddress;
759                     $eventdata->userfrom = $cloneduserfrom;
760                 }
762                 $smallmessagestrings = new stdClass();
763                 $smallmessagestrings->user = fullname($userfrom);
764                 $smallmessagestrings->forumname = "$shortname: ".format_string($forum->name,true).": ".$discussion->name;
765                 $smallmessagestrings->message = $post->message;
766                 //make sure strings are in message recipients language
767                 $eventdata->smallmessage = get_string_manager()->get_string('smallmessage', 'forum', $smallmessagestrings, $userto->lang);
769                 $eventdata->contexturl = "{$CFG->wwwroot}/mod/forum/discuss.php?d={$discussion->id}#p{$post->id}";
770                 $eventdata->contexturlname = $discussion->name;
772                 $mailresult = message_send($eventdata);
773                 if (!$mailresult){
774                     mtrace("Error: mod/forum/lib.php forum_cron(): Could not send out mail for id $post->id to user $userto->id".
775                          " ($userto->email) .. not trying again.");
776                     $errorcount[$post->id]++;
777                 } else {
778                     $mailcount[$post->id]++;
780                 // Mark post as read if forum_usermarksread is set off
781                     if (!$CFG->forum_usermarksread) {
782                         $userto->markposts[$post->id] = $post->id;
783                     }
784                 }
786                 mtrace('post '.$post->id. ': '.$post->subject);
787             }
789             // mark processed posts as read
790             forum_tp_mark_posts_read($userto, $userto->markposts);
791             unset($userto);
792         }
793     }
795     if ($posts) {
796         foreach ($posts as $post) {
797             mtrace($mailcount[$post->id]." users were sent post $post->id, '$post->subject'");
798             if ($errorcount[$post->id]) {
799                 $DB->set_field('forum_posts', 'mailed', FORUM_MAILED_ERROR, array('id' => $post->id));
800             }
801         }
802     }
804     // release some memory
805     unset($subscribedusers);
806     unset($mailcount);
807     unset($errorcount);
809     cron_setup_user();
811     $sitetimezone = $CFG->timezone;
813     // Now see if there are any digest mails waiting to be sent, and if we should send them
815     mtrace('Starting digest processing...');
817     core_php_time_limit::raise(300); // terminate if not able to fetch all digests in 5 minutes
819     if (!isset($CFG->digestmailtimelast)) {    // To catch the first time
820         set_config('digestmailtimelast', 0);
821     }
823     $timenow = time();
824     $digesttime = usergetmidnight($timenow, $sitetimezone) + ($CFG->digestmailtime * 3600);
826     // Delete any really old ones (normally there shouldn't be any)
827     $weekago = $timenow - (7 * 24 * 3600);
828     $DB->delete_records_select('forum_queue', "timemodified < ?", array($weekago));
829     mtrace ('Cleaned old digest records');
831     if ($CFG->digestmailtimelast < $digesttime and $timenow > $digesttime) {
833         mtrace('Sending forum digests: '.userdate($timenow, '', $sitetimezone));
835         $digestposts_rs = $DB->get_recordset_select('forum_queue', "timemodified < ?", array($digesttime));
837         if ($digestposts_rs->valid()) {
839             // We have work to do
840             $usermailcount = 0;
842             //caches - reuse the those filled before too
843             $discussionposts = array();
844             $userdiscussions = array();
846             foreach ($digestposts_rs as $digestpost) {
847                 if (!isset($posts[$digestpost->postid])) {
848                     if ($post = $DB->get_record('forum_posts', array('id' => $digestpost->postid))) {
849                         $posts[$digestpost->postid] = $post;
850                     } else {
851                         continue;
852                     }
853                 }
854                 $discussionid = $digestpost->discussionid;
855                 if (!isset($discussions[$discussionid])) {
856                     if ($discussion = $DB->get_record('forum_discussions', array('id' => $discussionid))) {
857                         $discussions[$discussionid] = $discussion;
858                     } else {
859                         continue;
860                     }
861                 }
862                 $forumid = $discussions[$discussionid]->forum;
863                 if (!isset($forums[$forumid])) {
864                     if ($forum = $DB->get_record('forum', array('id' => $forumid))) {
865                         $forums[$forumid] = $forum;
866                     } else {
867                         continue;
868                     }
869                 }
871                 $courseid = $forums[$forumid]->course;
872                 if (!isset($courses[$courseid])) {
873                     if ($course = $DB->get_record('course', array('id' => $courseid))) {
874                         $courses[$courseid] = $course;
875                     } else {
876                         continue;
877                     }
878                 }
880                 if (!isset($coursemodules[$forumid])) {
881                     if ($cm = get_coursemodule_from_instance('forum', $forumid, $courseid)) {
882                         $coursemodules[$forumid] = $cm;
883                     } else {
884                         continue;
885                     }
886                 }
887                 $userdiscussions[$digestpost->userid][$digestpost->discussionid] = $digestpost->discussionid;
888                 $discussionposts[$digestpost->discussionid][$digestpost->postid] = $digestpost->postid;
889             }
890             $digestposts_rs->close(); /// Finished iteration, let's close the resultset
892             // Data collected, start sending out emails to each user
893             foreach ($userdiscussions as $userid => $thesediscussions) {
895                 core_php_time_limit::raise(120); // terminate if processing of any account takes longer than 2 minutes
897                 cron_setup_user();
899                 mtrace(get_string('processingdigest', 'forum', $userid), '... ');
901                 // First of all delete all the queue entries for this user
902                 $DB->delete_records_select('forum_queue', "userid = ? AND timemodified < ?", array($userid, $digesttime));
904                 // Init user caches - we keep the cache for one cycle only,
905                 // otherwise it would unnecessarily consume memory.
906                 if (array_key_exists($userid, $users) and isset($users[$userid]->username)) {
907                     $userto = clone($users[$userid]);
908                 } else {
909                     $userto = $DB->get_record('user', array('id' => $userid));
910                     forum_cron_minimise_user_record($userto);
911                 }
912                 $userto->viewfullnames = array();
913                 $userto->canpost       = array();
914                 $userto->markposts     = array();
916                 // Override the language and timezone of the "current" user, so that
917                 // mail is customised for the receiver.
918                 cron_setup_user($userto);
920                 $postsubject = get_string('digestmailsubject', 'forum', format_string($site->shortname, true));
922                 $headerdata = new stdClass();
923                 $headerdata->sitename = format_string($site->fullname, true);
924                 $headerdata->userprefs = $CFG->wwwroot.'/user/edit.php?id='.$userid.'&amp;course='.$site->id;
926                 $posttext = get_string('digestmailheader', 'forum', $headerdata)."\n\n";
927                 $headerdata->userprefs = '<a target="_blank" href="'.$headerdata->userprefs.'">'.get_string('digestmailprefs', 'forum').'</a>';
929                 $posthtml = "<head>";
930 /*                foreach ($CFG->stylesheets as $stylesheet) {
931                     //TODO: MDL-21120
932                     $posthtml .= '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'" />'."\n";
933                 }*/
934                 $posthtml .= "</head>\n<body id=\"email\">\n";
935                 $posthtml .= '<p>'.get_string('digestmailheader', 'forum', $headerdata).'</p><br /><hr size="1" noshade="noshade" />';
937                 foreach ($thesediscussions as $discussionid) {
939                     core_php_time_limit::raise(120);   // to be reset for each post
941                     $discussion = $discussions[$discussionid];
942                     $forum      = $forums[$discussion->forum];
943                     $course     = $courses[$forum->course];
944                     $cm         = $coursemodules[$forum->id];
946                     //override language
947                     cron_setup_user($userto, $course);
949                     // Fill caches
950                     if (!isset($userto->viewfullnames[$forum->id])) {
951                         $modcontext = context_module::instance($cm->id);
952                         $userto->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $modcontext);
953                     }
954                     if (!isset($userto->canpost[$discussion->id])) {
955                         $modcontext = context_module::instance($cm->id);
956                         $userto->canpost[$discussion->id] = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
957                     }
959                     $strforums      = get_string('forums', 'forum');
960                     $canunsubscribe = ! forum_is_forcesubscribed($forum);
961                     $canreply       = $userto->canpost[$discussion->id];
962                     $shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
964                     $posttext .= "\n \n";
965                     $posttext .= '=====================================================================';
966                     $posttext .= "\n \n";
967                     $posttext .= "$shortname -> $strforums -> ".format_string($forum->name,true);
968                     if ($discussion->name != $forum->name) {
969                         $posttext  .= " -> ".format_string($discussion->name,true);
970                     }
971                     $posttext .= "\n";
972                     $posttext .= $CFG->wwwroot.'/mod/forum/discuss.php?d='.$discussion->id;
973                     $posttext .= "\n";
975                     $posthtml .= "<p><font face=\"sans-serif\">".
976                     "<a target=\"_blank\" href=\"$CFG->wwwroot/course/view.php?id=$course->id\">$shortname</a> -> ".
977                     "<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/index.php?id=$course->id\">$strforums</a> -> ".
978                     "<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/view.php?f=$forum->id\">".format_string($forum->name,true)."</a>";
979                     if ($discussion->name == $forum->name) {
980                         $posthtml .= "</font></p>";
981                     } else {
982                         $posthtml .= " -> <a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/discuss.php?d=$discussion->id\">".format_string($discussion->name,true)."</a></font></p>";
983                     }
984                     $posthtml .= '<p>';
986                     $postsarray = $discussionposts[$discussionid];
987                     sort($postsarray);
989                     foreach ($postsarray as $postid) {
990                         $post = $posts[$postid];
992                         if (array_key_exists($post->userid, $users)) { // we might know him/her already
993                             $userfrom = $users[$post->userid];
994                             if (!isset($userfrom->idnumber)) {
995                                 $userfrom = $DB->get_record('user', array('id' => $userfrom->id));
996                                 forum_cron_minimise_user_record($userfrom);
997                             }
999                         } else if ($userfrom = $DB->get_record('user', array('id' => $post->userid))) {
1000                             forum_cron_minimise_user_record($userfrom);
1001                             if ($userscount <= FORUM_CRON_USER_CACHE) {
1002                                 $userscount++;
1003                                 $users[$userfrom->id] = $userfrom;
1004                             }
1006                         } else {
1007                             mtrace('Could not find user '.$post->userid);
1008                             continue;
1009                         }
1011                         if (!isset($userfrom->groups[$forum->id])) {
1012                             if (!isset($userfrom->groups)) {
1013                                 $userfrom->groups = array();
1014                                 if (isset($users[$userfrom->id])) {
1015                                     $users[$userfrom->id]->groups = array();
1016                                 }
1017                             }
1018                             $userfrom->groups[$forum->id] = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid);
1019                             if (isset($users[$userfrom->id])) {
1020                                 $users[$userfrom->id]->groups[$forum->id] = $userfrom->groups[$forum->id];
1021                             }
1022                         }
1024                         $userfrom->customheaders = array ("Precedence: Bulk");
1026                         $maildigest = forum_get_user_maildigest_bulk($digests, $userto, $forum->id);
1027                         if ($maildigest == 2) {
1028                             // Subjects and link only
1029                             $posttext .= "\n";
1030                             $posttext .= $CFG->wwwroot.'/mod/forum/discuss.php?d='.$discussion->id;
1031                             $by = new stdClass();
1032                             $by->name = fullname($userfrom);
1033                             $by->date = userdate($post->modified);
1034                             $posttext .= "\n".format_string($post->subject,true).' '.get_string("bynameondate", "forum", $by);
1035                             $posttext .= "\n---------------------------------------------------------------------";
1037                             $by->name = "<a target=\"_blank\" href=\"$CFG->wwwroot/user/view.php?id=$userfrom->id&amp;course=$course->id\">$by->name</a>";
1038                             $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>';
1040                         } else {
1041                             // The full treatment
1042                             $posttext .= forum_make_mail_text($course, $cm, $forum, $discussion, $post, $userfrom, $userto, true);
1043                             $posthtml .= forum_make_mail_post($course, $cm, $forum, $discussion, $post, $userfrom, $userto, false, $canreply, true, false);
1045                         // Create an array of postid's for this user to mark as read.
1046                             if (!$CFG->forum_usermarksread) {
1047                                 $userto->markposts[$post->id] = $post->id;
1048                             }
1049                         }
1050                     }
1051                     $footerlinks = array();
1052                     if ($canunsubscribe) {
1053                         $footerlinks[] = "<a href=\"$CFG->wwwroot/mod/forum/subscribe.php?id=$forum->id\">" . get_string("unsubscribe", "forum") . "</a>";
1054                     } else {
1055                         $footerlinks[] = get_string("everyoneissubscribed", "forum");
1056                     }
1057                     $footerlinks[] = "<a href='{$CFG->wwwroot}/mod/forum/index.php?id={$forum->course}'>" . get_string("digestmailpost", "forum") . '</a>';
1058                     $posthtml .= "\n<div class='mdl-right'><font size=\"1\">" . implode('&nbsp;', $footerlinks) . '</font></div>';
1059                     $posthtml .= '<hr size="1" noshade="noshade" /></p>';
1060                 }
1061                 $posthtml .= '</body>';
1063                 if (empty($userto->mailformat) || $userto->mailformat != 1) {
1064                     // This user DOESN'T want to receive HTML
1065                     $posthtml = '';
1066                 }
1068                 $attachment = $attachname='';
1069                 // Directly email forum digests rather than sending them via messaging, use the
1070                 // site shortname as 'from name', the noreply address will be used by email_to_user.
1071                 $mailresult = email_to_user($userto, $site->shortname, $postsubject, $posttext, $posthtml, $attachment, $attachname);
1073                 if (!$mailresult) {
1074                     mtrace("ERROR: mod/forum/cron.php: Could not send out digest mail to user $userto->id ".
1075                         "($userto->email)... not trying again.");
1076                 } else {
1077                     mtrace("success.");
1078                     $usermailcount++;
1080                     // Mark post as read if forum_usermarksread is set off
1081                     forum_tp_mark_posts_read($userto, $userto->markposts);
1082                 }
1083             }
1084         }
1085     /// We have finishied all digest emails, update $CFG->digestmailtimelast
1086         set_config('digestmailtimelast', $timenow);
1087     }
1089     cron_setup_user();
1091     if (!empty($usermailcount)) {
1092         mtrace(get_string('digestsentusers', 'forum', $usermailcount));
1093     }
1095     if (!empty($CFG->forum_lastreadclean)) {
1096         $timenow = time();
1097         if ($CFG->forum_lastreadclean + (24*3600) < $timenow) {
1098             set_config('forum_lastreadclean', $timenow);
1099             mtrace('Removing old forum read tracking info...');
1100             forum_tp_clean_read_records();
1101         }
1102     } else {
1103         set_config('forum_lastreadclean', time());
1104     }
1107     return true;
1110 /**
1111  * Builds and returns the body of the email notification in plain text.
1112  *
1113  * @global object
1114  * @global object
1115  * @uses CONTEXT_MODULE
1116  * @param object $course
1117  * @param object $cm
1118  * @param object $forum
1119  * @param object $discussion
1120  * @param object $post
1121  * @param object $userfrom
1122  * @param object $userto
1123  * @param boolean $bare
1124  * @return string The email body in plain text format.
1125  */
1126 function forum_make_mail_text($course, $cm, $forum, $discussion, $post, $userfrom, $userto, $bare = false) {
1127     global $CFG, $USER;
1129     $modcontext = context_module::instance($cm->id);
1131     if (!isset($userto->viewfullnames[$forum->id])) {
1132         $viewfullnames = has_capability('moodle/site:viewfullnames', $modcontext, $userto->id);
1133     } else {
1134         $viewfullnames = $userto->viewfullnames[$forum->id];
1135     }
1137     if (!isset($userto->canpost[$discussion->id])) {
1138         $canreply = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
1139     } else {
1140         $canreply = $userto->canpost[$discussion->id];
1141     }
1143     $by = New stdClass;
1144     $by->name = fullname($userfrom, $viewfullnames);
1145     $by->date = userdate($post->modified, "", $userto->timezone);
1147     $strbynameondate = get_string('bynameondate', 'forum', $by);
1149     $strforums = get_string('forums', 'forum');
1151     $canunsubscribe = ! forum_is_forcesubscribed($forum);
1153     $posttext = '';
1155     if (!$bare) {
1156         $shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
1157         $posttext  = "$shortname -> $strforums -> ".format_string($forum->name,true);
1159         if ($discussion->name != $forum->name) {
1160             $posttext  .= " -> ".format_string($discussion->name,true);
1161         }
1162     }
1164     // add absolute file links
1165     $post->message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php', $modcontext->id, 'mod_forum', 'post', $post->id);
1167     $posttext .= "\n";
1168     $posttext .= $CFG->wwwroot.'/mod/forum/discuss.php?d='.$discussion->id;
1169     $posttext .= "\n---------------------------------------------------------------------\n";
1170     $posttext .= format_string($post->subject,true);
1171     if ($bare) {
1172         $posttext .= " ($CFG->wwwroot/mod/forum/discuss.php?d=$discussion->id#p$post->id)";
1173     }
1174     $posttext .= "\n".$strbynameondate."\n";
1175     $posttext .= "---------------------------------------------------------------------\n";
1176     $posttext .= format_text_email($post->message, $post->messageformat);
1177     $posttext .= "\n\n";
1178     $posttext .= forum_print_attachments($post, $cm, "text");
1180     if (!$bare && $canreply) {
1181         $posttext .= "---------------------------------------------------------------------\n";
1182         $posttext .= get_string("postmailinfo", "forum", $shortname)."\n";
1183         $posttext .= "$CFG->wwwroot/mod/forum/post.php?reply=$post->id\n";
1184     }
1185     if (!$bare && $canunsubscribe) {
1186         $posttext .= "\n---------------------------------------------------------------------\n";
1187         $posttext .= get_string("unsubscribe", "forum");
1188         $posttext .= ": $CFG->wwwroot/mod/forum/subscribe.php?id=$forum->id\n";
1189     }
1191     $posttext .= "\n---------------------------------------------------------------------\n";
1192     $posttext .= get_string("digestmailpost", "forum");
1193     $posttext .= ": {$CFG->wwwroot}/mod/forum/index.php?id={$forum->course}\n";
1195     return $posttext;
1198 /**
1199  * Builds and returns the body of the email notification in html format.
1200  *
1201  * @global object
1202  * @param object $course
1203  * @param object $cm
1204  * @param object $forum
1205  * @param object $discussion
1206  * @param object $post
1207  * @param object $userfrom
1208  * @param object $userto
1209  * @return string The email text in HTML format
1210  */
1211 function forum_make_mail_html($course, $cm, $forum, $discussion, $post, $userfrom, $userto) {
1212     global $CFG;
1214     if ($userto->mailformat != 1) {  // Needs to be HTML
1215         return '';
1216     }
1218     if (!isset($userto->canpost[$discussion->id])) {
1219         $canreply = forum_user_can_post($forum, $discussion, $userto, $cm, $course);
1220     } else {
1221         $canreply = $userto->canpost[$discussion->id];
1222     }
1224     $strforums = get_string('forums', 'forum');
1225     $canunsubscribe = ! forum_is_forcesubscribed($forum);
1226     $shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
1228     $posthtml = '<head>';
1229 /*    foreach ($CFG->stylesheets as $stylesheet) {
1230         //TODO: MDL-21120
1231         $posthtml .= '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'" />'."\n";
1232     }*/
1233     $posthtml .= '</head>';
1234     $posthtml .= "\n<body id=\"email\">\n\n";
1236     $posthtml .= '<div class="navbar">'.
1237     '<a target="_blank" href="'.$CFG->wwwroot.'/course/view.php?id='.$course->id.'">'.$shortname.'</a> &raquo; '.
1238     '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/index.php?id='.$course->id.'">'.$strforums.'</a> &raquo; '.
1239     '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/view.php?f='.$forum->id.'">'.format_string($forum->name,true).'</a>';
1240     if ($discussion->name == $forum->name) {
1241         $posthtml .= '</div>';
1242     } else {
1243         $posthtml .= ' &raquo; <a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$discussion->id.'">'.
1244                      format_string($discussion->name,true).'</a></div>';
1245     }
1246     $posthtml .= forum_make_mail_post($course, $cm, $forum, $discussion, $post, $userfrom, $userto, false, $canreply, true, false);
1248     $footerlinks = array();
1249     if ($canunsubscribe) {
1250         $footerlinks[] = '<a href="' . $CFG->wwwroot . '/mod/forum/subscribe.php?id=' . $forum->id . '">' . get_string('unsubscribe', 'forum') . '</a>';
1251         $footerlinks[] = '<a href="' . $CFG->wwwroot . '/mod/forum/unsubscribeall.php">' . get_string('unsubscribeall', 'forum') . '</a>';
1252     }
1253     $footerlinks[] = "<a href='{$CFG->wwwroot}/mod/forum/index.php?id={$forum->course}'>" . get_string('digestmailpost', 'forum') . '</a>';
1254     $posthtml .= '<hr /><div class="mdl-align unsubscribelink">' . implode('&nbsp;', $footerlinks) . '</div>';
1256     $posthtml .= '</body>';
1258     return $posthtml;
1262 /**
1263  *
1264  * @param object $course
1265  * @param object $user
1266  * @param object $mod TODO this is not used in this function, refactor
1267  * @param object $forum
1268  * @return object A standard object with 2 variables: info (number of posts for this user) and time (last modified)
1269  */
1270 function forum_user_outline($course, $user, $mod, $forum) {
1271     global $CFG;
1272     require_once("$CFG->libdir/gradelib.php");
1273     $grades = grade_get_grades($course->id, 'mod', 'forum', $forum->id, $user->id);
1274     if (empty($grades->items[0]->grades)) {
1275         $grade = false;
1276     } else {
1277         $grade = reset($grades->items[0]->grades);
1278     }
1280     $count = forum_count_user_posts($forum->id, $user->id);
1282     if ($count && $count->postcount > 0) {
1283         $result = new stdClass();
1284         $result->info = get_string("numposts", "forum", $count->postcount);
1285         $result->time = $count->lastpost;
1286         if ($grade) {
1287             $result->info .= ', ' . get_string('grade') . ': ' . $grade->str_long_grade;
1288         }
1289         return $result;
1290     } else if ($grade) {
1291         $result = new stdClass();
1292         $result->info = get_string('grade') . ': ' . $grade->str_long_grade;
1294         //datesubmitted == time created. dategraded == time modified or time overridden
1295         //if grade was last modified by the user themselves use date graded. Otherwise use date submitted
1296         //TODO: move this copied & pasted code somewhere in the grades API. See MDL-26704
1297         if ($grade->usermodified == $user->id || empty($grade->datesubmitted)) {
1298             $result->time = $grade->dategraded;
1299         } else {
1300             $result->time = $grade->datesubmitted;
1301         }
1303         return $result;
1304     }
1305     return NULL;
1309 /**
1310  * @global object
1311  * @global object
1312  * @param object $coure
1313  * @param object $user
1314  * @param object $mod
1315  * @param object $forum
1316  */
1317 function forum_user_complete($course, $user, $mod, $forum) {
1318     global $CFG,$USER, $OUTPUT;
1319     require_once("$CFG->libdir/gradelib.php");
1321     $grades = grade_get_grades($course->id, 'mod', 'forum', $forum->id, $user->id);
1322     if (!empty($grades->items[0]->grades)) {
1323         $grade = reset($grades->items[0]->grades);
1324         echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade);
1325         if ($grade->str_feedback) {
1326             echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
1327         }
1328     }
1330     if ($posts = forum_get_user_posts($forum->id, $user->id)) {
1332         if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $course->id)) {
1333             print_error('invalidcoursemodule');
1334         }
1335         $discussions = forum_get_user_involved_discussions($forum->id, $user->id);
1337         foreach ($posts as $post) {
1338             if (!isset($discussions[$post->discussion])) {
1339                 continue;
1340             }
1341             $discussion = $discussions[$post->discussion];
1343             forum_print_post($post, $discussion, $forum, $cm, $course, false, false, false);
1344         }
1345     } else {
1346         echo "<p>".get_string("noposts", "forum")."</p>";
1347     }
1355 /**
1356  * @global object
1357  * @global object
1358  * @global object
1359  * @param array $courses
1360  * @param array $htmlarray
1361  */
1362 function forum_print_overview($courses,&$htmlarray) {
1363     global $USER, $CFG, $DB, $SESSION;
1365     if (empty($courses) || !is_array($courses) || count($courses) == 0) {
1366         return array();
1367     }
1369     if (!$forums = get_all_instances_in_courses('forum',$courses)) {
1370         return;
1371     }
1373     // Courses to search for new posts
1374     $coursessqls = array();
1375     $params = array();
1376     foreach ($courses as $course) {
1378         // If the user has never entered into the course all posts are pending
1379         if ($course->lastaccess == 0) {
1380             $coursessqls[] = '(f.course = ?)';
1381             $params[] = $course->id;
1383         // Only posts created after the course last access
1384         } else {
1385             $coursessqls[] = '(f.course = ? AND p.created > ?)';
1386             $params[] = $course->id;
1387             $params[] = $course->lastaccess;
1388         }
1389     }
1390     $params[] = $USER->id;
1391     $coursessql = implode(' OR ', $coursessqls);
1393     $sql = "SELECT f.id, COUNT(*) as count "
1394                 .'FROM {forum} f '
1395                 .'JOIN {forum_discussions} d ON d.forum  = f.id '
1396                 .'JOIN {forum_posts} p ON p.discussion = d.id '
1397                 ."WHERE ($coursessql) "
1398                 .'AND p.userid != ? '
1399                 .'GROUP BY f.id';
1401     if (!$new = $DB->get_records_sql($sql, $params)) {
1402         $new = array(); // avoid warnings
1403     }
1405     // also get all forum tracking stuff ONCE.
1406     $trackingforums = array();
1407     foreach ($forums as $forum) {
1408         if (forum_tp_can_track_forums($forum)) {
1409             $trackingforums[$forum->id] = $forum;
1410         }
1411     }
1413     if (count($trackingforums) > 0) {
1414         $cutoffdate = isset($CFG->forum_oldpostdays) ? (time() - ($CFG->forum_oldpostdays*24*60*60)) : 0;
1415         $sql = 'SELECT d.forum,d.course,COUNT(p.id) AS count '.
1416             ' FROM {forum_posts} p '.
1417             ' JOIN {forum_discussions} d ON p.discussion = d.id '.
1418             ' LEFT JOIN {forum_read} r ON r.postid = p.id AND r.userid = ? WHERE (';
1419         $params = array($USER->id);
1421         foreach ($trackingforums as $track) {
1422             $sql .= '(d.forum = ? AND (d.groupid = -1 OR d.groupid = 0 OR d.groupid = ?)) OR ';
1423             $params[] = $track->id;
1424             if (isset($SESSION->currentgroup[$track->course])) {
1425                 $groupid =  $SESSION->currentgroup[$track->course];
1426             } else {
1427                 // get first groupid
1428                 $groupids = groups_get_all_groups($track->course, $USER->id);
1429                 if ($groupids) {
1430                     reset($groupids);
1431                     $groupid = key($groupids);
1432                     $SESSION->currentgroup[$track->course] = $groupid;
1433                 } else {
1434                     $groupid = 0;
1435                 }
1436                 unset($groupids);
1437             }
1438             $params[] = $groupid;
1439         }
1440         $sql = substr($sql,0,-3); // take off the last OR
1441         $sql .= ') AND p.modified >= ? AND r.id is NULL GROUP BY d.forum,d.course';
1442         $params[] = $cutoffdate;
1444         if (!$unread = $DB->get_records_sql($sql, $params)) {
1445             $unread = array();
1446         }
1447     } else {
1448         $unread = array();
1449     }
1451     if (empty($unread) and empty($new)) {
1452         return;
1453     }
1455     $strforum = get_string('modulename','forum');
1457     foreach ($forums as $forum) {
1458         $str = '';
1459         $count = 0;
1460         $thisunread = 0;
1461         $showunread = false;
1462         // either we have something from logs, or trackposts, or nothing.
1463         if (array_key_exists($forum->id, $new) && !empty($new[$forum->id])) {
1464             $count = $new[$forum->id]->count;
1465         }
1466         if (array_key_exists($forum->id,$unread)) {
1467             $thisunread = $unread[$forum->id]->count;
1468             $showunread = true;
1469         }
1470         if ($count > 0 || $thisunread > 0) {
1471             $str .= '<div class="overview forum"><div class="name">'.$strforum.': <a title="'.$strforum.'" href="'.$CFG->wwwroot.'/mod/forum/view.php?f='.$forum->id.'">'.
1472                 $forum->name.'</a></div>';
1473             $str .= '<div class="info"><span class="postsincelogin">';
1474             $str .= get_string('overviewnumpostssince', 'forum', $count)."</span>";
1475             if (!empty($showunread)) {
1476                 $str .= '<div class="unreadposts">'.get_string('overviewnumunread', 'forum', $thisunread).'</div>';
1477             }
1478             $str .= '</div></div>';
1479         }
1480         if (!empty($str)) {
1481             if (!array_key_exists($forum->course,$htmlarray)) {
1482                 $htmlarray[$forum->course] = array();
1483             }
1484             if (!array_key_exists('forum',$htmlarray[$forum->course])) {
1485                 $htmlarray[$forum->course]['forum'] = ''; // initialize, avoid warnings
1486             }
1487             $htmlarray[$forum->course]['forum'] .= $str;
1488         }
1489     }
1492 /**
1493  * Given a course and a date, prints a summary of all the new
1494  * messages posted in the course since that date
1495  *
1496  * @global object
1497  * @global object
1498  * @global object
1499  * @uses CONTEXT_MODULE
1500  * @uses VISIBLEGROUPS
1501  * @param object $course
1502  * @param bool $viewfullnames capability
1503  * @param int $timestart
1504  * @return bool success
1505  */
1506 function forum_print_recent_activity($course, $viewfullnames, $timestart) {
1507     global $CFG, $USER, $DB, $OUTPUT;
1509     // do not use log table if possible, it may be huge and is expensive to join with other tables
1511     $allnamefields = user_picture::fields('u', null, 'duserid');
1512     if (!$posts = $DB->get_records_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid,
1513                                               d.timestart, d.timeend, $allnamefields
1514                                          FROM {forum_posts} p
1515                                               JOIN {forum_discussions} d ON d.id = p.discussion
1516                                               JOIN {forum} f             ON f.id = d.forum
1517                                               JOIN {user} u              ON u.id = p.userid
1518                                         WHERE p.created > ? AND f.course = ?
1519                                      ORDER BY p.id ASC", array($timestart, $course->id))) { // order by initial posting date
1520          return false;
1521     }
1523     $modinfo = get_fast_modinfo($course);
1525     $groupmodes = array();
1526     $cms    = array();
1528     $strftimerecent = get_string('strftimerecent');
1530     $printposts = array();
1531     foreach ($posts as $post) {
1532         if (!isset($modinfo->instances['forum'][$post->forum])) {
1533             // not visible
1534             continue;
1535         }
1536         $cm = $modinfo->instances['forum'][$post->forum];
1537         if (!$cm->uservisible) {
1538             continue;
1539         }
1540         $context = context_module::instance($cm->id);
1542         if (!has_capability('mod/forum:viewdiscussion', $context)) {
1543             continue;
1544         }
1546         if (!empty($CFG->forum_enabletimedposts) and $USER->id != $post->duserid
1547           and (($post->timestart > 0 and $post->timestart > time()) or ($post->timeend > 0 and $post->timeend < time()))) {
1548             if (!has_capability('mod/forum:viewhiddentimedposts', $context)) {
1549                 continue;
1550             }
1551         }
1553         $groupmode = groups_get_activity_groupmode($cm, $course);
1555         if ($groupmode) {
1556             if ($post->groupid == -1 or $groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $context)) {
1557                 // oki (Open discussions have groupid -1)
1558             } else {
1559                 // separate mode
1560                 if (isguestuser()) {
1561                     // shortcut
1562                     continue;
1563                 }
1565                 if (!in_array($post->groupid, $modinfo->get_groups($cm->groupingid))) {
1566                     continue;
1567                 }
1568             }
1569         }
1571         $printposts[] = $post;
1572     }
1573     unset($posts);
1575     if (!$printposts) {
1576         return false;
1577     }
1579     echo $OUTPUT->heading(get_string('newforumposts', 'forum').':', 3);
1580     echo "\n<ul class='unlist'>\n";
1582     foreach ($printposts as $post) {
1583         $subjectclass = empty($post->parent) ? ' bold' : '';
1585         echo '<li><div class="head">'.
1586                '<div class="date">'.userdate($post->modified, $strftimerecent).'</div>'.
1587                '<div class="name">'.fullname($post, $viewfullnames).'</div>'.
1588              '</div>';
1589         echo '<div class="info'.$subjectclass.'">';
1590         if (empty($post->parent)) {
1591             echo '"<a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'">';
1592         } else {
1593             echo '"<a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'&amp;parent='.$post->parent.'#p'.$post->id.'">';
1594         }
1595         $post->subject = break_up_long_words(format_string($post->subject, true));
1596         echo $post->subject;
1597         echo "</a>\"</div></li>\n";
1598     }
1600     echo "</ul>\n";
1602     return true;
1605 /**
1606  * Return grade for given user or all users.
1607  *
1608  * @global object
1609  * @global object
1610  * @param object $forum
1611  * @param int $userid optional user id, 0 means all users
1612  * @return array array of grades, false if none
1613  */
1614 function forum_get_user_grades($forum, $userid = 0) {
1615     global $CFG;
1617     require_once($CFG->dirroot.'/rating/lib.php');
1619     $ratingoptions = new stdClass;
1620     $ratingoptions->component = 'mod_forum';
1621     $ratingoptions->ratingarea = 'post';
1623     //need these to work backwards to get a context id. Is there a better way to get contextid from a module instance?
1624     $ratingoptions->modulename = 'forum';
1625     $ratingoptions->moduleid   = $forum->id;
1626     $ratingoptions->userid = $userid;
1627     $ratingoptions->aggregationmethod = $forum->assessed;
1628     $ratingoptions->scaleid = $forum->scale;
1629     $ratingoptions->itemtable = 'forum_posts';
1630     $ratingoptions->itemtableusercolumn = 'userid';
1632     $rm = new rating_manager();
1633     return $rm->get_user_grades($ratingoptions);
1636 /**
1637  * Update activity grades
1638  *
1639  * @category grade
1640  * @param object $forum
1641  * @param int $userid specific user only, 0 means all
1642  * @param boolean $nullifnone return null if grade does not exist
1643  * @return void
1644  */
1645 function forum_update_grades($forum, $userid=0, $nullifnone=true) {
1646     global $CFG, $DB;
1647     require_once($CFG->libdir.'/gradelib.php');
1649     if (!$forum->assessed) {
1650         forum_grade_item_update($forum);
1652     } else if ($grades = forum_get_user_grades($forum, $userid)) {
1653         forum_grade_item_update($forum, $grades);
1655     } else if ($userid and $nullifnone) {
1656         $grade = new stdClass();
1657         $grade->userid   = $userid;
1658         $grade->rawgrade = NULL;
1659         forum_grade_item_update($forum, $grade);
1661     } else {
1662         forum_grade_item_update($forum);
1663     }
1666 /**
1667  * Update all grades in gradebook.
1668  * @global object
1669  */
1670 function forum_upgrade_grades() {
1671     global $DB;
1673     $sql = "SELECT COUNT('x')
1674               FROM {forum} f, {course_modules} cm, {modules} m
1675              WHERE m.name='forum' AND m.id=cm.module AND cm.instance=f.id";
1676     $count = $DB->count_records_sql($sql);
1678     $sql = "SELECT f.*, cm.idnumber AS cmidnumber, f.course AS courseid
1679               FROM {forum} f, {course_modules} cm, {modules} m
1680              WHERE m.name='forum' AND m.id=cm.module AND cm.instance=f.id";
1681     $rs = $DB->get_recordset_sql($sql);
1682     if ($rs->valid()) {
1683         $pbar = new progress_bar('forumupgradegrades', 500, true);
1684         $i=0;
1685         foreach ($rs as $forum) {
1686             $i++;
1687             upgrade_set_timeout(60*5); // set up timeout, may also abort execution
1688             forum_update_grades($forum, 0, false);
1689             $pbar->update($i, $count, "Updating Forum grades ($i/$count).");
1690         }
1691     }
1692     $rs->close();
1695 /**
1696  * Create/update grade item for given forum
1697  *
1698  * @category grade
1699  * @uses GRADE_TYPE_NONE
1700  * @uses GRADE_TYPE_VALUE
1701  * @uses GRADE_TYPE_SCALE
1702  * @param stdClass $forum Forum object with extra cmidnumber
1703  * @param mixed $grades Optional array/object of grade(s); 'reset' means reset grades in gradebook
1704  * @return int 0 if ok
1705  */
1706 function forum_grade_item_update($forum, $grades=NULL) {
1707     global $CFG;
1708     if (!function_exists('grade_update')) { //workaround for buggy PHP versions
1709         require_once($CFG->libdir.'/gradelib.php');
1710     }
1712     $params = array('itemname'=>$forum->name, 'idnumber'=>$forum->cmidnumber);
1714     if (!$forum->assessed or $forum->scale == 0) {
1715         $params['gradetype'] = GRADE_TYPE_NONE;
1717     } else if ($forum->scale > 0) {
1718         $params['gradetype'] = GRADE_TYPE_VALUE;
1719         $params['grademax']  = $forum->scale;
1720         $params['grademin']  = 0;
1722     } else if ($forum->scale < 0) {
1723         $params['gradetype'] = GRADE_TYPE_SCALE;
1724         $params['scaleid']   = -$forum->scale;
1725     }
1727     if ($grades  === 'reset') {
1728         $params['reset'] = true;
1729         $grades = NULL;
1730     }
1732     return grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, $grades, $params);
1735 /**
1736  * Delete grade item for given forum
1737  *
1738  * @category grade
1739  * @param stdClass $forum Forum object
1740  * @return grade_item
1741  */
1742 function forum_grade_item_delete($forum) {
1743     global $CFG;
1744     require_once($CFG->libdir.'/gradelib.php');
1746     return grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, NULL, array('deleted'=>1));
1750 /**
1751  * This function returns if a scale is being used by one forum
1752  *
1753  * @global object
1754  * @param int $forumid
1755  * @param int $scaleid negative number
1756  * @return bool
1757  */
1758 function forum_scale_used ($forumid,$scaleid) {
1759     global $DB;
1760     $return = false;
1762     $rec = $DB->get_record("forum",array("id" => "$forumid","scale" => "-$scaleid"));
1764     if (!empty($rec) && !empty($scaleid)) {
1765         $return = true;
1766     }
1768     return $return;
1771 /**
1772  * Checks if scale is being used by any instance of forum
1773  *
1774  * This is used to find out if scale used anywhere
1775  *
1776  * @global object
1777  * @param $scaleid int
1778  * @return boolean True if the scale is used by any forum
1779  */
1780 function forum_scale_used_anywhere($scaleid) {
1781     global $DB;
1782     if ($scaleid and $DB->record_exists('forum', array('scale' => -$scaleid))) {
1783         return true;
1784     } else {
1785         return false;
1786     }
1789 // SQL FUNCTIONS ///////////////////////////////////////////////////////////
1791 /**
1792  * Gets a post with all info ready for forum_print_post
1793  * Most of these joins are just to get the forum id
1794  *
1795  * @global object
1796  * @global object
1797  * @param int $postid
1798  * @return mixed array of posts or false
1799  */
1800 function forum_get_post_full($postid) {
1801     global $CFG, $DB;
1803     $allnames = get_all_user_name_fields(true, 'u');
1804     return $DB->get_record_sql("SELECT p.*, d.forum, $allnames, u.email, u.picture, u.imagealt
1805                              FROM {forum_posts} p
1806                                   JOIN {forum_discussions} d ON p.discussion = d.id
1807                                   LEFT JOIN {user} u ON p.userid = u.id
1808                             WHERE p.id = ?", array($postid));
1811 /**
1812  * Gets posts with all info ready for forum_print_post
1813  * We pass forumid in because we always know it so no need to make a
1814  * complicated join to find it out.
1815  *
1816  * @global object
1817  * @global object
1818  * @return mixed array of posts or false
1819  */
1820 function forum_get_discussion_posts($discussion, $sort, $forumid) {
1821     global $CFG, $DB;
1823     $allnames = get_all_user_name_fields(true, 'u');
1824     return $DB->get_records_sql("SELECT p.*, $forumid AS forum, $allnames, u.email, u.picture, u.imagealt
1825                               FROM {forum_posts} p
1826                          LEFT JOIN {user} u ON p.userid = u.id
1827                              WHERE p.discussion = ?
1828                                AND p.parent > 0 $sort", array($discussion));
1831 /**
1832  * Gets all posts in discussion including top parent.
1833  *
1834  * @global object
1835  * @global object
1836  * @global object
1837  * @param int $discussionid
1838  * @param string $sort
1839  * @param bool $tracking does user track the forum?
1840  * @return array of posts
1841  */
1842 function forum_get_all_discussion_posts($discussionid, $sort, $tracking=false) {
1843     global $CFG, $DB, $USER;
1845     $tr_sel  = "";
1846     $tr_join = "";
1847     $params = array();
1849     if ($tracking) {
1850         $now = time();
1851         $cutoffdate = $now - ($CFG->forum_oldpostdays * 24 * 3600);
1852         $tr_sel  = ", fr.id AS postread";
1853         $tr_join = "LEFT JOIN {forum_read} fr ON (fr.postid = p.id AND fr.userid = ?)";
1854         $params[] = $USER->id;
1855     }
1857     $allnames = get_all_user_name_fields(true, 'u');
1858     $params[] = $discussionid;
1859     if (!$posts = $DB->get_records_sql("SELECT p.*, $allnames, u.email, u.picture, u.imagealt $tr_sel
1860                                      FROM {forum_posts} p
1861                                           LEFT JOIN {user} u ON p.userid = u.id
1862                                           $tr_join
1863                                     WHERE p.discussion = ?
1864                                  ORDER BY $sort", $params)) {
1865         return array();
1866     }
1868     foreach ($posts as $pid=>$p) {
1869         if ($tracking) {
1870             if (forum_tp_is_post_old($p)) {
1871                  $posts[$pid]->postread = true;
1872             }
1873         }
1874         if (!$p->parent) {
1875             continue;
1876         }
1877         if (!isset($posts[$p->parent])) {
1878             continue; // parent does not exist??
1879         }
1880         if (!isset($posts[$p->parent]->children)) {
1881             $posts[$p->parent]->children = array();
1882         }
1883         $posts[$p->parent]->children[$pid] =& $posts[$pid];
1884     }
1886     return $posts;
1889 /**
1890  * Gets posts with all info ready for forum_print_post
1891  * We pass forumid in because we always know it so no need to make a
1892  * complicated join to find it out.
1893  *
1894  * @global object
1895  * @global object
1896  * @param int $parent
1897  * @param int $forumid
1898  * @return array
1899  */
1900 function forum_get_child_posts($parent, $forumid) {
1901     global $CFG, $DB;
1903     $allnames = get_all_user_name_fields(true, 'u');
1904     return $DB->get_records_sql("SELECT p.*, $forumid AS forum, $allnames, u.email, u.picture, u.imagealt
1905                               FROM {forum_posts} p
1906                          LEFT JOIN {user} u ON p.userid = u.id
1907                              WHERE p.parent = ?
1908                           ORDER BY p.created ASC", array($parent));
1911 /**
1912  * An array of forum objects that the user is allowed to read/search through.
1913  *
1914  * @global object
1915  * @global object
1916  * @global object
1917  * @param int $userid
1918  * @param int $courseid if 0, we look for forums throughout the whole site.
1919  * @return array of forum objects, or false if no matches
1920  *         Forum objects have the following attributes:
1921  *         id, type, course, cmid, cmvisible, cmgroupmode, accessallgroups,
1922  *         viewhiddentimedposts
1923  */
1924 function forum_get_readable_forums($userid, $courseid=0) {
1926     global $CFG, $DB, $USER;
1927     require_once($CFG->dirroot.'/course/lib.php');
1929     if (!$forummod = $DB->get_record('modules', array('name' => 'forum'))) {
1930         print_error('notinstalled', 'forum');
1931     }
1933     if ($courseid) {
1934         $courses = $DB->get_records('course', array('id' => $courseid));
1935     } else {
1936         // If no course is specified, then the user can see SITE + his courses.
1937         $courses1 = $DB->get_records('course', array('id' => SITEID));
1938         $courses2 = enrol_get_users_courses($userid, true, array('modinfo'));
1939         $courses = array_merge($courses1, $courses2);
1940     }
1941     if (!$courses) {
1942         return array();
1943     }
1945     $readableforums = array();
1947     foreach ($courses as $course) {
1949         $modinfo = get_fast_modinfo($course);
1951         if (empty($modinfo->instances['forum'])) {
1952             // hmm, no forums?
1953             continue;
1954         }
1956         $courseforums = $DB->get_records('forum', array('course' => $course->id));
1958         foreach ($modinfo->instances['forum'] as $forumid => $cm) {
1959             if (!$cm->uservisible or !isset($courseforums[$forumid])) {
1960                 continue;
1961             }
1962             $context = context_module::instance($cm->id);
1963             $forum = $courseforums[$forumid];
1964             $forum->context = $context;
1965             $forum->cm = $cm;
1967             if (!has_capability('mod/forum:viewdiscussion', $context)) {
1968                 continue;
1969             }
1971          /// group access
1972             if (groups_get_activity_groupmode($cm, $course) == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) {
1974                 $forum->onlygroups = $modinfo->get_groups($cm->groupingid);
1975                 $forum->onlygroups[] = -1;
1976             }
1978         /// hidden timed discussions
1979             $forum->viewhiddentimedposts = true;
1980             if (!empty($CFG->forum_enabletimedposts)) {
1981                 if (!has_capability('mod/forum:viewhiddentimedposts', $context)) {
1982                     $forum->viewhiddentimedposts = false;
1983                 }
1984             }
1986         /// qanda access
1987             if ($forum->type == 'qanda'
1988                     && !has_capability('mod/forum:viewqandawithoutposting', $context)) {
1990                 // We need to check whether the user has posted in the qanda forum.
1991                 $forum->onlydiscussions = array();  // Holds discussion ids for the discussions
1992                                                     // the user is allowed to see in this forum.
1993                 if ($discussionspostedin = forum_discussions_user_has_posted_in($forum->id, $USER->id)) {
1994                     foreach ($discussionspostedin as $d) {
1995                         $forum->onlydiscussions[] = $d->id;
1996                     }
1997                 }
1998             }
2000             $readableforums[$forum->id] = $forum;
2001         }
2003         unset($modinfo);
2005     } // End foreach $courses
2007     return $readableforums;
2010 /**
2011  * Returns a list of posts found using an array of search terms.
2012  *
2013  * @global object
2014  * @global object
2015  * @global object
2016  * @param array $searchterms array of search terms, e.g. word +word -word
2017  * @param int $courseid if 0, we search through the whole site
2018  * @param int $limitfrom
2019  * @param int $limitnum
2020  * @param int &$totalcount
2021  * @param string $extrasql
2022  * @return array|bool Array of posts found or false
2023  */
2024 function forum_search_posts($searchterms, $courseid=0, $limitfrom=0, $limitnum=50,
2025                             &$totalcount, $extrasql='') {
2026     global $CFG, $DB, $USER;
2027     require_once($CFG->libdir.'/searchlib.php');
2029     $forums = forum_get_readable_forums($USER->id, $courseid);
2031     if (count($forums) == 0) {
2032         $totalcount = 0;
2033         return false;
2034     }
2036     $now = round(time(), -2); // db friendly
2038     $fullaccess = array();
2039     $where = array();
2040     $params = array();
2042     foreach ($forums as $forumid => $forum) {
2043         $select = array();
2045         if (!$forum->viewhiddentimedposts) {
2046             $select[] = "(d.userid = :userid{$forumid} OR (d.timestart < :timestart{$forumid} AND (d.timeend = 0 OR d.timeend > :timeend{$forumid})))";
2047             $params = array_merge($params, array('userid'.$forumid=>$USER->id, 'timestart'.$forumid=>$now, 'timeend'.$forumid=>$now));
2048         }
2050         $cm = $forum->cm;
2051         $context = $forum->context;
2053         if ($forum->type == 'qanda'
2054             && !has_capability('mod/forum:viewqandawithoutposting', $context)) {
2055             if (!empty($forum->onlydiscussions)) {
2056                 list($discussionid_sql, $discussionid_params) = $DB->get_in_or_equal($forum->onlydiscussions, SQL_PARAMS_NAMED, 'qanda'.$forumid.'_');
2057                 $params = array_merge($params, $discussionid_params);
2058                 $select[] = "(d.id $discussionid_sql OR p.parent = 0)";
2059             } else {
2060                 $select[] = "p.parent = 0";
2061             }
2062         }
2064         if (!empty($forum->onlygroups)) {
2065             list($groupid_sql, $groupid_params) = $DB->get_in_or_equal($forum->onlygroups, SQL_PARAMS_NAMED, 'grps'.$forumid.'_');
2066             $params = array_merge($params, $groupid_params);
2067             $select[] = "d.groupid $groupid_sql";
2068         }
2070         if ($select) {
2071             $selects = implode(" AND ", $select);
2072             $where[] = "(d.forum = :forum{$forumid} AND $selects)";
2073             $params['forum'.$forumid] = $forumid;
2074         } else {
2075             $fullaccess[] = $forumid;
2076         }
2077     }
2079     if ($fullaccess) {
2080         list($fullid_sql, $fullid_params) = $DB->get_in_or_equal($fullaccess, SQL_PARAMS_NAMED, 'fula');
2081         $params = array_merge($params, $fullid_params);
2082         $where[] = "(d.forum $fullid_sql)";
2083     }
2085     $selectdiscussion = "(".implode(" OR ", $where).")";
2087     $messagesearch = '';
2088     $searchstring = '';
2090     // Need to concat these back together for parser to work.
2091     foreach($searchterms as $searchterm){
2092         if ($searchstring != '') {
2093             $searchstring .= ' ';
2094         }
2095         $searchstring .= $searchterm;
2096     }
2098     // We need to allow quoted strings for the search. The quotes *should* be stripped
2099     // by the parser, but this should be examined carefully for security implications.
2100     $searchstring = str_replace("\\\"","\"",$searchstring);
2101     $parser = new search_parser();
2102     $lexer = new search_lexer($parser);
2104     if ($lexer->parse($searchstring)) {
2105         $parsearray = $parser->get_parsed_array();
2106     // Experimental feature under 1.8! MDL-8830
2107     // Use alternative text searches if defined
2108     // This feature only works under mysql until properly implemented for other DBs
2109     // Requires manual creation of text index for forum_posts before enabling it:
2110     // CREATE FULLTEXT INDEX foru_post_tix ON [prefix]forum_posts (subject, message)
2111     // Experimental feature under 1.8! MDL-8830
2112         if (!empty($CFG->forum_usetextsearches)) {
2113             list($messagesearch, $msparams) = search_generate_text_SQL($parsearray, 'p.message', 'p.subject',
2114                                                  'p.userid', 'u.id', 'u.firstname',
2115                                                  'u.lastname', 'p.modified', 'd.forum');
2116         } else {
2117             list($messagesearch, $msparams) = search_generate_SQL($parsearray, 'p.message', 'p.subject',
2118                                                  'p.userid', 'u.id', 'u.firstname',
2119                                                  'u.lastname', 'p.modified', 'd.forum');
2120         }
2121         $params = array_merge($params, $msparams);
2122     }
2124     $fromsql = "{forum_posts} p,
2125                   {forum_discussions} d,
2126                   {user} u";
2128     $selectsql = " $messagesearch
2129                AND p.discussion = d.id
2130                AND p.userid = u.id
2131                AND $selectdiscussion
2132                    $extrasql";
2134     $countsql = "SELECT COUNT(*)
2135                    FROM $fromsql
2136                   WHERE $selectsql";
2138     $allnames = get_all_user_name_fields(true, 'u');
2139     $searchsql = "SELECT p.*,
2140                          d.forum,
2141                          $allnames,
2142                          u.email,
2143                          u.picture,
2144                          u.imagealt
2145                     FROM $fromsql
2146                    WHERE $selectsql
2147                 ORDER BY p.modified DESC";
2149     $totalcount = $DB->count_records_sql($countsql, $params);
2151     return $DB->get_records_sql($searchsql, $params, $limitfrom, $limitnum);
2154 /**
2155  * Returns a list of ratings for a particular post - sorted.
2156  *
2157  * TODO: Check if this function is actually used anywhere.
2158  * Up until the fix for MDL-27471 this function wasn't even returning.
2159  *
2160  * @param stdClass $context
2161  * @param int $postid
2162  * @param string $sort
2163  * @return array Array of ratings or false
2164  */
2165 function forum_get_ratings($context, $postid, $sort = "u.firstname ASC") {
2166     $options = new stdClass;
2167     $options->context = $context;
2168     $options->component = 'mod_forum';
2169     $options->ratingarea = 'post';
2170     $options->itemid = $postid;
2171     $options->sort = "ORDER BY $sort";
2173     $rm = new rating_manager();
2174     return $rm->get_all_ratings_for_item($options);
2177 /**
2178  * Returns a list of all new posts that have not been mailed yet
2179  *
2180  * @param int $starttime posts created after this time
2181  * @param int $endtime posts created before this
2182  * @param int $now used for timed discussions only
2183  * @return array
2184  */
2185 function forum_get_unmailed_posts($starttime, $endtime, $now=null) {
2186     global $CFG, $DB;
2188     $params = array();
2189     $params['mailed'] = FORUM_MAILED_PENDING;
2190     $params['ptimestart'] = $starttime;
2191     $params['ptimeend'] = $endtime;
2192     $params['mailnow'] = 1;
2194     if (!empty($CFG->forum_enabletimedposts)) {
2195         if (empty($now)) {
2196             $now = time();
2197         }
2198         $timedsql = "AND (d.timestart < :dtimestart AND (d.timeend = 0 OR d.timeend > :dtimeend))";
2199         $params['dtimestart'] = $now;
2200         $params['dtimeend'] = $now;
2201     } else {
2202         $timedsql = "";
2203     }
2205     return $DB->get_records_sql("SELECT p.*, d.course, d.forum
2206                                  FROM {forum_posts} p
2207                                  JOIN {forum_discussions} d ON d.id = p.discussion
2208                                  WHERE p.mailed = :mailed
2209                                  AND p.created >= :ptimestart
2210                                  AND (p.created < :ptimeend OR p.mailnow = :mailnow)
2211                                  $timedsql
2212                                  ORDER BY p.modified ASC", $params);
2215 /**
2216  * Marks posts before a certain time as being mailed already
2217  *
2218  * @global object
2219  * @global object
2220  * @param int $endtime
2221  * @param int $now Defaults to time()
2222  * @return bool
2223  */
2224 function forum_mark_old_posts_as_mailed($endtime, $now=null) {
2225     global $CFG, $DB;
2227     if (empty($now)) {
2228         $now = time();
2229     }
2231     $params = array();
2232     $params['mailedsuccess'] = FORUM_MAILED_SUCCESS;
2233     $params['now'] = $now;
2234     $params['endtime'] = $endtime;
2235     $params['mailnow'] = 1;
2236     $params['mailedpending'] = FORUM_MAILED_PENDING;
2238     if (empty($CFG->forum_enabletimedposts)) {
2239         return $DB->execute("UPDATE {forum_posts}
2240                              SET mailed = :mailedsuccess
2241                              WHERE (created < :endtime OR mailnow = :mailnow)
2242                              AND mailed = :mailedpending", $params);
2243     } else {
2244         return $DB->execute("UPDATE {forum_posts}
2245                              SET mailed = :mailedsuccess
2246                              WHERE discussion NOT IN (SELECT d.id
2247                                                       FROM {forum_discussions} d
2248                                                       WHERE d.timestart > :now)
2249                              AND (created < :endtime OR mailnow = :mailnow)
2250                              AND mailed = :mailedpending", $params);
2251     }
2254 /**
2255  * Get all the posts for a user in a forum suitable for forum_print_post
2256  *
2257  * @global object
2258  * @global object
2259  * @uses CONTEXT_MODULE
2260  * @return array
2261  */
2262 function forum_get_user_posts($forumid, $userid) {
2263     global $CFG, $DB;
2265     $timedsql = "";
2266     $params = array($forumid, $userid);
2268     if (!empty($CFG->forum_enabletimedposts)) {
2269         $cm = get_coursemodule_from_instance('forum', $forumid);
2270         if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
2271             $now = time();
2272             $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2273             $params[] = $now;
2274             $params[] = $now;
2275         }
2276     }
2278     $allnames = get_all_user_name_fields(true, 'u');
2279     return $DB->get_records_sql("SELECT p.*, d.forum, $allnames, u.email, u.picture, u.imagealt
2280                               FROM {forum} f
2281                                    JOIN {forum_discussions} d ON d.forum = f.id
2282                                    JOIN {forum_posts} p       ON p.discussion = d.id
2283                                    JOIN {user} u              ON u.id = p.userid
2284                              WHERE f.id = ?
2285                                    AND p.userid = ?
2286                                    $timedsql
2287                           ORDER BY p.modified ASC", $params);
2290 /**
2291  * Get all the discussions user participated in
2292  *
2293  * @global object
2294  * @global object
2295  * @uses CONTEXT_MODULE
2296  * @param int $forumid
2297  * @param int $userid
2298  * @return array Array or false
2299  */
2300 function forum_get_user_involved_discussions($forumid, $userid) {
2301     global $CFG, $DB;
2303     $timedsql = "";
2304     $params = array($forumid, $userid);
2305     if (!empty($CFG->forum_enabletimedposts)) {
2306         $cm = get_coursemodule_from_instance('forum', $forumid);
2307         if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
2308             $now = time();
2309             $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2310             $params[] = $now;
2311             $params[] = $now;
2312         }
2313     }
2315     return $DB->get_records_sql("SELECT DISTINCT d.*
2316                               FROM {forum} f
2317                                    JOIN {forum_discussions} d ON d.forum = f.id
2318                                    JOIN {forum_posts} p       ON p.discussion = d.id
2319                              WHERE f.id = ?
2320                                    AND p.userid = ?
2321                                    $timedsql", $params);
2324 /**
2325  * Get all the posts for a user in a forum suitable for forum_print_post
2326  *
2327  * @global object
2328  * @global object
2329  * @param int $forumid
2330  * @param int $userid
2331  * @return array of counts or false
2332  */
2333 function forum_count_user_posts($forumid, $userid) {
2334     global $CFG, $DB;
2336     $timedsql = "";
2337     $params = array($forumid, $userid);
2338     if (!empty($CFG->forum_enabletimedposts)) {
2339         $cm = get_coursemodule_from_instance('forum', $forumid);
2340         if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
2341             $now = time();
2342             $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2343             $params[] = $now;
2344             $params[] = $now;
2345         }
2346     }
2348     return $DB->get_record_sql("SELECT COUNT(p.id) AS postcount, MAX(p.modified) AS lastpost
2349                              FROM {forum} f
2350                                   JOIN {forum_discussions} d ON d.forum = f.id
2351                                   JOIN {forum_posts} p       ON p.discussion = d.id
2352                                   JOIN {user} u              ON u.id = p.userid
2353                             WHERE f.id = ?
2354                                   AND p.userid = ?
2355                                   $timedsql", $params);
2358 /**
2359  * Given a log entry, return the forum post details for it.
2360  *
2361  * @global object
2362  * @global object
2363  * @param object $log
2364  * @return array|null
2365  */
2366 function forum_get_post_from_log($log) {
2367     global $CFG, $DB;
2369     $allnames = get_all_user_name_fields(true, 'u');
2370     if ($log->action == "add post") {
2372         return $DB->get_record_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid, $allnames, u.email, u.picture
2373                                  FROM {forum_discussions} d,
2374                                       {forum_posts} p,
2375                                       {forum} f,
2376                                       {user} u
2377                                 WHERE p.id = ?
2378                                   AND d.id = p.discussion
2379                                   AND p.userid = u.id
2380                                   AND u.deleted <> '1'
2381                                   AND f.id = d.forum", array($log->info));
2384     } else if ($log->action == "add discussion") {
2386         return $DB->get_record_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid, $allnames, u.email, u.picture
2387                                  FROM {forum_discussions} d,
2388                                       {forum_posts} p,
2389                                       {forum} f,
2390                                       {user} u
2391                                 WHERE d.id = ?
2392                                   AND d.firstpost = p.id
2393                                   AND p.userid = u.id
2394                                   AND u.deleted <> '1'
2395                                   AND f.id = d.forum", array($log->info));
2396     }
2397     return NULL;
2400 /**
2401  * Given a discussion id, return the first post from the discussion
2402  *
2403  * @global object
2404  * @global object
2405  * @param int $dicsussionid
2406  * @return array
2407  */
2408 function forum_get_firstpost_from_discussion($discussionid) {
2409     global $CFG, $DB;
2411     return $DB->get_record_sql("SELECT p.*
2412                              FROM {forum_discussions} d,
2413                                   {forum_posts} p
2414                             WHERE d.id = ?
2415                               AND d.firstpost = p.id ", array($discussionid));
2418 /**
2419  * Returns an array of counts of replies to each discussion
2420  *
2421  * @global object
2422  * @global object
2423  * @param int $forumid
2424  * @param string $forumsort
2425  * @param int $limit
2426  * @param int $page
2427  * @param int $perpage
2428  * @return array
2429  */
2430 function forum_count_discussion_replies($forumid, $forumsort="", $limit=-1, $page=-1, $perpage=0) {
2431     global $CFG, $DB;
2433     if ($limit > 0) {
2434         $limitfrom = 0;
2435         $limitnum  = $limit;
2436     } else if ($page != -1) {
2437         $limitfrom = $page*$perpage;
2438         $limitnum  = $perpage;
2439     } else {
2440         $limitfrom = 0;
2441         $limitnum  = 0;
2442     }
2444     if ($forumsort == "") {
2445         $orderby = "";
2446         $groupby = "";
2448     } else {
2449         $orderby = "ORDER BY $forumsort";
2450         $groupby = ", ".strtolower($forumsort);
2451         $groupby = str_replace('desc', '', $groupby);
2452         $groupby = str_replace('asc', '', $groupby);
2453     }
2455     if (($limitfrom == 0 and $limitnum == 0) or $forumsort == "") {
2456         $sql = "SELECT p.discussion, COUNT(p.id) AS replies, MAX(p.id) AS lastpostid
2457                   FROM {forum_posts} p
2458                        JOIN {forum_discussions} d ON p.discussion = d.id
2459                  WHERE p.parent > 0 AND d.forum = ?
2460               GROUP BY p.discussion";
2461         return $DB->get_records_sql($sql, array($forumid));
2463     } else {
2464         $sql = "SELECT p.discussion, (COUNT(p.id) - 1) AS replies, MAX(p.id) AS lastpostid
2465                   FROM {forum_posts} p
2466                        JOIN {forum_discussions} d ON p.discussion = d.id
2467                  WHERE d.forum = ?
2468               GROUP BY p.discussion $groupby
2469               $orderby";
2470         return $DB->get_records_sql("SELECT * FROM ($sql) sq", array($forumid), $limitfrom, $limitnum);
2471     }
2474 /**
2475  * @global object
2476  * @global object
2477  * @global object
2478  * @staticvar array $cache
2479  * @param object $forum
2480  * @param object $cm
2481  * @param object $course
2482  * @return mixed
2483  */
2484 function forum_count_discussions($forum, $cm, $course) {
2485     global $CFG, $DB, $USER;
2487     static $cache = array();
2489     $now = round(time(), -2); // db cache friendliness
2491     $params = array($course->id);
2493     if (!isset($cache[$course->id])) {
2494         if (!empty($CFG->forum_enabletimedposts)) {
2495             $timedsql = "AND d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?)";
2496             $params[] = $now;
2497             $params[] = $now;
2498         } else {
2499             $timedsql = "";
2500         }
2502         $sql = "SELECT f.id, COUNT(d.id) as dcount
2503                   FROM {forum} f
2504                        JOIN {forum_discussions} d ON d.forum = f.id
2505                  WHERE f.course = ?
2506                        $timedsql
2507               GROUP BY f.id";
2509         if ($counts = $DB->get_records_sql($sql, $params)) {
2510             foreach ($counts as $count) {
2511                 $counts[$count->id] = $count->dcount;
2512             }
2513             $cache[$course->id] = $counts;
2514         } else {
2515             $cache[$course->id] = array();
2516         }
2517     }
2519     if (empty($cache[$course->id][$forum->id])) {
2520         return 0;
2521     }
2523     $groupmode = groups_get_activity_groupmode($cm, $course);
2525     if ($groupmode != SEPARATEGROUPS) {
2526         return $cache[$course->id][$forum->id];
2527     }
2529     if (has_capability('moodle/site:accessallgroups', context_module::instance($cm->id))) {
2530         return $cache[$course->id][$forum->id];
2531     }
2533     require_once($CFG->dirroot.'/course/lib.php');
2535     $modinfo = get_fast_modinfo($course);
2537     $mygroups = $modinfo->get_groups($cm->groupingid);
2539     // add all groups posts
2540     $mygroups[-1] = -1;
2542     list($mygroups_sql, $params) = $DB->get_in_or_equal($mygroups);
2543     $params[] = $forum->id;
2545     if (!empty($CFG->forum_enabletimedposts)) {
2546         $timedsql = "AND d.timestart < $now AND (d.timeend = 0 OR d.timeend > $now)";
2547         $params[] = $now;
2548         $params[] = $now;
2549     } else {
2550         $timedsql = "";
2551     }
2553     $sql = "SELECT COUNT(d.id)
2554               FROM {forum_discussions} d
2555              WHERE d.groupid $mygroups_sql AND d.forum = ?
2556                    $timedsql";
2558     return $DB->get_field_sql($sql, $params);
2561 /**
2562  * How many posts by other users are unrated by a given user in the given discussion?
2563  *
2564  * TODO: Is this function still used anywhere?
2565  *
2566  * @param int $discussionid
2567  * @param int $userid
2568  * @return mixed
2569  */
2570 function forum_count_unrated_posts($discussionid, $userid) {
2571     global $CFG, $DB;
2573     $sql = "SELECT COUNT(*) as num
2574               FROM {forum_posts}
2575              WHERE parent > 0
2576                AND discussion = :discussionid
2577                AND userid <> :userid";
2578     $params = array('discussionid' => $discussionid, 'userid' => $userid);
2579     $posts = $DB->get_record_sql($sql, $params);
2580     if ($posts) {
2581         $sql = "SELECT count(*) as num
2582                   FROM {forum_posts} p,
2583                        {rating} r
2584                  WHERE p.discussion = :discussionid AND
2585                        p.id = r.itemid AND
2586                        r.userid = userid AND
2587                        r.component = 'mod_forum' AND
2588                        r.ratingarea = 'post'";
2589         $rated = $DB->get_record_sql($sql, $params);
2590         if ($rated) {
2591             if ($posts->num > $rated->num) {
2592                 return $posts->num - $rated->num;
2593             } else {
2594                 return 0;    // Just in case there was a counting error
2595             }
2596         } else {
2597             return $posts->num;
2598         }
2599     } else {
2600         return 0;
2601     }
2604 /**
2605  * Get all discussions in a forum
2606  *
2607  * @global object
2608  * @global object
2609  * @global object
2610  * @uses CONTEXT_MODULE
2611  * @uses VISIBLEGROUPS
2612  * @param object $cm
2613  * @param string $forumsort
2614  * @param bool $fullpost
2615  * @param int $unused
2616  * @param int $limit
2617  * @param bool $userlastmodified
2618  * @param int $page
2619  * @param int $perpage
2620  * @return array
2621  */
2622 function forum_get_discussions($cm, $forumsort="d.timemodified DESC", $fullpost=true, $unused=-1, $limit=-1, $userlastmodified=false, $page=-1, $perpage=0) {
2623     global $CFG, $DB, $USER;
2625     $timelimit = '';
2627     $now = round(time(), -2);
2628     $params = array($cm->instance);
2630     $modcontext = context_module::instance($cm->id);
2632     if (!has_capability('mod/forum:viewdiscussion', $modcontext)) { /// User must have perms to view discussions
2633         return array();
2634     }
2636     if (!empty($CFG->forum_enabletimedposts)) { /// Users must fulfill timed posts
2638         if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
2639             $timelimit = " AND ((d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?))";
2640             $params[] = $now;
2641             $params[] = $now;
2642             if (isloggedin()) {
2643                 $timelimit .= " OR d.userid = ?";
2644                 $params[] = $USER->id;
2645             }
2646             $timelimit .= ")";
2647         }
2648     }
2650     if ($limit > 0) {
2651         $limitfrom = 0;
2652         $limitnum  = $limit;
2653     } else if ($page != -1) {
2654         $limitfrom = $page*$perpage;
2655         $limitnum  = $perpage;
2656     } else {
2657         $limitfrom = 0;
2658         $limitnum  = 0;
2659     }
2661     $groupmode    = groups_get_activity_groupmode($cm);
2662     $currentgroup = groups_get_activity_group($cm);
2664     if ($groupmode) {
2665         if (empty($modcontext)) {
2666             $modcontext = context_module::instance($cm->id);
2667         }
2669         if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
2670             if ($currentgroup) {
2671                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2672                 $params[] = $currentgroup;
2673             } else {
2674                 $groupselect = "";
2675             }
2677         } else {
2678             //seprate groups without access all
2679             if ($currentgroup) {
2680                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2681                 $params[] = $currentgroup;
2682             } else {
2683                 $groupselect = "AND d.groupid = -1";
2684             }
2685         }
2686     } else {
2687         $groupselect = "";
2688     }
2691     if (empty($forumsort)) {
2692         $forumsort = "d.timemodified DESC";
2693     }
2694     if (empty($fullpost)) {
2695         $postdata = "p.id,p.subject,p.modified,p.discussion,p.userid";
2696     } else {
2697         $postdata = "p.*";
2698     }
2700     if (empty($userlastmodified)) {  // We don't need to know this
2701         $umfields = "";
2702         $umtable  = "";
2703     } else {
2704         $umfields = ', ' . get_all_user_name_fields(true, 'um', null, 'um');
2705         $umtable  = " LEFT JOIN {user} um ON (d.usermodified = um.id)";
2706     }
2708     $allnames = get_all_user_name_fields(true, 'u');
2709     $sql = "SELECT $postdata, d.name, d.timemodified, d.usermodified, d.groupid, d.timestart, d.timeend, $allnames,
2710                    u.email, u.picture, u.imagealt $umfields
2711               FROM {forum_discussions} d
2712                    JOIN {forum_posts} p ON p.discussion = d.id
2713                    JOIN {user} u ON p.userid = u.id
2714                    $umtable
2715              WHERE d.forum = ? AND p.parent = 0
2716                    $timelimit $groupselect
2717           ORDER BY $forumsort";
2718     return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
2721 /**
2722  *
2723  * @global object
2724  * @global object
2725  * @global object
2726  * @uses CONTEXT_MODULE
2727  * @uses VISIBLEGROUPS
2728  * @param object $cm
2729  * @return array
2730  */
2731 function forum_get_discussions_unread($cm) {
2732     global $CFG, $DB, $USER;
2734     $now = round(time(), -2);
2735     $cutoffdate = $now - ($CFG->forum_oldpostdays*24*60*60);
2737     $params = array();
2738     $groupmode    = groups_get_activity_groupmode($cm);
2739     $currentgroup = groups_get_activity_group($cm);
2741     if ($groupmode) {
2742         $modcontext = context_module::instance($cm->id);
2744         if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
2745             if ($currentgroup) {
2746                 $groupselect = "AND (d.groupid = :currentgroup OR d.groupid = -1)";
2747                 $params['currentgroup'] = $currentgroup;
2748             } else {
2749                 $groupselect = "";
2750             }
2752         } else {
2753             //separate groups without access all
2754             if ($currentgroup) {
2755                 $groupselect = "AND (d.groupid = :currentgroup OR d.groupid = -1)";
2756                 $params['currentgroup'] = $currentgroup;
2757             } else {
2758                 $groupselect = "AND d.groupid = -1";
2759             }
2760         }
2761     } else {
2762         $groupselect = "";
2763     }
2765     if (!empty($CFG->forum_enabletimedposts)) {
2766         $timedsql = "AND d.timestart < :now1 AND (d.timeend = 0 OR d.timeend > :now2)";
2767         $params['now1'] = $now;
2768         $params['now2'] = $now;
2769     } else {
2770         $timedsql = "";
2771     }
2773     $sql = "SELECT d.id, COUNT(p.id) AS unread
2774               FROM {forum_discussions} d
2775                    JOIN {forum_posts} p     ON p.discussion = d.id
2776                    LEFT JOIN {forum_read} r ON (r.postid = p.id AND r.userid = $USER->id)
2777              WHERE d.forum = {$cm->instance}
2778                    AND p.modified >= :cutoffdate AND r.id is NULL
2779                    $groupselect
2780                    $timedsql
2781           GROUP BY d.id";
2782     $params['cutoffdate'] = $cutoffdate;
2784     if ($unreads = $DB->get_records_sql($sql, $params)) {
2785         foreach ($unreads as $unread) {
2786             $unreads[$unread->id] = $unread->unread;
2787         }
2788         return $unreads;
2789     } else {
2790         return array();
2791     }
2794 /**
2795  * @global object
2796  * @global object
2797  * @global object
2798  * @uses CONEXT_MODULE
2799  * @uses VISIBLEGROUPS
2800  * @param object $cm
2801  * @return array
2802  */
2803 function forum_get_discussions_count($cm) {
2804     global $CFG, $DB, $USER;
2806     $now = round(time(), -2);
2807     $params = array($cm->instance);
2808     $groupmode    = groups_get_activity_groupmode($cm);
2809     $currentgroup = groups_get_activity_group($cm);
2811     if ($groupmode) {
2812         $modcontext = context_module::instance($cm->id);
2814         if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
2815             if ($currentgroup) {
2816                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2817                 $params[] = $currentgroup;
2818             } else {
2819                 $groupselect = "";
2820             }
2822         } else {
2823             //seprate groups without access all
2824             if ($currentgroup) {
2825                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2826                 $params[] = $currentgroup;
2827             } else {
2828                 $groupselect = "AND d.groupid = -1";
2829             }
2830         }
2831     } else {
2832         $groupselect = "";
2833     }
2835     $cutoffdate = $now - ($CFG->forum_oldpostdays*24*60*60);
2837     $timelimit = "";
2839     if (!empty($CFG->forum_enabletimedposts)) {
2841         $modcontext = context_module::instance($cm->id);
2843         if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
2844             $timelimit = " AND ((d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?))";
2845             $params[] = $now;
2846             $params[] = $now;
2847             if (isloggedin()) {
2848                 $timelimit .= " OR d.userid = ?";
2849                 $params[] = $USER->id;
2850             }
2851             $timelimit .= ")";
2852         }
2853     }
2855     $sql = "SELECT COUNT(d.id)
2856               FROM {forum_discussions} d
2857                    JOIN {forum_posts} p ON p.discussion = d.id
2858              WHERE d.forum = ? AND p.parent = 0
2859                    $groupselect $timelimit";
2861     return $DB->get_field_sql($sql, $params);
2865 /**
2866  * Get all discussions started by a particular user in a course (or group)
2867  * This function no longer used ...
2868  *
2869  * @todo Remove this function if no longer used
2870  * @global object
2871  * @global object
2872  * @param int $courseid
2873  * @param int $userid
2874  * @param int $groupid
2875  * @return array
2876  */
2877 function forum_get_user_discussions($courseid, $userid, $groupid=0) {
2878     global $CFG, $DB;
2879     $params = array($courseid, $userid);
2880     if ($groupid) {
2881         $groupselect = " AND d.groupid = ? ";
2882         $params[] = $groupid;
2883     } else  {
2884         $groupselect = "";
2885     }
2887     $allnames = get_all_user_name_fields(true, 'u');
2888     return $DB->get_records_sql("SELECT p.*, d.groupid, $allnames, u.email, u.picture, u.imagealt,
2889                                    f.type as forumtype, f.name as forumname, f.id as forumid
2890                               FROM {forum_discussions} d,
2891                                    {forum_posts} p,
2892                                    {user} u,
2893                                    {forum} f
2894                              WHERE d.course = ?
2895                                AND p.discussion = d.id
2896                                AND p.parent = 0
2897                                AND p.userid = u.id
2898                                AND u.id = ?
2899                                AND d.forum = f.id $groupselect
2900                           ORDER BY p.created DESC", $params);
2903 /**
2904  * Get the list of potential subscribers to a forum.
2905  *
2906  * @param object $forumcontext the forum context.
2907  * @param integer $groupid the id of a group, or 0 for all groups.
2908  * @param string $fields the list of fields to return for each user. As for get_users_by_capability.
2909  * @param string $sort sort order. As for get_users_by_capability.
2910  * @return array list of users.
2911  */
2912 function forum_get_potential_subscribers($forumcontext, $groupid, $fields, $sort = '') {
2913     global $DB;
2915     // only active enrolled users or everybody on the frontpage
2916     list($esql, $params) = get_enrolled_sql($forumcontext, 'mod/forum:allowforcesubscribe', $groupid, true);
2917     if (!$sort) {
2918         list($sort, $sortparams) = users_order_by_sql('u');
2919         $params = array_merge($params, $sortparams);
2920     }
2922     $sql = "SELECT $fields
2923               FROM {user} u
2924               JOIN ($esql) je ON je.id = u.id
2925           ORDER BY $sort";
2927     return $DB->get_records_sql($sql, $params);
2930 /**
2931  * Returns list of user objects that are subscribed to this forum
2932  *
2933  * @global object
2934  * @global object
2935  * @param object $course the course
2936  * @param forum $forum the forum
2937  * @param integer $groupid group id, or 0 for all.
2938  * @param object $context the forum context, to save re-fetching it where possible.
2939  * @param string $fields requested user fields (with "u." table prefix)
2940  * @return array list of users.
2941  */
2942 function forum_subscribed_users($course, $forum, $groupid=0, $context = null, $fields = null) {
2943     global $CFG, $DB;
2945     $allnames = get_all_user_name_fields(true, 'u');
2946     if (empty($fields)) {
2947         $fields ="u.id,
2948                   u.username,
2949                   $allnames,
2950                   u.maildisplay,
2951                   u.mailformat,
2952                   u.maildigest,
2953                   u.imagealt,
2954                   u.email,
2955                   u.emailstop,
2956                   u.city,
2957                   u.country,
2958                   u.lastaccess,
2959                   u.lastlogin,
2960                   u.picture,
2961                   u.timezone,
2962                   u.theme,
2963                   u.lang,
2964                   u.trackforums,
2965                   u.mnethostid";
2966     }
2968     if (empty($context)) {
2969         $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id);
2970         $context = context_module::instance($cm->id);
2971     }
2973     if (forum_is_forcesubscribed($forum)) {
2974         $results = forum_get_potential_subscribers($context, $groupid, $fields, "u.email ASC");
2976     } else {
2977         // only active enrolled users or everybody on the frontpage
2978         list($esql, $params) = get_enrolled_sql($context, '', $groupid, true);
2979         $params['forumid'] = $forum->id;
2980         $results = $DB->get_records_sql("SELECT $fields
2981                                            FROM {user} u
2982                                            JOIN ($esql) je ON je.id = u.id
2983                                            JOIN {forum_subscriptions} s ON s.userid = u.id
2984                                           WHERE s.forum = :forumid
2985                                        ORDER BY u.email ASC", $params);
2986     }
2988     // Guest user should never be subscribed to a forum.
2989     unset($results[$CFG->siteguest]);
2991     return $results;
2996 // OTHER FUNCTIONS ///////////////////////////////////////////////////////////
2999 /**
3000  * @global object
3001  * @global object
3002  * @param int $courseid
3003  * @param string $type
3004  */
3005 function forum_get_course_forum($courseid, $type) {
3006 // How to set up special 1-per-course forums
3007     global $CFG, $DB, $OUTPUT, $USER;
3009     if ($forums = $DB->get_records_select("forum", "course = ? AND type = ?", array($courseid, $type), "id ASC")) {
3010         // There should always only be ONE, but with the right combination of
3011         // errors there might be more.  In this case, just return the oldest one (lowest ID).
3012         foreach ($forums as $forum) {
3013             return $forum;   // ie the first one
3014         }
3015     }
3017     // Doesn't exist, so create one now.
3018     $forum = new stdClass();
3019     $forum->course = $courseid;
3020     $forum->type = "$type";
3021     if (!empty($USER->htmleditor)) {
3022         $forum->introformat = $USER->htmleditor;
3023     }
3024     switch ($forum->type) {
3025         case "news":
3026             $forum->name  = get_string("namenews", "forum");
3027             $forum->intro = get_string("intronews", "forum");
3028             $forum->forcesubscribe = FORUM_FORCESUBSCRIBE;
3029             $forum->assessed = 0;
3030             if ($courseid == SITEID) {
3031                 $forum->name  = get_string("sitenews");
3032                 $forum->forcesubscribe = 0;
3033             }
3034             break;
3035         case "social":
3036             $forum->name  = get_string("namesocial", "forum");
3037             $forum->intro = get_string("introsocial", "forum");
3038             $forum->assessed = 0;
3039             $forum->forcesubscribe = 0;
3040             break;
3041         case "blog":
3042             $forum->name = get_string('blogforum', 'forum');
3043             $forum->intro = get_string('introblog', 'forum');
3044             $forum->assessed = 0;
3045             $forum->forcesubscribe = 0;
3046             break;
3047         default:
3048             echo $OUTPUT->notification("That forum type doesn't exist!");
3049             return false;
3050             break;
3051     }
3053     $forum->timemodified = time();
3054     $forum->id = $DB->insert_record("forum", $forum);
3056     if (! $module = $DB->get_record("modules", array("name" => "forum"))) {
3057         echo $OUTPUT->notification("Could not find forum module!!");
3058         return false;
3059     }
3060     $mod = new stdClass();
3061     $mod->course = $courseid;
3062     $mod->module = $module->id;
3063     $mod->instance = $forum->id;
3064     $mod->section = 0;
3065     include_once("$CFG->dirroot/course/lib.php");
3066     if (! $mod->coursemodule = add_course_module($mod) ) {
3067         echo $OUTPUT->notification("Could not add a new course module to the course '" . $courseid . "'");
3068         return false;
3069     }
3070     $sectionid = course_add_cm_to_section($courseid, $mod->coursemodule, 0);
3071     return $DB->get_record("forum", array("id" => "$forum->id"));
3075 /**
3076  * Given the data about a posting, builds up the HTML to display it and
3077  * returns the HTML in a string.  This is designed for sending via HTML email.
3078  *
3079  * @global object
3080  * @param object $course
3081  * @param object $cm
3082  * @param object $forum
3083  * @param object $discussion
3084  * @param object $post
3085  * @param object $userform
3086  * @param object $userto
3087  * @param bool $ownpost
3088  * @param bool $reply
3089  * @param bool $link
3090  * @param bool $rate
3091  * @param string $footer
3092  * @return string
3093  */
3094 function forum_make_mail_post($course, $cm, $forum, $discussion, $post, $userfrom, $userto,
3095                               $ownpost=false, $reply=false, $link=false, $rate=false, $footer="") {
3097     global $CFG, $OUTPUT;
3099     $modcontext = context_module::instance($cm->id);
3101     if (!isset($userto->viewfullnames[$forum->id])) {
3102         $viewfullnames = has_capability('moodle/site:viewfullnames', $modcontext, $userto->id);
3103     } else {
3104         $viewfullnames = $userto->viewfullnames[$forum->id];
3105     }
3107     // add absolute file links
3108     $post->message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php', $modcontext->id, 'mod_forum', 'post', $post->id);
3110     // format the post body
3111     $options = new stdClass();
3112     $options->para = true;
3113     $formattedtext = format_text($post->message, $post->messageformat, $options, $course->id);
3115     $output = '<table border="0" cellpadding="3" cellspacing="0" class="forumpost">';
3117     $output .= '<tr class="header"><td width="35" valign="top" class="picture left">';
3118     $output .= $OUTPUT->user_picture($userfrom, array('courseid'=>$course->id));
3119     $output .= '</td>';
3121     if ($post->parent) {
3122         $output .= '<td class="topic">';
3123     } else {
3124         $output .= '<td class="topic starter">';
3125     }
3126     $output .= '<div class="subject">'.format_string($post->subject).'</div>';
3128     $fullname = fullname($userfrom, $viewfullnames);
3129     $by = new stdClass();
3130     $by->name = '<a href="'.$CFG->wwwroot.'/user/view.php?id='.$userfrom->id.'&amp;course='.$course->id.'">'.$fullname.'</a>';
3131     $by->date = userdate($post->modified, '', $userto->timezone);
3132     $output .= '<div class="author">'.get_string('bynameondate', 'forum', $by).'</div>';
3134     $output .= '</td></tr>';
3136     $output .= '<tr><td class="left side" valign="top">';
3138     if (isset($userfrom->groups)) {
3139         $groups = $userfrom->groups[$forum->id];
3140     } else {
3141         $groups = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid);
3142     }
3144     if ($groups) {
3145         $output .= print_group_picture($groups, $course->id, false, true, true);
3146     } else {
3147         $output .= '&nbsp;';
3148     }
3150     $output .= '</td><td class="content">';
3152     $attachments = forum_print_attachments($post, $cm, 'html');
3153     if ($attachments !== '') {
3154         $output .= '<div class="attachments">';
3155         $output .= $attachments;
3156         $output .= '</div>';
3157     }
3159     $output .= $formattedtext;
3161 // Commands
3162     $commands = array();
3164     if ($post->parent) {
3165         $commands[] = '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.
3166                       $post->discussion.'&amp;parent='.$post->parent.'">'.get_string('parent', 'forum').'</a>';
3167     }
3169     if ($reply) {
3170         $commands[] = '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/post.php?reply='.$post->id.'">'.
3171                       get_string('reply', 'forum').'</a>';
3172     }
3174     $output .= '<div class="commands">';
3175     $output .= implode(' | ', $commands);
3176     $output .= '</div>';
3178 // Context link to post if required
3179     if ($link) {
3180         $output .= '<div class="link">';
3181         $output .= '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'#p'.$post->id.'">'.
3182                      get_string('postincontext', 'forum').'</a>';
3183         $output .= '</div>';
3184     }
3186     if ($footer) {
3187         $output .= '<div class="footer">'.$footer.'</div>';
3188     }
3189     $output .= '</td></tr></table>'."\n\n";
3191     return $output;
3194 /**
3195  * Print a forum post
3196  *
3197  * @global object
3198  * @global object
3199  * @uses FORUM_MODE_THREADED
3200  * @uses PORTFOLIO_FORMAT_PLAINHTML
3201  * @uses PORTFOLIO_FORMAT_FILE
3202  * @uses PORTFOLIO_FORMAT_RICHHTML
3203  * @uses PORTFOLIO_ADD_TEXT_LINK
3204  * @uses CONTEXT_MODULE
3205  * @param object $post The post to print.
3206  * @param object $discussion
3207  * @param object $forum
3208  * @param object $cm
3209  * @param object $course
3210  * @param boolean $ownpost Whether this post belongs to the current user.
3211  * @param boolean $reply Whether to print a 'reply' link at the bottom of the message.
3212  * @param boolean $link Just print a shortened version of the post as a link to the full post.
3213  * @param string $footer Extra stuff to print after the message.
3214  * @param string $highlight Space-separated list of terms to highlight.
3215  * @param int $post_read true, false or -99. If we already know whether this user
3216  *          has read this post, pass that in, otherwise, pass in -99, and this
3217  *          function will work it out.
3218  * @param boolean $dummyifcantsee When forum_user_can_see_post says that
3219  *          the current user can't see this post, if this argument is true
3220  *          (the default) then print a dummy 'you can't see this post' post.
3221  *          If false, don't output anything at all.
3222  * @param bool|null $istracked
3223  * @return void
3224  */
3225 function forum_print_post($post, $discussion, $forum, &$cm, $course, $ownpost=false, $reply=false, $link=false,
3226                           $footer="", $highlight="", $postisread=null, $dummyifcantsee=true, $istracked=null, $return=false) {
3227     global $USER, $CFG, $OUTPUT;
3229     require_once($CFG->libdir . '/filelib.php');
3231     // String cache
3232     static $str;
3234     $modcontext = context_module::instance($cm->id);
3236     $post->course = $course->id;
3237     $post->forum  = $forum->id;
3238     $post->message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php', $modcontext->id, 'mod_forum', 'post', $post->id);
3239     if (!empty($CFG->enableplagiarism)) {
3240         require_once($CFG->libdir.'/plagiarismlib.php');
3241         $post->message .= plagiarism_get_links(array('userid' => $post->userid,
3242             'content' => $post->message,
3243             'cmid' => $cm->id,
3244             'course' => $post->course,
3245             'forum' => $post->forum));
3246     }
3248     // caching
3249     if (!isset($cm->cache)) {
3250         $cm->cache = new stdClass;
3251     }
3253     if (!isset($cm->cache->caps)) {
3254         $cm->cache->caps = array();
3255         $cm->cache->caps['mod/forum:viewdiscussion']   = has_capability('mod/forum:viewdiscussion', $modcontext);
3256         $cm->cache->caps['moodle/site:viewfullnames']  = has_capability('moodle/site:viewfullnames', $modcontext);
3257         $cm->cache->caps['mod/forum:editanypost']      = has_capability('mod/forum:editanypost', $modcontext);
3258         $cm->cache->caps['mod/forum:splitdiscussions'] = has_capability('mod/forum:splitdiscussions', $modcontext);
3259         $cm->cache->caps['mod/forum:deleteownpost']    = has_capability('mod/forum:deleteownpost', $modcontext);
3260         $cm->cache->caps['mod/forum:deleteanypost']    = has_capability('mod/forum:deleteanypost', $modcontext);
3261         $cm->cache->caps['mod/forum:viewanyrating']    = has_capability('mod/forum:viewanyrating', $modcontext);
3262         $cm->cache->caps['mod/forum:exportpost']       = has_capability('mod/forum:exportpost', $modcontext);
3263         $cm->cache->caps['mod/forum:exportownpost']    = has_capability('mod/forum:exportownpost', $modcontext);
3264     }
3266     if (!isset($cm->uservisible)) {
3267         $cm->uservisible = coursemodule_visible_for_user($cm);
3268     }
3270     if ($istracked && is_null($postisread)) {
3271         $postisread = forum_tp_is_post_read($USER->id, $post);
3272     }
3274     if (!forum_user_can_see_post($forum, $discussion, $post, NULL, $cm)) {
3275         $output = '';
3276         if (!$dummyifcantsee) {
3277             if ($return) {
3278                 return $output;
3279             }
3280             echo $output;
3281             return;
3282         }
3283         $output .= html_writer::tag('a', '', array('id'=>'p'.$post->id));
3284         $output .= html_writer::start_tag('div', array('class'=>'forumpost clearfix',
3285                                                        'role' => 'region',
3286                                                        'aria-label' => get_string('hiddenforumpost', 'forum')));
3287         $output .= html_writer::start_tag('div', array('class'=>'row header'));
3288         $output .= html_writer::tag('div', '', array('class'=>'left picture')); // Picture
3289         if ($post->parent) {
3290             $output .= html_writer::start_tag('div', array('class'=>'topic'));
3291         } else {
3292             $output .= html_writer::start_tag('div', array('class'=>'topic starter'));
3293         }
3294         $output .= html_writer::tag('div', get_string('forumsubjecthidden','forum'), array('class' => 'subject',
3295                                                                                            'role' => 'header')); // Subject.
3296         $output .= html_writer::tag('div', get_string('forumauthorhidden', 'forum'), array('class' => 'author',
3297                                                                                            'role' => 'header')); // Author.
3298         $output .= html_writer::end_tag('div');
3299         $output .= html_writer::end_tag('div'); // row
3300         $output .= html_writer::start_tag('div', array('class'=>'row'));
3301         $output .= html_writer::tag('div', '&nbsp;', array('class'=>'left side')); // Groups
3302         $output .= html_writer::tag('div', get_string('forumbodyhidden','forum'), array('class'=>'content')); // Content
3303         $output .= html_writer::end_tag('div'); // row
3304         $output .= html_writer::end_tag('div'); // forumpost
3306         if ($return) {
3307             return $output;
3308         }
3309         echo $output;
3310         return;
3311     }
3313     if (empty($str)) {
3314         $str = new stdClass;
3315         $str->edit         = get_string('edit', 'forum');
3316         $str->delete       = get_string('delete', 'forum');
3317         $str->reply        = get_string('reply', 'forum');
3318         $str->parent       = get_string('parent', 'forum');
3319         $str->pruneheading = get_string('pruneheading', 'forum');
3320         $str->prune        = get_string('prune', 'forum');
3321         $str->displaymode     = get_user_preferences('forum_displaymode', $CFG->forum_displaymode);
3322         $str->markread     = get_string('markread', 'forum');
3323         $str->markunread   = get_string('markunread', 'forum');
3324     }
3326     $discussionlink = new moodle_url('/mod/forum/discuss.php', array('d'=>$post->discussion));
3328     // Build an object that represents the posting user
3329     $postuser = new stdClass;
3330     $postuserfields = explode(',', user_picture::fields());
3331     $postuser = username_load_fields_from_object($postuser, $post, null, $postuserfields);
3332     $postuser->id = $post->userid;
3333     $postuser->fullname    = fullname($postuser, $cm->cache->caps['moodle/site:viewfullnames']);
3334     $postuser->profilelink = new moodle_url('/user/view.php', array('id'=>$post->userid, 'course'=>$course->id));
3336     // Prepare the groups the posting user belongs to
3337     if (isset($cm->cache->usersgroups)) {
3338         $groups = array();
3339         if (isset($cm->cache->usersgroups[$post->userid])) {
3340             foreach ($cm->cache->usersgroups[$post->userid] as $gid) {
3341                 $groups[$gid] = $cm->cache->groups[$gid];
3342             }
3343         }
3344     } else {
3345         $groups = groups_get_all_groups($course->id, $post->userid, $cm->groupingid);
3346     }
3348     // Prepare the attachements for the post, files then images
3349     list($attachments, $attachedimages) = forum_print_attachments($post, $cm, 'separateimages');
3351     // Determine if we need to shorten this post
3352     $shortenpost = ($link && (strlen(strip_tags($post->message)) > $CFG->forum_longpost));
3355     // Prepare an array of commands
3356     $commands = array();
3358     // SPECIAL CASE: The front page can display a news item post to non-logged in users.
3359     // Don't display the mark read / unread controls in this case.
3360     if ($istracked && $CFG->forum_usermarksread && isloggedin()) {
3361         $url = new moodle_url($discussionlink, array('postid'=>$post->id, 'mark'=>'unread'));
3362         $text = $str->markunread;
3363         if (!$postisread) {
3364             $url->param('mark', 'read');
3365             $text = $str->markread;
3366         }
3367         if ($str->displaymode == FORUM_MODE_THREADED) {
3368             $url->param('parent', $post->parent);
3369         } else {
3370             $url->set_anchor('p'.$post->id);
3371         }
3372         $commands[] = array('url'=>$url, 'text'=>$text);
3373     }
3375     // Zoom in to the parent specifically
3376     if ($post->parent) {
3377         $url = new moodle_url($discussionlink);
3378         if ($str->displaymode == FORUM_MODE_THREADED) {
3379             $url->param('parent', $post->parent);
3380         } else {
3381             $url->set_anchor('p'.$post->parent);
3382         }
3383         $commands[] = array('url'=>$url, 'text'=>$str->parent);
3384     }
3386     // Hack for allow to edit news posts those are not displayed yet until they are displayed
3387     $age = time() - $post->created;
3388     if (!$post->parent && $forum->type == 'news' && $discussion->timestart > time()) {
3389         $age = 0;
3390     }
3392     if ($forum->type == 'single' and $discussion->firstpost == $post->id) {
3393         if (has_capability('moodle/course:manageactivities', $modcontext)) {
3394             // The first post in single simple is the forum description.
3395             $commands[] = array('url'=>new moodle_url('/course/modedit.php', array('update'=>$cm->id, 'sesskey'=>sesskey(), 'return'=>1)), 'text'=>$str->edit);
3396         }
3397     } else if (($ownpost && $age < $CFG->maxeditingtime) || $cm->cache->caps['mod/forum:editanypost']) {
3398         $commands[] = array('url'=>new moodle_url('/mod/forum/post.php', array('edit'=>$post->id)), 'text'=>$str->edit);
3399     }
3401     if ($cm->cache->caps['mod/forum:splitdiscussions'] && $post->parent && $forum->type != 'single') {
3402         $commands[] = array('url'=>new moodle_url('/mod/forum/post.php', array('prune'=>$post->id)), 'text'=>$str->prune, 'title'=>$str->pruneheading);
3403     }
3405     if ($forum->type == 'single' and $discussion->firstpost == $post->id) {
3406         // Do not allow deleting of first post in single simple type.
3407     } else if (($ownpost && $age < $CFG->maxeditingtime && $cm->cache->caps['mod/forum:deleteownpost']) || $cm->cache->caps['mod/forum:deleteanypost']) {
3408         $commands[] = array('url'=>new moodle_url('/mod/forum/post.php', array('delete'=>$post->id)), 'text'=>$str->delete);
3409     }
3411     if ($reply) {
3412         $commands[] = array('url'=>new moodle_url('/mod/forum/post.php#mformforum', array('reply'=>$post->id)), 'text'=>$str->reply);
3413     }
3415     if ($CFG->enableportfolios && ($cm->cache->caps['mod/forum:exportpost'] || ($ownpost && $cm->cache->caps['mod/forum:exportownpost']))) {
3416         $p = array('postid' => $post->id);
3417         require_once($CFG->libdir.'/portfoliolib.php');
3418         $button = new portfolio_add_button();
3419         $button->set_callback_options('forum_portfolio_caller', array('postid' => $post->id), 'mod_forum');
3420         if (empty($attachments)) {
3421             $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML);
3422         } else {
3423             $button->set_formats(PORTFOLIO_FORMAT_RICHHTML);
3424         }
3426         $porfoliohtml = $button->to_html(PORTFOLIO_ADD_TEXT_LINK);
3427         if (!empty($porfoliohtml)) {
3428             $commands[] = $porfoliohtml;
3429         }
3430     }
3431     // Finished building commands
3434     // Begin output
3436     $output  = '';
3438     if ($istracked) {
3439         if ($postisread) {
3440             $forumpostclass = ' read';
3441         } else {
3442             $forumpostclass = ' unread';
3443             $output .= html_writer::tag('a', '', array('name'=>'unread'));
3444         }
3445     } else {
3446         // ignore trackign status if not tracked or tracked param missing
3447         $forumpostclass = '';
3448     }
3450     $topicclass = '';
3451     if (empty($post->parent)) {
3452         $topicclass = ' firstpost starter';
3453     }
3455     $postbyuser = new stdClass;
3456     $postbyuser->post = $post->subject;
3457     $postbyuser->user = $postuser->fullname;
3458     $discussionbyuser = get_string('postbyuser', 'forum', $postbyuser);
3459     $output .= html_writer::tag('a', '', array('id'=>'p'.$post->id));
3460     $output .= html_writer::start_tag('div', array('class'=>'forumpost clearfix'.$forumpostclass.$topicclass,
3461                                                    'role' => 'region',
3462                                                    'aria-label' => $discussionbyuser));
3463     $output .= html_writer::start_tag('div', array('class'=>'row header clearfix'));
3464     $output .= html_writer::start_tag('div', array('class'=>'left picture'));
3465     $output .= $OUTPUT->user_picture($postuser, array('courseid'=>$course->id));
3466     $output .= html_writer::end_tag('div');
3469     $output .= html_writer::start_tag('div', array('class'=>'topic'.$topicclass));
3471     $postsubject = $post->subject;
3472     if (empty($post->subjectnoformat)) {
3473         $postsubject = format_string($postsubject);
3474     }
3475     $output .= html_writer::tag('div', $postsubject, array('class'=>'subject',
3476                                                            'role' => 'heading',
3477                                                            'aria-level' => '2'));
3479     $by = new stdClass();
3480     $by->name = html_writer::link($postuser->profilelink, $postuser->fullname);
3481     $by->date = userdate($post->modified);
3482     $output .= html_writer::tag('div', get_string('bynameondate', 'forum', $by), array('class'=>'author',
3483                                                                                        'role' => 'heading',
3484                                                                                        'aria-level' => '2'));