Merge branch 'MDL-43178_forum' of https://github.com/andyjdavis/moodle
[moodle.git] / mod / forum / lib.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * @package    mod
19  * @subpackage forum
20  * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
21  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22  */
24 defined('MOODLE_INTERNAL') || die();
26 /** Include required files */
27 require_once($CFG->libdir.'/filelib.php');
28 require_once($CFG->libdir.'/eventslib.php');
29 require_once($CFG->dirroot.'/user/selector/lib.php');
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                     add_to_log($course->id, 'forum', 'mail error', "discuss.php?d=$discussion->id#p$post->id",
777                                substr(format_string($post->subject,true),0,30), $cm->id, $userto->id);
778                     $errorcount[$post->id]++;
779                 } else {
780                     $mailcount[$post->id]++;
782                 // Mark post as read if forum_usermarksread is set off
783                     if (!$CFG->forum_usermarksread) {
784                         $userto->markposts[$post->id] = $post->id;
785                     }
786                 }
788                 mtrace('post '.$post->id. ': '.$post->subject);
789             }
791             // mark processed posts as read
792             forum_tp_mark_posts_read($userto, $userto->markposts);
793             unset($userto);
794         }
795     }
797     if ($posts) {
798         foreach ($posts as $post) {
799             mtrace($mailcount[$post->id]." users were sent post $post->id, '$post->subject'");
800             if ($errorcount[$post->id]) {
801                 $DB->set_field('forum_posts', 'mailed', FORUM_MAILED_ERROR, array('id' => $post->id));
802             }
803         }
804     }
806     // release some memory
807     unset($subscribedusers);
808     unset($mailcount);
809     unset($errorcount);
811     cron_setup_user();
813     $sitetimezone = $CFG->timezone;
815     // Now see if there are any digest mails waiting to be sent, and if we should send them
817     mtrace('Starting digest processing...');
819     core_php_time_limit::raise(300); // terminate if not able to fetch all digests in 5 minutes
821     if (!isset($CFG->digestmailtimelast)) {    // To catch the first time
822         set_config('digestmailtimelast', 0);
823     }
825     $timenow = time();
826     $digesttime = usergetmidnight($timenow, $sitetimezone) + ($CFG->digestmailtime * 3600);
828     // Delete any really old ones (normally there shouldn't be any)
829     $weekago = $timenow - (7 * 24 * 3600);
830     $DB->delete_records_select('forum_queue', "timemodified < ?", array($weekago));
831     mtrace ('Cleaned old digest records');
833     if ($CFG->digestmailtimelast < $digesttime and $timenow > $digesttime) {
835         mtrace('Sending forum digests: '.userdate($timenow, '', $sitetimezone));
837         $digestposts_rs = $DB->get_recordset_select('forum_queue', "timemodified < ?", array($digesttime));
839         if ($digestposts_rs->valid()) {
841             // We have work to do
842             $usermailcount = 0;
844             //caches - reuse the those filled before too
845             $discussionposts = array();
846             $userdiscussions = array();
848             foreach ($digestposts_rs as $digestpost) {
849                 if (!isset($posts[$digestpost->postid])) {
850                     if ($post = $DB->get_record('forum_posts', array('id' => $digestpost->postid))) {
851                         $posts[$digestpost->postid] = $post;
852                     } else {
853                         continue;
854                     }
855                 }
856                 $discussionid = $digestpost->discussionid;
857                 if (!isset($discussions[$discussionid])) {
858                     if ($discussion = $DB->get_record('forum_discussions', array('id' => $discussionid))) {
859                         $discussions[$discussionid] = $discussion;
860                     } else {
861                         continue;
862                     }
863                 }
864                 $forumid = $discussions[$discussionid]->forum;
865                 if (!isset($forums[$forumid])) {
866                     if ($forum = $DB->get_record('forum', array('id' => $forumid))) {
867                         $forums[$forumid] = $forum;
868                     } else {
869                         continue;
870                     }
871                 }
873                 $courseid = $forums[$forumid]->course;
874                 if (!isset($courses[$courseid])) {
875                     if ($course = $DB->get_record('course', array('id' => $courseid))) {
876                         $courses[$courseid] = $course;
877                     } else {
878                         continue;
879                     }
880                 }
882                 if (!isset($coursemodules[$forumid])) {
883                     if ($cm = get_coursemodule_from_instance('forum', $forumid, $courseid)) {
884                         $coursemodules[$forumid] = $cm;
885                     } else {
886                         continue;
887                     }
888                 }
889                 $userdiscussions[$digestpost->userid][$digestpost->discussionid] = $digestpost->discussionid;
890                 $discussionposts[$digestpost->discussionid][$digestpost->postid] = $digestpost->postid;
891             }
892             $digestposts_rs->close(); /// Finished iteration, let's close the resultset
894             // Data collected, start sending out emails to each user
895             foreach ($userdiscussions as $userid => $thesediscussions) {
897                 core_php_time_limit::raise(120); // terminate if processing of any account takes longer than 2 minutes
899                 cron_setup_user();
901                 mtrace(get_string('processingdigest', 'forum', $userid), '... ');
903                 // First of all delete all the queue entries for this user
904                 $DB->delete_records_select('forum_queue', "userid = ? AND timemodified < ?", array($userid, $digesttime));
906                 // Init user caches - we keep the cache for one cycle only,
907                 // otherwise it would unnecessarily consume memory.
908                 if (array_key_exists($userid, $users) and isset($users[$userid]->username)) {
909                     $userto = clone($users[$userid]);
910                 } else {
911                     $userto = $DB->get_record('user', array('id' => $userid));
912                     forum_cron_minimise_user_record($userto);
913                 }
914                 $userto->viewfullnames = array();
915                 $userto->canpost       = array();
916                 $userto->markposts     = array();
918                 // Override the language and timezone of the "current" user, so that
919                 // mail is customised for the receiver.
920                 cron_setup_user($userto);
922                 $postsubject = get_string('digestmailsubject', 'forum', format_string($site->shortname, true));
924                 $headerdata = new stdClass();
925                 $headerdata->sitename = format_string($site->fullname, true);
926                 $headerdata->userprefs = $CFG->wwwroot.'/user/edit.php?id='.$userid.'&amp;course='.$site->id;
928                 $posttext = get_string('digestmailheader', 'forum', $headerdata)."\n\n";
929                 $headerdata->userprefs = '<a target="_blank" href="'.$headerdata->userprefs.'">'.get_string('digestmailprefs', 'forum').'</a>';
931                 $posthtml = "<head>";
932 /*                foreach ($CFG->stylesheets as $stylesheet) {
933                     //TODO: MDL-21120
934                     $posthtml .= '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'" />'."\n";
935                 }*/
936                 $posthtml .= "</head>\n<body id=\"email\">\n";
937                 $posthtml .= '<p>'.get_string('digestmailheader', 'forum', $headerdata).'</p><br /><hr size="1" noshade="noshade" />';
939                 foreach ($thesediscussions as $discussionid) {
941                     core_php_time_limit::raise(120);   // to be reset for each post
943                     $discussion = $discussions[$discussionid];
944                     $forum      = $forums[$discussion->forum];
945                     $course     = $courses[$forum->course];
946                     $cm         = $coursemodules[$forum->id];
948                     //override language
949                     cron_setup_user($userto, $course);
951                     // Fill caches
952                     if (!isset($userto->viewfullnames[$forum->id])) {
953                         $modcontext = context_module::instance($cm->id);
954                         $userto->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $modcontext);
955                     }
956                     if (!isset($userto->canpost[$discussion->id])) {
957                         $modcontext = context_module::instance($cm->id);
958                         $userto->canpost[$discussion->id] = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
959                     }
961                     $strforums      = get_string('forums', 'forum');
962                     $canunsubscribe = ! forum_is_forcesubscribed($forum);
963                     $canreply       = $userto->canpost[$discussion->id];
964                     $shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
966                     $posttext .= "\n \n";
967                     $posttext .= '=====================================================================';
968                     $posttext .= "\n \n";
969                     $posttext .= "$shortname -> $strforums -> ".format_string($forum->name,true);
970                     if ($discussion->name != $forum->name) {
971                         $posttext  .= " -> ".format_string($discussion->name,true);
972                     }
973                     $posttext .= "\n";
974                     $posttext .= $CFG->wwwroot.'/mod/forum/discuss.php?d='.$discussion->id;
975                     $posttext .= "\n";
977                     $posthtml .= "<p><font face=\"sans-serif\">".
978                     "<a target=\"_blank\" href=\"$CFG->wwwroot/course/view.php?id=$course->id\">$shortname</a> -> ".
979                     "<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/index.php?id=$course->id\">$strforums</a> -> ".
980                     "<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/view.php?f=$forum->id\">".format_string($forum->name,true)."</a>";
981                     if ($discussion->name == $forum->name) {
982                         $posthtml .= "</font></p>";
983                     } else {
984                         $posthtml .= " -> <a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/discuss.php?d=$discussion->id\">".format_string($discussion->name,true)."</a></font></p>";
985                     }
986                     $posthtml .= '<p>';
988                     $postsarray = $discussionposts[$discussionid];
989                     sort($postsarray);
991                     foreach ($postsarray as $postid) {
992                         $post = $posts[$postid];
994                         if (array_key_exists($post->userid, $users)) { // we might know him/her already
995                             $userfrom = $users[$post->userid];
996                             if (!isset($userfrom->idnumber)) {
997                                 $userfrom = $DB->get_record('user', array('id' => $userfrom->id));
998                                 forum_cron_minimise_user_record($userfrom);
999                             }
1001                         } else if ($userfrom = $DB->get_record('user', array('id' => $post->userid))) {
1002                             forum_cron_minimise_user_record($userfrom);
1003                             if ($userscount <= FORUM_CRON_USER_CACHE) {
1004                                 $userscount++;
1005                                 $users[$userfrom->id] = $userfrom;
1006                             }
1008                         } else {
1009                             mtrace('Could not find user '.$post->userid);
1010                             continue;
1011                         }
1013                         if (!isset($userfrom->groups[$forum->id])) {
1014                             if (!isset($userfrom->groups)) {
1015                                 $userfrom->groups = array();
1016                                 if (isset($users[$userfrom->id])) {
1017                                     $users[$userfrom->id]->groups = array();
1018                                 }
1019                             }
1020                             $userfrom->groups[$forum->id] = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid);
1021                             if (isset($users[$userfrom->id])) {
1022                                 $users[$userfrom->id]->groups[$forum->id] = $userfrom->groups[$forum->id];
1023                             }
1024                         }
1026                         $userfrom->customheaders = array ("Precedence: Bulk");
1028                         $maildigest = forum_get_user_maildigest_bulk($digests, $userto, $forum->id);
1029                         if ($maildigest == 2) {
1030                             // Subjects and link only
1031                             $posttext .= "\n";
1032                             $posttext .= $CFG->wwwroot.'/mod/forum/discuss.php?d='.$discussion->id;
1033                             $by = new stdClass();
1034                             $by->name = fullname($userfrom);
1035                             $by->date = userdate($post->modified);
1036                             $posttext .= "\n".format_string($post->subject,true).' '.get_string("bynameondate", "forum", $by);
1037                             $posttext .= "\n---------------------------------------------------------------------";
1039                             $by->name = "<a target=\"_blank\" href=\"$CFG->wwwroot/user/view.php?id=$userfrom->id&amp;course=$course->id\">$by->name</a>";
1040                             $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>';
1042                         } else {
1043                             // The full treatment
1044                             $posttext .= forum_make_mail_text($course, $cm, $forum, $discussion, $post, $userfrom, $userto, true);
1045                             $posthtml .= forum_make_mail_post($course, $cm, $forum, $discussion, $post, $userfrom, $userto, false, $canreply, true, false);
1047                         // Create an array of postid's for this user to mark as read.
1048                             if (!$CFG->forum_usermarksread) {
1049                                 $userto->markposts[$post->id] = $post->id;
1050                             }
1051                         }
1052                     }
1053                     $footerlinks = array();
1054                     if ($canunsubscribe) {
1055                         $footerlinks[] = "<a href=\"$CFG->wwwroot/mod/forum/subscribe.php?id=$forum->id\">" . get_string("unsubscribe", "forum") . "</a>";
1056                     } else {
1057                         $footerlinks[] = get_string("everyoneissubscribed", "forum");
1058                     }
1059                     $footerlinks[] = "<a href='{$CFG->wwwroot}/mod/forum/index.php?id={$forum->course}'>" . get_string("digestmailpost", "forum") . '</a>';
1060                     $posthtml .= "\n<div class='mdl-right'><font size=\"1\">" . implode('&nbsp;', $footerlinks) . '</font></div>';
1061                     $posthtml .= '<hr size="1" noshade="noshade" /></p>';
1062                 }
1063                 $posthtml .= '</body>';
1065                 if (empty($userto->mailformat) || $userto->mailformat != 1) {
1066                     // This user DOESN'T want to receive HTML
1067                     $posthtml = '';
1068                 }
1070                 $attachment = $attachname='';
1071                 // Directly email forum digests rather than sending them via messaging, use the
1072                 // site shortname as 'from name', the noreply address will be used by email_to_user.
1073                 $mailresult = email_to_user($userto, $site->shortname, $postsubject, $posttext, $posthtml, $attachment, $attachname);
1075                 if (!$mailresult) {
1076                     mtrace("ERROR!");
1077                     echo "Error: mod/forum/cron.php: Could not send out digest mail to user $userto->id ($userto->email)... not trying again.\n";
1078                     add_to_log($course->id, 'forum', 'mail digest error', '', '', $cm->id, $userto->id);
1079                 } else {
1080                     mtrace("success.");
1081                     $usermailcount++;
1083                     // Mark post as read if forum_usermarksread is set off
1084                     forum_tp_mark_posts_read($userto, $userto->markposts);
1085                 }
1086             }
1087         }
1088     /// We have finishied all digest emails, update $CFG->digestmailtimelast
1089         set_config('digestmailtimelast', $timenow);
1090     }
1092     cron_setup_user();
1094     if (!empty($usermailcount)) {
1095         mtrace(get_string('digestsentusers', 'forum', $usermailcount));
1096     }
1098     if (!empty($CFG->forum_lastreadclean)) {
1099         $timenow = time();
1100         if ($CFG->forum_lastreadclean + (24*3600) < $timenow) {
1101             set_config('forum_lastreadclean', $timenow);
1102             mtrace('Removing old forum read tracking info...');
1103             forum_tp_clean_read_records();
1104         }
1105     } else {
1106         set_config('forum_lastreadclean', time());
1107     }
1110     return true;
1113 /**
1114  * Builds and returns the body of the email notification in plain text.
1115  *
1116  * @global object
1117  * @global object
1118  * @uses CONTEXT_MODULE
1119  * @param object $course
1120  * @param object $cm
1121  * @param object $forum
1122  * @param object $discussion
1123  * @param object $post
1124  * @param object $userfrom
1125  * @param object $userto
1126  * @param boolean $bare
1127  * @return string The email body in plain text format.
1128  */
1129 function forum_make_mail_text($course, $cm, $forum, $discussion, $post, $userfrom, $userto, $bare = false) {
1130     global $CFG, $USER;
1132     $modcontext = context_module::instance($cm->id);
1134     if (!isset($userto->viewfullnames[$forum->id])) {
1135         $viewfullnames = has_capability('moodle/site:viewfullnames', $modcontext, $userto->id);
1136     } else {
1137         $viewfullnames = $userto->viewfullnames[$forum->id];
1138     }
1140     if (!isset($userto->canpost[$discussion->id])) {
1141         $canreply = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
1142     } else {
1143         $canreply = $userto->canpost[$discussion->id];
1144     }
1146     $by = New stdClass;
1147     $by->name = fullname($userfrom, $viewfullnames);
1148     $by->date = userdate($post->modified, "", $userto->timezone);
1150     $strbynameondate = get_string('bynameondate', 'forum', $by);
1152     $strforums = get_string('forums', 'forum');
1154     $canunsubscribe = ! forum_is_forcesubscribed($forum);
1156     $posttext = '';
1158     if (!$bare) {
1159         $shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
1160         $posttext  = "$shortname -> $strforums -> ".format_string($forum->name,true);
1162         if ($discussion->name != $forum->name) {
1163             $posttext  .= " -> ".format_string($discussion->name,true);
1164         }
1165     }
1167     // add absolute file links
1168     $post->message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php', $modcontext->id, 'mod_forum', 'post', $post->id);
1170     $posttext .= "\n";
1171     $posttext .= $CFG->wwwroot.'/mod/forum/discuss.php?d='.$discussion->id;
1172     $posttext .= "\n---------------------------------------------------------------------\n";
1173     $posttext .= format_string($post->subject,true);
1174     if ($bare) {
1175         $posttext .= " ($CFG->wwwroot/mod/forum/discuss.php?d=$discussion->id#p$post->id)";
1176     }
1177     $posttext .= "\n".$strbynameondate."\n";
1178     $posttext .= "---------------------------------------------------------------------\n";
1179     $posttext .= format_text_email($post->message, $post->messageformat);
1180     $posttext .= "\n\n";
1181     $posttext .= forum_print_attachments($post, $cm, "text");
1183     if (!$bare && $canreply) {
1184         $posttext .= "---------------------------------------------------------------------\n";
1185         $posttext .= get_string("postmailinfo", "forum", $shortname)."\n";
1186         $posttext .= "$CFG->wwwroot/mod/forum/post.php?reply=$post->id\n";
1187     }
1188     if (!$bare && $canunsubscribe) {
1189         $posttext .= "\n---------------------------------------------------------------------\n";
1190         $posttext .= get_string("unsubscribe", "forum");
1191         $posttext .= ": $CFG->wwwroot/mod/forum/subscribe.php?id=$forum->id\n";
1192     }
1194     $posttext .= "\n---------------------------------------------------------------------\n";
1195     $posttext .= get_string("digestmailpost", "forum");
1196     $posttext .= ": {$CFG->wwwroot}/mod/forum/index.php?id={$forum->course}\n";
1198     return $posttext;
1201 /**
1202  * Builds and returns the body of the email notification in html format.
1203  *
1204  * @global object
1205  * @param object $course
1206  * @param object $cm
1207  * @param object $forum
1208  * @param object $discussion
1209  * @param object $post
1210  * @param object $userfrom
1211  * @param object $userto
1212  * @return string The email text in HTML format
1213  */
1214 function forum_make_mail_html($course, $cm, $forum, $discussion, $post, $userfrom, $userto) {
1215     global $CFG;
1217     if ($userto->mailformat != 1) {  // Needs to be HTML
1218         return '';
1219     }
1221     if (!isset($userto->canpost[$discussion->id])) {
1222         $canreply = forum_user_can_post($forum, $discussion, $userto, $cm, $course);
1223     } else {
1224         $canreply = $userto->canpost[$discussion->id];
1225     }
1227     $strforums = get_string('forums', 'forum');
1228     $canunsubscribe = ! forum_is_forcesubscribed($forum);
1229     $shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
1231     $posthtml = '<head>';
1232 /*    foreach ($CFG->stylesheets as $stylesheet) {
1233         //TODO: MDL-21120
1234         $posthtml .= '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'" />'."\n";
1235     }*/
1236     $posthtml .= '</head>';
1237     $posthtml .= "\n<body id=\"email\">\n\n";
1239     $posthtml .= '<div class="navbar">'.
1240     '<a target="_blank" href="'.$CFG->wwwroot.'/course/view.php?id='.$course->id.'">'.$shortname.'</a> &raquo; '.
1241     '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/index.php?id='.$course->id.'">'.$strforums.'</a> &raquo; '.
1242     '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/view.php?f='.$forum->id.'">'.format_string($forum->name,true).'</a>';
1243     if ($discussion->name == $forum->name) {
1244         $posthtml .= '</div>';
1245     } else {
1246         $posthtml .= ' &raquo; <a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$discussion->id.'">'.
1247                      format_string($discussion->name,true).'</a></div>';
1248     }
1249     $posthtml .= forum_make_mail_post($course, $cm, $forum, $discussion, $post, $userfrom, $userto, false, $canreply, true, false);
1251     $footerlinks = array();
1252     if ($canunsubscribe) {
1253         $footerlinks[] = '<a href="' . $CFG->wwwroot . '/mod/forum/subscribe.php?id=' . $forum->id . '">' . get_string('unsubscribe', 'forum') . '</a>';
1254         $footerlinks[] = '<a href="' . $CFG->wwwroot . '/mod/forum/unsubscribeall.php">' . get_string('unsubscribeall', 'forum') . '</a>';
1255     }
1256     $footerlinks[] = "<a href='{$CFG->wwwroot}/mod/forum/index.php?id={$forum->course}'>" . get_string('digestmailpost', 'forum') . '</a>';
1257     $posthtml .= '<hr /><div class="mdl-align unsubscribelink">' . implode('&nbsp;', $footerlinks) . '</div>';
1259     $posthtml .= '</body>';
1261     return $posthtml;
1265 /**
1266  *
1267  * @param object $course
1268  * @param object $user
1269  * @param object $mod TODO this is not used in this function, refactor
1270  * @param object $forum
1271  * @return object A standard object with 2 variables: info (number of posts for this user) and time (last modified)
1272  */
1273 function forum_user_outline($course, $user, $mod, $forum) {
1274     global $CFG;
1275     require_once("$CFG->libdir/gradelib.php");
1276     $grades = grade_get_grades($course->id, 'mod', 'forum', $forum->id, $user->id);
1277     if (empty($grades->items[0]->grades)) {
1278         $grade = false;
1279     } else {
1280         $grade = reset($grades->items[0]->grades);
1281     }
1283     $count = forum_count_user_posts($forum->id, $user->id);
1285     if ($count && $count->postcount > 0) {
1286         $result = new stdClass();
1287         $result->info = get_string("numposts", "forum", $count->postcount);
1288         $result->time = $count->lastpost;
1289         if ($grade) {
1290             $result->info .= ', ' . get_string('grade') . ': ' . $grade->str_long_grade;
1291         }
1292         return $result;
1293     } else if ($grade) {
1294         $result = new stdClass();
1295         $result->info = get_string('grade') . ': ' . $grade->str_long_grade;
1297         //datesubmitted == time created. dategraded == time modified or time overridden
1298         //if grade was last modified by the user themselves use date graded. Otherwise use date submitted
1299         //TODO: move this copied & pasted code somewhere in the grades API. See MDL-26704
1300         if ($grade->usermodified == $user->id || empty($grade->datesubmitted)) {
1301             $result->time = $grade->dategraded;
1302         } else {
1303             $result->time = $grade->datesubmitted;
1304         }
1306         return $result;
1307     }
1308     return NULL;
1312 /**
1313  * @global object
1314  * @global object
1315  * @param object $coure
1316  * @param object $user
1317  * @param object $mod
1318  * @param object $forum
1319  */
1320 function forum_user_complete($course, $user, $mod, $forum) {
1321     global $CFG,$USER, $OUTPUT;
1322     require_once("$CFG->libdir/gradelib.php");
1324     $grades = grade_get_grades($course->id, 'mod', 'forum', $forum->id, $user->id);
1325     if (!empty($grades->items[0]->grades)) {
1326         $grade = reset($grades->items[0]->grades);
1327         echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade);
1328         if ($grade->str_feedback) {
1329             echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
1330         }
1331     }
1333     if ($posts = forum_get_user_posts($forum->id, $user->id)) {
1335         if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $course->id)) {
1336             print_error('invalidcoursemodule');
1337         }
1338         $discussions = forum_get_user_involved_discussions($forum->id, $user->id);
1340         foreach ($posts as $post) {
1341             if (!isset($discussions[$post->discussion])) {
1342                 continue;
1343             }
1344             $discussion = $discussions[$post->discussion];
1346             forum_print_post($post, $discussion, $forum, $cm, $course, false, false, false);
1347         }
1348     } else {
1349         echo "<p>".get_string("noposts", "forum")."</p>";
1350     }
1358 /**
1359  * @global object
1360  * @global object
1361  * @global object
1362  * @param array $courses
1363  * @param array $htmlarray
1364  */
1365 function forum_print_overview($courses,&$htmlarray) {
1366     global $USER, $CFG, $DB, $SESSION;
1368     if (empty($courses) || !is_array($courses) || count($courses) == 0) {
1369         return array();
1370     }
1372     if (!$forums = get_all_instances_in_courses('forum',$courses)) {
1373         return;
1374     }
1376     // Courses to search for new posts
1377     $coursessqls = array();
1378     $params = array();
1379     foreach ($courses as $course) {
1381         // If the user has never entered into the course all posts are pending
1382         if ($course->lastaccess == 0) {
1383             $coursessqls[] = '(f.course = ?)';
1384             $params[] = $course->id;
1386         // Only posts created after the course last access
1387         } else {
1388             $coursessqls[] = '(f.course = ? AND p.created > ?)';
1389             $params[] = $course->id;
1390             $params[] = $course->lastaccess;
1391         }
1392     }
1393     $params[] = $USER->id;
1394     $coursessql = implode(' OR ', $coursessqls);
1396     $sql = "SELECT f.id, COUNT(*) as count "
1397                 .'FROM {forum} f '
1398                 .'JOIN {forum_discussions} d ON d.forum  = f.id '
1399                 .'JOIN {forum_posts} p ON p.discussion = d.id '
1400                 ."WHERE ($coursessql) "
1401                 .'AND p.userid != ? '
1402                 .'GROUP BY f.id';
1404     if (!$new = $DB->get_records_sql($sql, $params)) {
1405         $new = array(); // avoid warnings
1406     }
1408     // also get all forum tracking stuff ONCE.
1409     $trackingforums = array();
1410     foreach ($forums as $forum) {
1411         if (forum_tp_can_track_forums($forum)) {
1412             $trackingforums[$forum->id] = $forum;
1413         }
1414     }
1416     if (count($trackingforums) > 0) {
1417         $cutoffdate = isset($CFG->forum_oldpostdays) ? (time() - ($CFG->forum_oldpostdays*24*60*60)) : 0;
1418         $sql = 'SELECT d.forum,d.course,COUNT(p.id) AS count '.
1419             ' FROM {forum_posts} p '.
1420             ' JOIN {forum_discussions} d ON p.discussion = d.id '.
1421             ' LEFT JOIN {forum_read} r ON r.postid = p.id AND r.userid = ? WHERE (';
1422         $params = array($USER->id);
1424         foreach ($trackingforums as $track) {
1425             $sql .= '(d.forum = ? AND (d.groupid = -1 OR d.groupid = 0 OR d.groupid = ?)) OR ';
1426             $params[] = $track->id;
1427             if (isset($SESSION->currentgroup[$track->course])) {
1428                 $groupid =  $SESSION->currentgroup[$track->course];
1429             } else {
1430                 // get first groupid
1431                 $groupids = groups_get_all_groups($track->course, $USER->id);
1432                 if ($groupids) {
1433                     reset($groupids);
1434                     $groupid = key($groupids);
1435                     $SESSION->currentgroup[$track->course] = $groupid;
1436                 } else {
1437                     $groupid = 0;
1438                 }
1439                 unset($groupids);
1440             }
1441             $params[] = $groupid;
1442         }
1443         $sql = substr($sql,0,-3); // take off the last OR
1444         $sql .= ') AND p.modified >= ? AND r.id is NULL GROUP BY d.forum,d.course';
1445         $params[] = $cutoffdate;
1447         if (!$unread = $DB->get_records_sql($sql, $params)) {
1448             $unread = array();
1449         }
1450     } else {
1451         $unread = array();
1452     }
1454     if (empty($unread) and empty($new)) {
1455         return;
1456     }
1458     $strforum = get_string('modulename','forum');
1460     foreach ($forums as $forum) {
1461         $str = '';
1462         $count = 0;
1463         $thisunread = 0;
1464         $showunread = false;
1465         // either we have something from logs, or trackposts, or nothing.
1466         if (array_key_exists($forum->id, $new) && !empty($new[$forum->id])) {
1467             $count = $new[$forum->id]->count;
1468         }
1469         if (array_key_exists($forum->id,$unread)) {
1470             $thisunread = $unread[$forum->id]->count;
1471             $showunread = true;
1472         }
1473         if ($count > 0 || $thisunread > 0) {
1474             $str .= '<div class="overview forum"><div class="name">'.$strforum.': <a title="'.$strforum.'" href="'.$CFG->wwwroot.'/mod/forum/view.php?f='.$forum->id.'">'.
1475                 $forum->name.'</a></div>';
1476             $str .= '<div class="info"><span class="postsincelogin">';
1477             $str .= get_string('overviewnumpostssince', 'forum', $count)."</span>";
1478             if (!empty($showunread)) {
1479                 $str .= '<div class="unreadposts">'.get_string('overviewnumunread', 'forum', $thisunread).'</div>';
1480             }
1481             $str .= '</div></div>';
1482         }
1483         if (!empty($str)) {
1484             if (!array_key_exists($forum->course,$htmlarray)) {
1485                 $htmlarray[$forum->course] = array();
1486             }
1487             if (!array_key_exists('forum',$htmlarray[$forum->course])) {
1488                 $htmlarray[$forum->course]['forum'] = ''; // initialize, avoid warnings
1489             }
1490             $htmlarray[$forum->course]['forum'] .= $str;
1491         }
1492     }
1495 /**
1496  * Given a course and a date, prints a summary of all the new
1497  * messages posted in the course since that date
1498  *
1499  * @global object
1500  * @global object
1501  * @global object
1502  * @uses CONTEXT_MODULE
1503  * @uses VISIBLEGROUPS
1504  * @param object $course
1505  * @param bool $viewfullnames capability
1506  * @param int $timestart
1507  * @return bool success
1508  */
1509 function forum_print_recent_activity($course, $viewfullnames, $timestart) {
1510     global $CFG, $USER, $DB, $OUTPUT;
1512     // do not use log table if possible, it may be huge and is expensive to join with other tables
1514     $allnamefields = user_picture::fields('u', null, 'duserid');
1515     if (!$posts = $DB->get_records_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid,
1516                                               d.timestart, d.timeend, $allnamefields
1517                                          FROM {forum_posts} p
1518                                               JOIN {forum_discussions} d ON d.id = p.discussion
1519                                               JOIN {forum} f             ON f.id = d.forum
1520                                               JOIN {user} u              ON u.id = p.userid
1521                                         WHERE p.created > ? AND f.course = ?
1522                                      ORDER BY p.id ASC", array($timestart, $course->id))) { // order by initial posting date
1523          return false;
1524     }
1526     $modinfo = get_fast_modinfo($course);
1528     $groupmodes = array();
1529     $cms    = array();
1531     $strftimerecent = get_string('strftimerecent');
1533     $printposts = array();
1534     foreach ($posts as $post) {
1535         if (!isset($modinfo->instances['forum'][$post->forum])) {
1536             // not visible
1537             continue;
1538         }
1539         $cm = $modinfo->instances['forum'][$post->forum];
1540         if (!$cm->uservisible) {
1541             continue;
1542         }
1543         $context = context_module::instance($cm->id);
1545         if (!has_capability('mod/forum:viewdiscussion', $context)) {
1546             continue;
1547         }
1549         if (!empty($CFG->forum_enabletimedposts) and $USER->id != $post->duserid
1550           and (($post->timestart > 0 and $post->timestart > time()) or ($post->timeend > 0 and $post->timeend < time()))) {
1551             if (!has_capability('mod/forum:viewhiddentimedposts', $context)) {
1552                 continue;
1553             }
1554         }
1556         $groupmode = groups_get_activity_groupmode($cm, $course);
1558         if ($groupmode) {
1559             if ($post->groupid == -1 or $groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $context)) {
1560                 // oki (Open discussions have groupid -1)
1561             } else {
1562                 // separate mode
1563                 if (isguestuser()) {
1564                     // shortcut
1565                     continue;
1566                 }
1568                 if (!in_array($post->groupid, $modinfo->get_groups($cm->groupingid))) {
1569                     continue;
1570                 }
1571             }
1572         }
1574         $printposts[] = $post;
1575     }
1576     unset($posts);
1578     if (!$printposts) {
1579         return false;
1580     }
1582     echo $OUTPUT->heading(get_string('newforumposts', 'forum').':', 3);
1583     echo "\n<ul class='unlist'>\n";
1585     foreach ($printposts as $post) {
1586         $subjectclass = empty($post->parent) ? ' bold' : '';
1588         echo '<li><div class="head">'.
1589                '<div class="date">'.userdate($post->modified, $strftimerecent).'</div>'.
1590                '<div class="name">'.fullname($post, $viewfullnames).'</div>'.
1591              '</div>';
1592         echo '<div class="info'.$subjectclass.'">';
1593         if (empty($post->parent)) {
1594             echo '"<a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'">';
1595         } else {
1596             echo '"<a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'&amp;parent='.$post->parent.'#p'.$post->id.'">';
1597         }
1598         $post->subject = break_up_long_words(format_string($post->subject, true));
1599         echo $post->subject;
1600         echo "</a>\"</div></li>\n";
1601     }
1603     echo "</ul>\n";
1605     return true;
1608 /**
1609  * Return grade for given user or all users.
1610  *
1611  * @global object
1612  * @global object
1613  * @param object $forum
1614  * @param int $userid optional user id, 0 means all users
1615  * @return array array of grades, false if none
1616  */
1617 function forum_get_user_grades($forum, $userid = 0) {
1618     global $CFG;
1620     require_once($CFG->dirroot.'/rating/lib.php');
1622     $ratingoptions = new stdClass;
1623     $ratingoptions->component = 'mod_forum';
1624     $ratingoptions->ratingarea = 'post';
1626     //need these to work backwards to get a context id. Is there a better way to get contextid from a module instance?
1627     $ratingoptions->modulename = 'forum';
1628     $ratingoptions->moduleid   = $forum->id;
1629     $ratingoptions->userid = $userid;
1630     $ratingoptions->aggregationmethod = $forum->assessed;
1631     $ratingoptions->scaleid = $forum->scale;
1632     $ratingoptions->itemtable = 'forum_posts';
1633     $ratingoptions->itemtableusercolumn = 'userid';
1635     $rm = new rating_manager();
1636     return $rm->get_user_grades($ratingoptions);
1639 /**
1640  * Update activity grades
1641  *
1642  * @category grade
1643  * @param object $forum
1644  * @param int $userid specific user only, 0 means all
1645  * @param boolean $nullifnone return null if grade does not exist
1646  * @return void
1647  */
1648 function forum_update_grades($forum, $userid=0, $nullifnone=true) {
1649     global $CFG, $DB;
1650     require_once($CFG->libdir.'/gradelib.php');
1652     if (!$forum->assessed) {
1653         forum_grade_item_update($forum);
1655     } else if ($grades = forum_get_user_grades($forum, $userid)) {
1656         forum_grade_item_update($forum, $grades);
1658     } else if ($userid and $nullifnone) {
1659         $grade = new stdClass();
1660         $grade->userid   = $userid;
1661         $grade->rawgrade = NULL;
1662         forum_grade_item_update($forum, $grade);
1664     } else {
1665         forum_grade_item_update($forum);
1666     }
1669 /**
1670  * Update all grades in gradebook.
1671  * @global object
1672  */
1673 function forum_upgrade_grades() {
1674     global $DB;
1676     $sql = "SELECT COUNT('x')
1677               FROM {forum} f, {course_modules} cm, {modules} m
1678              WHERE m.name='forum' AND m.id=cm.module AND cm.instance=f.id";
1679     $count = $DB->count_records_sql($sql);
1681     $sql = "SELECT f.*, cm.idnumber AS cmidnumber, f.course AS courseid
1682               FROM {forum} f, {course_modules} cm, {modules} m
1683              WHERE m.name='forum' AND m.id=cm.module AND cm.instance=f.id";
1684     $rs = $DB->get_recordset_sql($sql);
1685     if ($rs->valid()) {
1686         $pbar = new progress_bar('forumupgradegrades', 500, true);
1687         $i=0;
1688         foreach ($rs as $forum) {
1689             $i++;
1690             upgrade_set_timeout(60*5); // set up timeout, may also abort execution
1691             forum_update_grades($forum, 0, false);
1692             $pbar->update($i, $count, "Updating Forum grades ($i/$count).");
1693         }
1694     }
1695     $rs->close();
1698 /**
1699  * Create/update grade item for given forum
1700  *
1701  * @category grade
1702  * @uses GRADE_TYPE_NONE
1703  * @uses GRADE_TYPE_VALUE
1704  * @uses GRADE_TYPE_SCALE
1705  * @param stdClass $forum Forum object with extra cmidnumber
1706  * @param mixed $grades Optional array/object of grade(s); 'reset' means reset grades in gradebook
1707  * @return int 0 if ok
1708  */
1709 function forum_grade_item_update($forum, $grades=NULL) {
1710     global $CFG;
1711     if (!function_exists('grade_update')) { //workaround for buggy PHP versions
1712         require_once($CFG->libdir.'/gradelib.php');
1713     }
1715     $params = array('itemname'=>$forum->name, 'idnumber'=>$forum->cmidnumber);
1717     if (!$forum->assessed or $forum->scale == 0) {
1718         $params['gradetype'] = GRADE_TYPE_NONE;
1720     } else if ($forum->scale > 0) {
1721         $params['gradetype'] = GRADE_TYPE_VALUE;
1722         $params['grademax']  = $forum->scale;
1723         $params['grademin']  = 0;
1725     } else if ($forum->scale < 0) {
1726         $params['gradetype'] = GRADE_TYPE_SCALE;
1727         $params['scaleid']   = -$forum->scale;
1728     }
1730     if ($grades  === 'reset') {
1731         $params['reset'] = true;
1732         $grades = NULL;
1733     }
1735     return grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, $grades, $params);
1738 /**
1739  * Delete grade item for given forum
1740  *
1741  * @category grade
1742  * @param stdClass $forum Forum object
1743  * @return grade_item
1744  */
1745 function forum_grade_item_delete($forum) {
1746     global $CFG;
1747     require_once($CFG->libdir.'/gradelib.php');
1749     return grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, NULL, array('deleted'=>1));
1753 /**
1754  * This function returns if a scale is being used by one forum
1755  *
1756  * @global object
1757  * @param int $forumid
1758  * @param int $scaleid negative number
1759  * @return bool
1760  */
1761 function forum_scale_used ($forumid,$scaleid) {
1762     global $DB;
1763     $return = false;
1765     $rec = $DB->get_record("forum",array("id" => "$forumid","scale" => "-$scaleid"));
1767     if (!empty($rec) && !empty($scaleid)) {
1768         $return = true;
1769     }
1771     return $return;
1774 /**
1775  * Checks if scale is being used by any instance of forum
1776  *
1777  * This is used to find out if scale used anywhere
1778  *
1779  * @global object
1780  * @param $scaleid int
1781  * @return boolean True if the scale is used by any forum
1782  */
1783 function forum_scale_used_anywhere($scaleid) {
1784     global $DB;
1785     if ($scaleid and $DB->record_exists('forum', array('scale' => -$scaleid))) {
1786         return true;
1787     } else {
1788         return false;
1789     }
1792 // SQL FUNCTIONS ///////////////////////////////////////////////////////////
1794 /**
1795  * Gets a post with all info ready for forum_print_post
1796  * Most of these joins are just to get the forum id
1797  *
1798  * @global object
1799  * @global object
1800  * @param int $postid
1801  * @return mixed array of posts or false
1802  */
1803 function forum_get_post_full($postid) {
1804     global $CFG, $DB;
1806     $allnames = get_all_user_name_fields(true, 'u');
1807     return $DB->get_record_sql("SELECT p.*, d.forum, $allnames, u.email, u.picture, u.imagealt
1808                              FROM {forum_posts} p
1809                                   JOIN {forum_discussions} d ON p.discussion = d.id
1810                                   LEFT JOIN {user} u ON p.userid = u.id
1811                             WHERE p.id = ?", array($postid));
1814 /**
1815  * Gets posts with all info ready for forum_print_post
1816  * We pass forumid in because we always know it so no need to make a
1817  * complicated join to find it out.
1818  *
1819  * @global object
1820  * @global object
1821  * @return mixed array of posts or false
1822  */
1823 function forum_get_discussion_posts($discussion, $sort, $forumid) {
1824     global $CFG, $DB;
1826     $allnames = get_all_user_name_fields(true, 'u');
1827     return $DB->get_records_sql("SELECT p.*, $forumid AS forum, $allnames, u.email, u.picture, u.imagealt
1828                               FROM {forum_posts} p
1829                          LEFT JOIN {user} u ON p.userid = u.id
1830                              WHERE p.discussion = ?
1831                                AND p.parent > 0 $sort", array($discussion));
1834 /**
1835  * Gets all posts in discussion including top parent.
1836  *
1837  * @global object
1838  * @global object
1839  * @global object
1840  * @param int $discussionid
1841  * @param string $sort
1842  * @param bool $tracking does user track the forum?
1843  * @return array of posts
1844  */
1845 function forum_get_all_discussion_posts($discussionid, $sort, $tracking=false) {
1846     global $CFG, $DB, $USER;
1848     $tr_sel  = "";
1849     $tr_join = "";
1850     $params = array();
1852     if ($tracking) {
1853         $now = time();
1854         $cutoffdate = $now - ($CFG->forum_oldpostdays * 24 * 3600);
1855         $tr_sel  = ", fr.id AS postread";
1856         $tr_join = "LEFT JOIN {forum_read} fr ON (fr.postid = p.id AND fr.userid = ?)";
1857         $params[] = $USER->id;
1858     }
1860     $allnames = get_all_user_name_fields(true, 'u');
1861     $params[] = $discussionid;
1862     if (!$posts = $DB->get_records_sql("SELECT p.*, $allnames, u.email, u.picture, u.imagealt $tr_sel
1863                                      FROM {forum_posts} p
1864                                           LEFT JOIN {user} u ON p.userid = u.id
1865                                           $tr_join
1866                                     WHERE p.discussion = ?
1867                                  ORDER BY $sort", $params)) {
1868         return array();
1869     }
1871     foreach ($posts as $pid=>$p) {
1872         if ($tracking) {
1873             if (forum_tp_is_post_old($p)) {
1874                  $posts[$pid]->postread = true;
1875             }
1876         }
1877         if (!$p->parent) {
1878             continue;
1879         }
1880         if (!isset($posts[$p->parent])) {
1881             continue; // parent does not exist??
1882         }
1883         if (!isset($posts[$p->parent]->children)) {
1884             $posts[$p->parent]->children = array();
1885         }
1886         $posts[$p->parent]->children[$pid] =& $posts[$pid];
1887     }
1889     return $posts;
1892 /**
1893  * Gets posts with all info ready for forum_print_post
1894  * We pass forumid in because we always know it so no need to make a
1895  * complicated join to find it out.
1896  *
1897  * @global object
1898  * @global object
1899  * @param int $parent
1900  * @param int $forumid
1901  * @return array
1902  */
1903 function forum_get_child_posts($parent, $forumid) {
1904     global $CFG, $DB;
1906     $allnames = get_all_user_name_fields(true, 'u');
1907     return $DB->get_records_sql("SELECT p.*, $forumid AS forum, $allnames, u.email, u.picture, u.imagealt
1908                               FROM {forum_posts} p
1909                          LEFT JOIN {user} u ON p.userid = u.id
1910                              WHERE p.parent = ?
1911                           ORDER BY p.created ASC", array($parent));
1914 /**
1915  * An array of forum objects that the user is allowed to read/search through.
1916  *
1917  * @global object
1918  * @global object
1919  * @global object
1920  * @param int $userid
1921  * @param int $courseid if 0, we look for forums throughout the whole site.
1922  * @return array of forum objects, or false if no matches
1923  *         Forum objects have the following attributes:
1924  *         id, type, course, cmid, cmvisible, cmgroupmode, accessallgroups,
1925  *         viewhiddentimedposts
1926  */
1927 function forum_get_readable_forums($userid, $courseid=0) {
1929     global $CFG, $DB, $USER;
1930     require_once($CFG->dirroot.'/course/lib.php');
1932     if (!$forummod = $DB->get_record('modules', array('name' => 'forum'))) {
1933         print_error('notinstalled', 'forum');
1934     }
1936     if ($courseid) {
1937         $courses = $DB->get_records('course', array('id' => $courseid));
1938     } else {
1939         // If no course is specified, then the user can see SITE + his courses.
1940         $courses1 = $DB->get_records('course', array('id' => SITEID));
1941         $courses2 = enrol_get_users_courses($userid, true, array('modinfo'));
1942         $courses = array_merge($courses1, $courses2);
1943     }
1944     if (!$courses) {
1945         return array();
1946     }
1948     $readableforums = array();
1950     foreach ($courses as $course) {
1952         $modinfo = get_fast_modinfo($course);
1954         if (empty($modinfo->instances['forum'])) {
1955             // hmm, no forums?
1956             continue;
1957         }
1959         $courseforums = $DB->get_records('forum', array('course' => $course->id));
1961         foreach ($modinfo->instances['forum'] as $forumid => $cm) {
1962             if (!$cm->uservisible or !isset($courseforums[$forumid])) {
1963                 continue;
1964             }
1965             $context = context_module::instance($cm->id);
1966             $forum = $courseforums[$forumid];
1967             $forum->context = $context;
1968             $forum->cm = $cm;
1970             if (!has_capability('mod/forum:viewdiscussion', $context)) {
1971                 continue;
1972             }
1974          /// group access
1975             if (groups_get_activity_groupmode($cm, $course) == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) {
1977                 $forum->onlygroups = $modinfo->get_groups($cm->groupingid);
1978                 $forum->onlygroups[] = -1;
1979             }
1981         /// hidden timed discussions
1982             $forum->viewhiddentimedposts = true;
1983             if (!empty($CFG->forum_enabletimedposts)) {
1984                 if (!has_capability('mod/forum:viewhiddentimedposts', $context)) {
1985                     $forum->viewhiddentimedposts = false;
1986                 }
1987             }
1989         /// qanda access
1990             if ($forum->type == 'qanda'
1991                     && !has_capability('mod/forum:viewqandawithoutposting', $context)) {
1993                 // We need to check whether the user has posted in the qanda forum.
1994                 $forum->onlydiscussions = array();  // Holds discussion ids for the discussions
1995                                                     // the user is allowed to see in this forum.
1996                 if ($discussionspostedin = forum_discussions_user_has_posted_in($forum->id, $USER->id)) {
1997                     foreach ($discussionspostedin as $d) {
1998                         $forum->onlydiscussions[] = $d->id;
1999                     }
2000                 }
2001             }
2003             $readableforums[$forum->id] = $forum;
2004         }
2006         unset($modinfo);
2008     } // End foreach $courses
2010     return $readableforums;
2013 /**
2014  * Returns a list of posts found using an array of search terms.
2015  *
2016  * @global object
2017  * @global object
2018  * @global object
2019  * @param array $searchterms array of search terms, e.g. word +word -word
2020  * @param int $courseid if 0, we search through the whole site
2021  * @param int $limitfrom
2022  * @param int $limitnum
2023  * @param int &$totalcount
2024  * @param string $extrasql
2025  * @return array|bool Array of posts found or false
2026  */
2027 function forum_search_posts($searchterms, $courseid=0, $limitfrom=0, $limitnum=50,
2028                             &$totalcount, $extrasql='') {
2029     global $CFG, $DB, $USER;
2030     require_once($CFG->libdir.'/searchlib.php');
2032     $forums = forum_get_readable_forums($USER->id, $courseid);
2034     if (count($forums) == 0) {
2035         $totalcount = 0;
2036         return false;
2037     }
2039     $now = round(time(), -2); // db friendly
2041     $fullaccess = array();
2042     $where = array();
2043     $params = array();
2045     foreach ($forums as $forumid => $forum) {
2046         $select = array();
2048         if (!$forum->viewhiddentimedposts) {
2049             $select[] = "(d.userid = :userid{$forumid} OR (d.timestart < :timestart{$forumid} AND (d.timeend = 0 OR d.timeend > :timeend{$forumid})))";
2050             $params = array_merge($params, array('userid'.$forumid=>$USER->id, 'timestart'.$forumid=>$now, 'timeend'.$forumid=>$now));
2051         }
2053         $cm = $forum->cm;
2054         $context = $forum->context;
2056         if ($forum->type == 'qanda'
2057             && !has_capability('mod/forum:viewqandawithoutposting', $context)) {
2058             if (!empty($forum->onlydiscussions)) {
2059                 list($discussionid_sql, $discussionid_params) = $DB->get_in_or_equal($forum->onlydiscussions, SQL_PARAMS_NAMED, 'qanda'.$forumid.'_');
2060                 $params = array_merge($params, $discussionid_params);
2061                 $select[] = "(d.id $discussionid_sql OR p.parent = 0)";
2062             } else {
2063                 $select[] = "p.parent = 0";
2064             }
2065         }
2067         if (!empty($forum->onlygroups)) {
2068             list($groupid_sql, $groupid_params) = $DB->get_in_or_equal($forum->onlygroups, SQL_PARAMS_NAMED, 'grps'.$forumid.'_');
2069             $params = array_merge($params, $groupid_params);
2070             $select[] = "d.groupid $groupid_sql";
2071         }
2073         if ($select) {
2074             $selects = implode(" AND ", $select);
2075             $where[] = "(d.forum = :forum{$forumid} AND $selects)";
2076             $params['forum'.$forumid] = $forumid;
2077         } else {
2078             $fullaccess[] = $forumid;
2079         }
2080     }
2082     if ($fullaccess) {
2083         list($fullid_sql, $fullid_params) = $DB->get_in_or_equal($fullaccess, SQL_PARAMS_NAMED, 'fula');
2084         $params = array_merge($params, $fullid_params);
2085         $where[] = "(d.forum $fullid_sql)";
2086     }
2088     $selectdiscussion = "(".implode(" OR ", $where).")";
2090     $messagesearch = '';
2091     $searchstring = '';
2093     // Need to concat these back together for parser to work.
2094     foreach($searchterms as $searchterm){
2095         if ($searchstring != '') {
2096             $searchstring .= ' ';
2097         }
2098         $searchstring .= $searchterm;
2099     }
2101     // We need to allow quoted strings for the search. The quotes *should* be stripped
2102     // by the parser, but this should be examined carefully for security implications.
2103     $searchstring = str_replace("\\\"","\"",$searchstring);
2104     $parser = new search_parser();
2105     $lexer = new search_lexer($parser);
2107     if ($lexer->parse($searchstring)) {
2108         $parsearray = $parser->get_parsed_array();
2109     // Experimental feature under 1.8! MDL-8830
2110     // Use alternative text searches if defined
2111     // This feature only works under mysql until properly implemented for other DBs
2112     // Requires manual creation of text index for forum_posts before enabling it:
2113     // CREATE FULLTEXT INDEX foru_post_tix ON [prefix]forum_posts (subject, message)
2114     // Experimental feature under 1.8! MDL-8830
2115         if (!empty($CFG->forum_usetextsearches)) {
2116             list($messagesearch, $msparams) = search_generate_text_SQL($parsearray, 'p.message', 'p.subject',
2117                                                  'p.userid', 'u.id', 'u.firstname',
2118                                                  'u.lastname', 'p.modified', 'd.forum');
2119         } else {
2120             list($messagesearch, $msparams) = search_generate_SQL($parsearray, 'p.message', 'p.subject',
2121                                                  'p.userid', 'u.id', 'u.firstname',
2122                                                  'u.lastname', 'p.modified', 'd.forum');
2123         }
2124         $params = array_merge($params, $msparams);
2125     }
2127     $fromsql = "{forum_posts} p,
2128                   {forum_discussions} d,
2129                   {user} u";
2131     $selectsql = " $messagesearch
2132                AND p.discussion = d.id
2133                AND p.userid = u.id
2134                AND $selectdiscussion
2135                    $extrasql";
2137     $countsql = "SELECT COUNT(*)
2138                    FROM $fromsql
2139                   WHERE $selectsql";
2141     $allnames = get_all_user_name_fields(true, 'u');
2142     $searchsql = "SELECT p.*,
2143                          d.forum,
2144                          $allnames,
2145                          u.email,
2146                          u.picture,
2147                          u.imagealt
2148                     FROM $fromsql
2149                    WHERE $selectsql
2150                 ORDER BY p.modified DESC";
2152     $totalcount = $DB->count_records_sql($countsql, $params);
2154     return $DB->get_records_sql($searchsql, $params, $limitfrom, $limitnum);
2157 /**
2158  * Returns a list of ratings for a particular post - sorted.
2159  *
2160  * TODO: Check if this function is actually used anywhere.
2161  * Up until the fix for MDL-27471 this function wasn't even returning.
2162  *
2163  * @param stdClass $context
2164  * @param int $postid
2165  * @param string $sort
2166  * @return array Array of ratings or false
2167  */
2168 function forum_get_ratings($context, $postid, $sort = "u.firstname ASC") {
2169     $options = new stdClass;
2170     $options->context = $context;
2171     $options->component = 'mod_forum';
2172     $options->ratingarea = 'post';
2173     $options->itemid = $postid;
2174     $options->sort = "ORDER BY $sort";
2176     $rm = new rating_manager();
2177     return $rm->get_all_ratings_for_item($options);
2180 /**
2181  * Returns a list of all new posts that have not been mailed yet
2182  *
2183  * @param int $starttime posts created after this time
2184  * @param int $endtime posts created before this
2185  * @param int $now used for timed discussions only
2186  * @return array
2187  */
2188 function forum_get_unmailed_posts($starttime, $endtime, $now=null) {
2189     global $CFG, $DB;
2191     $params = array();
2192     $params['mailed'] = FORUM_MAILED_PENDING;
2193     $params['ptimestart'] = $starttime;
2194     $params['ptimeend'] = $endtime;
2195     $params['mailnow'] = 1;
2197     if (!empty($CFG->forum_enabletimedposts)) {
2198         if (empty($now)) {
2199             $now = time();
2200         }
2201         $timedsql = "AND (d.timestart < :dtimestart AND (d.timeend = 0 OR d.timeend > :dtimeend))";
2202         $params['dtimestart'] = $now;
2203         $params['dtimeend'] = $now;
2204     } else {
2205         $timedsql = "";
2206     }
2208     return $DB->get_records_sql("SELECT p.*, d.course, d.forum
2209                                  FROM {forum_posts} p
2210                                  JOIN {forum_discussions} d ON d.id = p.discussion
2211                                  WHERE p.mailed = :mailed
2212                                  AND p.created >= :ptimestart
2213                                  AND (p.created < :ptimeend OR p.mailnow = :mailnow)
2214                                  $timedsql
2215                                  ORDER BY p.modified ASC", $params);
2218 /**
2219  * Marks posts before a certain time as being mailed already
2220  *
2221  * @global object
2222  * @global object
2223  * @param int $endtime
2224  * @param int $now Defaults to time()
2225  * @return bool
2226  */
2227 function forum_mark_old_posts_as_mailed($endtime, $now=null) {
2228     global $CFG, $DB;
2230     if (empty($now)) {
2231         $now = time();
2232     }
2234     $params = array();
2235     $params['mailedsuccess'] = FORUM_MAILED_SUCCESS;
2236     $params['now'] = $now;
2237     $params['endtime'] = $endtime;
2238     $params['mailnow'] = 1;
2239     $params['mailedpending'] = FORUM_MAILED_PENDING;
2241     if (empty($CFG->forum_enabletimedposts)) {
2242         return $DB->execute("UPDATE {forum_posts}
2243                              SET mailed = :mailedsuccess
2244                              WHERE (created < :endtime OR mailnow = :mailnow)
2245                              AND mailed = :mailedpending", $params);
2246     } else {
2247         return $DB->execute("UPDATE {forum_posts}
2248                              SET mailed = :mailedsuccess
2249                              WHERE discussion NOT IN (SELECT d.id
2250                                                       FROM {forum_discussions} d
2251                                                       WHERE d.timestart > :now)
2252                              AND (created < :endtime OR mailnow = :mailnow)
2253                              AND mailed = :mailedpending", $params);
2254     }
2257 /**
2258  * Get all the posts for a user in a forum suitable for forum_print_post
2259  *
2260  * @global object
2261  * @global object
2262  * @uses CONTEXT_MODULE
2263  * @return array
2264  */
2265 function forum_get_user_posts($forumid, $userid) {
2266     global $CFG, $DB;
2268     $timedsql = "";
2269     $params = array($forumid, $userid);
2271     if (!empty($CFG->forum_enabletimedposts)) {
2272         $cm = get_coursemodule_from_instance('forum', $forumid);
2273         if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
2274             $now = time();
2275             $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2276             $params[] = $now;
2277             $params[] = $now;
2278         }
2279     }
2281     $allnames = get_all_user_name_fields(true, 'u');
2282     return $DB->get_records_sql("SELECT p.*, d.forum, $allnames, u.email, u.picture, u.imagealt
2283                               FROM {forum} f
2284                                    JOIN {forum_discussions} d ON d.forum = f.id
2285                                    JOIN {forum_posts} p       ON p.discussion = d.id
2286                                    JOIN {user} u              ON u.id = p.userid
2287                              WHERE f.id = ?
2288                                    AND p.userid = ?
2289                                    $timedsql
2290                           ORDER BY p.modified ASC", $params);
2293 /**
2294  * Get all the discussions user participated in
2295  *
2296  * @global object
2297  * @global object
2298  * @uses CONTEXT_MODULE
2299  * @param int $forumid
2300  * @param int $userid
2301  * @return array Array or false
2302  */
2303 function forum_get_user_involved_discussions($forumid, $userid) {
2304     global $CFG, $DB;
2306     $timedsql = "";
2307     $params = array($forumid, $userid);
2308     if (!empty($CFG->forum_enabletimedposts)) {
2309         $cm = get_coursemodule_from_instance('forum', $forumid);
2310         if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
2311             $now = time();
2312             $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2313             $params[] = $now;
2314             $params[] = $now;
2315         }
2316     }
2318     return $DB->get_records_sql("SELECT DISTINCT d.*
2319                               FROM {forum} f
2320                                    JOIN {forum_discussions} d ON d.forum = f.id
2321                                    JOIN {forum_posts} p       ON p.discussion = d.id
2322                              WHERE f.id = ?
2323                                    AND p.userid = ?
2324                                    $timedsql", $params);
2327 /**
2328  * Get all the posts for a user in a forum suitable for forum_print_post
2329  *
2330  * @global object
2331  * @global object
2332  * @param int $forumid
2333  * @param int $userid
2334  * @return array of counts or false
2335  */
2336 function forum_count_user_posts($forumid, $userid) {
2337     global $CFG, $DB;
2339     $timedsql = "";
2340     $params = array($forumid, $userid);
2341     if (!empty($CFG->forum_enabletimedposts)) {
2342         $cm = get_coursemodule_from_instance('forum', $forumid);
2343         if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
2344             $now = time();
2345             $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2346             $params[] = $now;
2347             $params[] = $now;
2348         }
2349     }
2351     return $DB->get_record_sql("SELECT COUNT(p.id) AS postcount, MAX(p.modified) AS lastpost
2352                              FROM {forum} f
2353                                   JOIN {forum_discussions} d ON d.forum = f.id
2354                                   JOIN {forum_posts} p       ON p.discussion = d.id
2355                                   JOIN {user} u              ON u.id = p.userid
2356                             WHERE f.id = ?
2357                                   AND p.userid = ?
2358                                   $timedsql", $params);
2361 /**
2362  * Given a log entry, return the forum post details for it.
2363  *
2364  * @global object
2365  * @global object
2366  * @param object $log
2367  * @return array|null
2368  */
2369 function forum_get_post_from_log($log) {
2370     global $CFG, $DB;
2372     $allnames = get_all_user_name_fields(true, 'u');
2373     if ($log->action == "add post") {
2375         return $DB->get_record_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid, $allnames, u.email, u.picture
2376                                  FROM {forum_discussions} d,
2377                                       {forum_posts} p,
2378                                       {forum} f,
2379                                       {user} u
2380                                 WHERE p.id = ?
2381                                   AND d.id = p.discussion
2382                                   AND p.userid = u.id
2383                                   AND u.deleted <> '1'
2384                                   AND f.id = d.forum", array($log->info));
2387     } else if ($log->action == "add discussion") {
2389         return $DB->get_record_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid, $allnames, u.email, u.picture
2390                                  FROM {forum_discussions} d,
2391                                       {forum_posts} p,
2392                                       {forum} f,
2393                                       {user} u
2394                                 WHERE d.id = ?
2395                                   AND d.firstpost = p.id
2396                                   AND p.userid = u.id
2397                                   AND u.deleted <> '1'
2398                                   AND f.id = d.forum", array($log->info));
2399     }
2400     return NULL;
2403 /**
2404  * Given a discussion id, return the first post from the discussion
2405  *
2406  * @global object
2407  * @global object
2408  * @param int $dicsussionid
2409  * @return array
2410  */
2411 function forum_get_firstpost_from_discussion($discussionid) {
2412     global $CFG, $DB;
2414     return $DB->get_record_sql("SELECT p.*
2415                              FROM {forum_discussions} d,
2416                                   {forum_posts} p
2417                             WHERE d.id = ?
2418                               AND d.firstpost = p.id ", array($discussionid));
2421 /**
2422  * Returns an array of counts of replies to each discussion
2423  *
2424  * @global object
2425  * @global object
2426  * @param int $forumid
2427  * @param string $forumsort
2428  * @param int $limit
2429  * @param int $page
2430  * @param int $perpage
2431  * @return array
2432  */
2433 function forum_count_discussion_replies($forumid, $forumsort="", $limit=-1, $page=-1, $perpage=0) {
2434     global $CFG, $DB;
2436     if ($limit > 0) {
2437         $limitfrom = 0;
2438         $limitnum  = $limit;
2439     } else if ($page != -1) {
2440         $limitfrom = $page*$perpage;
2441         $limitnum  = $perpage;
2442     } else {
2443         $limitfrom = 0;
2444         $limitnum  = 0;
2445     }
2447     if ($forumsort == "") {
2448         $orderby = "";
2449         $groupby = "";
2451     } else {
2452         $orderby = "ORDER BY $forumsort";
2453         $groupby = ", ".strtolower($forumsort);
2454         $groupby = str_replace('desc', '', $groupby);
2455         $groupby = str_replace('asc', '', $groupby);
2456     }
2458     if (($limitfrom == 0 and $limitnum == 0) or $forumsort == "") {
2459         $sql = "SELECT p.discussion, COUNT(p.id) AS replies, MAX(p.id) AS lastpostid
2460                   FROM {forum_posts} p
2461                        JOIN {forum_discussions} d ON p.discussion = d.id
2462                  WHERE p.parent > 0 AND d.forum = ?
2463               GROUP BY p.discussion";
2464         return $DB->get_records_sql($sql, array($forumid));
2466     } else {
2467         $sql = "SELECT p.discussion, (COUNT(p.id) - 1) AS replies, MAX(p.id) AS lastpostid
2468                   FROM {forum_posts} p
2469                        JOIN {forum_discussions} d ON p.discussion = d.id
2470                  WHERE d.forum = ?
2471               GROUP BY p.discussion $groupby
2472               $orderby";
2473         return $DB->get_records_sql("SELECT * FROM ($sql) sq", array($forumid), $limitfrom, $limitnum);
2474     }
2477 /**
2478  * @global object
2479  * @global object
2480  * @global object
2481  * @staticvar array $cache
2482  * @param object $forum
2483  * @param object $cm
2484  * @param object $course
2485  * @return mixed
2486  */
2487 function forum_count_discussions($forum, $cm, $course) {
2488     global $CFG, $DB, $USER;
2490     static $cache = array();
2492     $now = round(time(), -2); // db cache friendliness
2494     $params = array($course->id);
2496     if (!isset($cache[$course->id])) {
2497         if (!empty($CFG->forum_enabletimedposts)) {
2498             $timedsql = "AND d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?)";
2499             $params[] = $now;
2500             $params[] = $now;
2501         } else {
2502             $timedsql = "";
2503         }
2505         $sql = "SELECT f.id, COUNT(d.id) as dcount
2506                   FROM {forum} f
2507                        JOIN {forum_discussions} d ON d.forum = f.id
2508                  WHERE f.course = ?
2509                        $timedsql
2510               GROUP BY f.id";
2512         if ($counts = $DB->get_records_sql($sql, $params)) {
2513             foreach ($counts as $count) {
2514                 $counts[$count->id] = $count->dcount;
2515             }
2516             $cache[$course->id] = $counts;
2517         } else {
2518             $cache[$course->id] = array();
2519         }
2520     }
2522     if (empty($cache[$course->id][$forum->id])) {
2523         return 0;
2524     }
2526     $groupmode = groups_get_activity_groupmode($cm, $course);
2528     if ($groupmode != SEPARATEGROUPS) {
2529         return $cache[$course->id][$forum->id];
2530     }
2532     if (has_capability('moodle/site:accessallgroups', context_module::instance($cm->id))) {
2533         return $cache[$course->id][$forum->id];
2534     }
2536     require_once($CFG->dirroot.'/course/lib.php');
2538     $modinfo = get_fast_modinfo($course);
2540     $mygroups = $modinfo->get_groups($cm->groupingid);
2542     // add all groups posts
2543     $mygroups[-1] = -1;
2545     list($mygroups_sql, $params) = $DB->get_in_or_equal($mygroups);
2546     $params[] = $forum->id;
2548     if (!empty($CFG->forum_enabletimedposts)) {
2549         $timedsql = "AND d.timestart < $now AND (d.timeend = 0 OR d.timeend > $now)";
2550         $params[] = $now;
2551         $params[] = $now;
2552     } else {
2553         $timedsql = "";
2554     }
2556     $sql = "SELECT COUNT(d.id)
2557               FROM {forum_discussions} d
2558              WHERE d.groupid $mygroups_sql AND d.forum = ?
2559                    $timedsql";
2561     return $DB->get_field_sql($sql, $params);
2564 /**
2565  * How many posts by other users are unrated by a given user in the given discussion?
2566  *
2567  * TODO: Is this function still used anywhere?
2568  *
2569  * @param int $discussionid
2570  * @param int $userid
2571  * @return mixed
2572  */
2573 function forum_count_unrated_posts($discussionid, $userid) {
2574     global $CFG, $DB;
2576     $sql = "SELECT COUNT(*) as num
2577               FROM {forum_posts}
2578              WHERE parent > 0
2579                AND discussion = :discussionid
2580                AND userid <> :userid";
2581     $params = array('discussionid' => $discussionid, 'userid' => $userid);
2582     $posts = $DB->get_record_sql($sql, $params);
2583     if ($posts) {
2584         $sql = "SELECT count(*) as num
2585                   FROM {forum_posts} p,
2586                        {rating} r
2587                  WHERE p.discussion = :discussionid AND
2588                        p.id = r.itemid AND
2589                        r.userid = userid AND
2590                        r.component = 'mod_forum' AND
2591                        r.ratingarea = 'post'";
2592         $rated = $DB->get_record_sql($sql, $params);
2593         if ($rated) {
2594             if ($posts->num > $rated->num) {
2595                 return $posts->num - $rated->num;
2596             } else {
2597                 return 0;    // Just in case there was a counting error
2598             }
2599         } else {
2600             return $posts->num;
2601         }
2602     } else {
2603         return 0;
2604     }
2607 /**
2608  * Get all discussions in a forum
2609  *
2610  * @global object
2611  * @global object
2612  * @global object
2613  * @uses CONTEXT_MODULE
2614  * @uses VISIBLEGROUPS
2615  * @param object $cm
2616  * @param string $forumsort
2617  * @param bool $fullpost
2618  * @param int $unused
2619  * @param int $limit
2620  * @param bool $userlastmodified
2621  * @param int $page
2622  * @param int $perpage
2623  * @return array
2624  */
2625 function forum_get_discussions($cm, $forumsort="d.timemodified DESC", $fullpost=true, $unused=-1, $limit=-1, $userlastmodified=false, $page=-1, $perpage=0) {
2626     global $CFG, $DB, $USER;
2628     $timelimit = '';
2630     $now = round(time(), -2);
2631     $params = array($cm->instance);
2633     $modcontext = context_module::instance($cm->id);
2635     if (!has_capability('mod/forum:viewdiscussion', $modcontext)) { /// User must have perms to view discussions
2636         return array();
2637     }
2639     if (!empty($CFG->forum_enabletimedposts)) { /// Users must fulfill timed posts
2641         if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
2642             $timelimit = " AND ((d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?))";
2643             $params[] = $now;
2644             $params[] = $now;
2645             if (isloggedin()) {
2646                 $timelimit .= " OR d.userid = ?";
2647                 $params[] = $USER->id;
2648             }
2649             $timelimit .= ")";
2650         }
2651     }
2653     if ($limit > 0) {
2654         $limitfrom = 0;
2655         $limitnum  = $limit;
2656     } else if ($page != -1) {
2657         $limitfrom = $page*$perpage;
2658         $limitnum  = $perpage;
2659     } else {
2660         $limitfrom = 0;
2661         $limitnum  = 0;
2662     }
2664     $groupmode    = groups_get_activity_groupmode($cm);
2665     $currentgroup = groups_get_activity_group($cm);
2667     if ($groupmode) {
2668         if (empty($modcontext)) {
2669             $modcontext = context_module::instance($cm->id);
2670         }
2672         if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
2673             if ($currentgroup) {
2674                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2675                 $params[] = $currentgroup;
2676             } else {
2677                 $groupselect = "";
2678             }
2680         } else {
2681             //seprate groups without access all
2682             if ($currentgroup) {
2683                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2684                 $params[] = $currentgroup;
2685             } else {
2686                 $groupselect = "AND d.groupid = -1";
2687             }
2688         }
2689     } else {
2690         $groupselect = "";
2691     }
2694     if (empty($forumsort)) {
2695         $forumsort = "d.timemodified DESC";
2696     }
2697     if (empty($fullpost)) {
2698         $postdata = "p.id,p.subject,p.modified,p.discussion,p.userid";
2699     } else {
2700         $postdata = "p.*";
2701     }
2703     if (empty($userlastmodified)) {  // We don't need to know this
2704         $umfields = "";
2705         $umtable  = "";
2706     } else {
2707         $umfields = ', ' . get_all_user_name_fields(true, 'um', null, 'um');
2708         $umtable  = " LEFT JOIN {user} um ON (d.usermodified = um.id)";
2709     }
2711     $allnames = get_all_user_name_fields(true, 'u');
2712     $sql = "SELECT $postdata, d.name, d.timemodified, d.usermodified, d.groupid, d.timestart, d.timeend, $allnames,
2713                    u.email, u.picture, u.imagealt $umfields
2714               FROM {forum_discussions} d
2715                    JOIN {forum_posts} p ON p.discussion = d.id
2716                    JOIN {user} u ON p.userid = u.id
2717                    $umtable
2718              WHERE d.forum = ? AND p.parent = 0
2719                    $timelimit $groupselect
2720           ORDER BY $forumsort";
2721     return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
2724 /**
2725  *
2726  * @global object
2727  * @global object
2728  * @global object
2729  * @uses CONTEXT_MODULE
2730  * @uses VISIBLEGROUPS
2731  * @param object $cm
2732  * @return array
2733  */
2734 function forum_get_discussions_unread($cm) {
2735     global $CFG, $DB, $USER;
2737     $now = round(time(), -2);
2738     $cutoffdate = $now - ($CFG->forum_oldpostdays*24*60*60);
2740     $params = array();
2741     $groupmode    = groups_get_activity_groupmode($cm);
2742     $currentgroup = groups_get_activity_group($cm);
2744     if ($groupmode) {
2745         $modcontext = context_module::instance($cm->id);
2747         if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
2748             if ($currentgroup) {
2749                 $groupselect = "AND (d.groupid = :currentgroup OR d.groupid = -1)";
2750                 $params['currentgroup'] = $currentgroup;
2751             } else {
2752                 $groupselect = "";
2753             }
2755         } else {
2756             //separate groups without access all
2757             if ($currentgroup) {
2758                 $groupselect = "AND (d.groupid = :currentgroup OR d.groupid = -1)";
2759                 $params['currentgroup'] = $currentgroup;
2760             } else {
2761                 $groupselect = "AND d.groupid = -1";
2762             }
2763         }
2764     } else {
2765         $groupselect = "";
2766     }
2768     if (!empty($CFG->forum_enabletimedposts)) {
2769         $timedsql = "AND d.timestart < :now1 AND (d.timeend = 0 OR d.timeend > :now2)";
2770         $params['now1'] = $now;
2771         $params['now2'] = $now;
2772     } else {
2773         $timedsql = "";
2774     }
2776     $sql = "SELECT d.id, COUNT(p.id) AS unread
2777               FROM {forum_discussions} d
2778                    JOIN {forum_posts} p     ON p.discussion = d.id
2779                    LEFT JOIN {forum_read} r ON (r.postid = p.id AND r.userid = $USER->id)
2780              WHERE d.forum = {$cm->instance}
2781                    AND p.modified >= :cutoffdate AND r.id is NULL
2782                    $groupselect
2783                    $timedsql
2784           GROUP BY d.id";
2785     $params['cutoffdate'] = $cutoffdate;
2787     if ($unreads = $DB->get_records_sql($sql, $params)) {
2788         foreach ($unreads as $unread) {
2789             $unreads[$unread->id] = $unread->unread;
2790         }
2791         return $unreads;
2792     } else {
2793         return array();
2794     }
2797 /**
2798  * @global object
2799  * @global object
2800  * @global object
2801  * @uses CONEXT_MODULE
2802  * @uses VISIBLEGROUPS
2803  * @param object $cm
2804  * @return array
2805  */
2806 function forum_get_discussions_count($cm) {
2807     global $CFG, $DB, $USER;
2809     $now = round(time(), -2);
2810     $params = array($cm->instance);
2811     $groupmode    = groups_get_activity_groupmode($cm);
2812     $currentgroup = groups_get_activity_group($cm);
2814     if ($groupmode) {
2815         $modcontext = context_module::instance($cm->id);
2817         if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
2818             if ($currentgroup) {
2819                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2820                 $params[] = $currentgroup;
2821             } else {
2822                 $groupselect = "";
2823             }
2825         } else {
2826             //seprate groups without access all
2827             if ($currentgroup) {
2828                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2829                 $params[] = $currentgroup;
2830             } else {
2831                 $groupselect = "AND d.groupid = -1";
2832             }
2833         }
2834     } else {
2835         $groupselect = "";
2836     }
2838     $cutoffdate = $now - ($CFG->forum_oldpostdays*24*60*60);
2840     $timelimit = "";
2842     if (!empty($CFG->forum_enabletimedposts)) {
2844         $modcontext = context_module::instance($cm->id);
2846         if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
2847             $timelimit = " AND ((d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?))";
2848             $params[] = $now;
2849             $params[] = $now;
2850             if (isloggedin()) {
2851                 $timelimit .= " OR d.userid = ?";
2852                 $params[] = $USER->id;
2853             }
2854             $timelimit .= ")";
2855         }
2856     }
2858     $sql = "SELECT COUNT(d.id)
2859               FROM {forum_discussions} d
2860                    JOIN {forum_posts} p ON p.discussion = d.id
2861              WHERE d.forum = ? AND p.parent = 0
2862                    $groupselect $timelimit";
2864     return $DB->get_field_sql($sql, $params);
2868 /**
2869  * Get all discussions started by a particular user in a course (or group)
2870  * This function no longer used ...
2871  *
2872  * @todo Remove this function if no longer used
2873  * @global object
2874  * @global object
2875  * @param int $courseid
2876  * @param int $userid
2877  * @param int $groupid
2878  * @return array
2879  */
2880 function forum_get_user_discussions($courseid, $userid, $groupid=0) {
2881     global $CFG, $DB;
2882     $params = array($courseid, $userid);
2883     if ($groupid) {
2884         $groupselect = " AND d.groupid = ? ";
2885         $params[] = $groupid;
2886     } else  {
2887         $groupselect = "";
2888     }
2890     $allnames = get_all_user_name_fields(true, 'u');
2891     return $DB->get_records_sql("SELECT p.*, d.groupid, $allnames, u.email, u.picture, u.imagealt,
2892                                    f.type as forumtype, f.name as forumname, f.id as forumid
2893                               FROM {forum_discussions} d,
2894                                    {forum_posts} p,
2895                                    {user} u,
2896                                    {forum} f
2897                              WHERE d.course = ?
2898                                AND p.discussion = d.id
2899                                AND p.parent = 0
2900                                AND p.userid = u.id
2901                                AND u.id = ?
2902                                AND d.forum = f.id $groupselect
2903                           ORDER BY p.created DESC", $params);
2906 /**
2907  * Get the list of potential subscribers to a forum.
2908  *
2909  * @param object $forumcontext the forum context.
2910  * @param integer $groupid the id of a group, or 0 for all groups.
2911  * @param string $fields the list of fields to return for each user. As for get_users_by_capability.
2912  * @param string $sort sort order. As for get_users_by_capability.
2913  * @return array list of users.
2914  */
2915 function forum_get_potential_subscribers($forumcontext, $groupid, $fields, $sort = '') {
2916     global $DB;
2918     // only active enrolled users or everybody on the frontpage
2919     list($esql, $params) = get_enrolled_sql($forumcontext, 'mod/forum:allowforcesubscribe', $groupid, true);
2920     if (!$sort) {
2921         list($sort, $sortparams) = users_order_by_sql('u');
2922         $params = array_merge($params, $sortparams);
2923     }
2925     $sql = "SELECT $fields
2926               FROM {user} u
2927               JOIN ($esql) je ON je.id = u.id
2928           ORDER BY $sort";
2930     return $DB->get_records_sql($sql, $params);
2933 /**
2934  * Returns list of user objects that are subscribed to this forum
2935  *
2936  * @global object
2937  * @global object
2938  * @param object $course the course
2939  * @param forum $forum the forum
2940  * @param integer $groupid group id, or 0 for all.
2941  * @param object $context the forum context, to save re-fetching it where possible.
2942  * @param string $fields requested user fields (with "u." table prefix)
2943  * @return array list of users.
2944  */
2945 function forum_subscribed_users($course, $forum, $groupid=0, $context = null, $fields = null) {
2946     global $CFG, $DB;
2948     $allnames = get_all_user_name_fields(true, 'u');
2949     if (empty($fields)) {
2950         $fields ="u.id,
2951                   u.username,
2952                   $allnames,
2953                   u.maildisplay,
2954                   u.mailformat,
2955                   u.maildigest,
2956                   u.imagealt,
2957                   u.email,
2958                   u.emailstop,
2959                   u.city,
2960                   u.country,
2961                   u.lastaccess,
2962                   u.lastlogin,
2963                   u.picture,
2964                   u.timezone,
2965                   u.theme,
2966                   u.lang,
2967                   u.trackforums,
2968                   u.mnethostid";
2969     }
2971     if (empty($context)) {
2972         $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id);
2973         $context = context_module::instance($cm->id);
2974     }
2976     if (forum_is_forcesubscribed($forum)) {
2977         $results = forum_get_potential_subscribers($context, $groupid, $fields, "u.email ASC");
2979     } else {
2980         // only active enrolled users or everybody on the frontpage
2981         list($esql, $params) = get_enrolled_sql($context, '', $groupid, true);
2982         $params['forumid'] = $forum->id;
2983         $results = $DB->get_records_sql("SELECT $fields
2984                                            FROM {user} u
2985                                            JOIN ($esql) je ON je.id = u.id
2986                                            JOIN {forum_subscriptions} s ON s.userid = u.id
2987                                           WHERE s.forum = :forumid
2988                                        ORDER BY u.email ASC", $params);
2989     }
2991     // Guest user should never be subscribed to a forum.
2992     unset($results[$CFG->siteguest]);
2994     return $results;
2999 // OTHER FUNCTIONS ///////////////////////////////////////////////////////////
3002 /**
3003  * @global object
3004  * @global object
3005  * @param int $courseid
3006  * @param string $type
3007  */
3008 function forum_get_course_forum($courseid, $type) {
3009 // How to set up special 1-per-course forums
3010     global $CFG, $DB, $OUTPUT, $USER;
3012     if ($forums = $DB->get_records_select("forum", "course = ? AND type = ?", array($courseid, $type), "id ASC")) {
3013         // There should always only be ONE, but with the right combination of
3014         // errors there might be more.  In this case, just return the oldest one (lowest ID).
3015         foreach ($forums as $forum) {
3016             return $forum;   // ie the first one
3017         }
3018     }
3020     // Doesn't exist, so create one now.
3021     $forum = new stdClass();
3022     $forum->course = $courseid;
3023     $forum->type = "$type";
3024     if (!empty($USER->htmleditor)) {
3025         $forum->introformat = $USER->htmleditor;
3026     }
3027     switch ($forum->type) {
3028         case "news":
3029             $forum->name  = get_string("namenews", "forum");
3030             $forum->intro = get_string("intronews", "forum");
3031             $forum->forcesubscribe = FORUM_FORCESUBSCRIBE;
3032             $forum->assessed = 0;
3033             if ($courseid == SITEID) {
3034                 $forum->name  = get_string("sitenews");
3035                 $forum->forcesubscribe = 0;
3036             }
3037             break;
3038         case "social":
3039             $forum->name  = get_string("namesocial", "forum");
3040             $forum->intro = get_string("introsocial", "forum");
3041             $forum->assessed = 0;
3042             $forum->forcesubscribe = 0;
3043             break;
3044         case "blog":
3045             $forum->name = get_string('blogforum', 'forum');
3046             $forum->intro = get_string('introblog', 'forum');
3047             $forum->assessed = 0;
3048             $forum->forcesubscribe = 0;
3049             break;
3050         default:
3051             echo $OUTPUT->notification("That forum type doesn't exist!");
3052             return false;
3053             break;
3054     }
3056     $forum->timemodified = time();
3057     $forum->id = $DB->insert_record("forum", $forum);
3059     if (! $module = $DB->get_record("modules", array("name" => "forum"))) {
3060         echo $OUTPUT->notification("Could not find forum module!!");
3061         return false;
3062     }
3063     $mod = new stdClass();
3064     $mod->course = $courseid;
3065     $mod->module = $module->id;
3066     $mod->instance = $forum->id;
3067     $mod->section = 0;
3068     include_once("$CFG->dirroot/course/lib.php");
3069     if (! $mod->coursemodule = add_course_module($mod) ) {
3070         echo $OUTPUT->notification("Could not add a new course module to the course '" . $courseid . "'");
3071         return false;
3072     }
3073     $sectionid = course_add_cm_to_section($courseid, $mod->coursemodule, 0);
3074     return $DB->get_record("forum", array("id" => "$forum->id"));
3078 /**
3079  * Given the data about a posting, builds up the HTML to display it and
3080  * returns the HTML in a string.  This is designed for sending via HTML email.
3081  *
3082  * @global object
3083  * @param object $course
3084  * @param object $cm
3085  * @param object $forum
3086  * @param object $discussion
3087  * @param object $post
3088  * @param object $userform
3089  * @param object $userto
3090  * @param bool $ownpost
3091  * @param bool $reply
3092  * @param bool $link
3093  * @param bool $rate
3094  * @param string $footer
3095  * @return string
3096  */
3097 function forum_make_mail_post($course, $cm, $forum, $discussion, $post, $userfrom, $userto,
3098                               $ownpost=false, $reply=false, $link=false, $rate=false, $footer="") {
3100     global $CFG, $OUTPUT;
3102     $modcontext = context_module::instance($cm->id);
3104     if (!isset($userto->viewfullnames[$forum->id])) {
3105         $viewfullnames = has_capability('moodle/site:viewfullnames', $modcontext, $userto->id);
3106     } else {
3107         $viewfullnames = $userto->viewfullnames[$forum->id];
3108     }
3110     // add absolute file links
3111     $post->message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php', $modcontext->id, 'mod_forum', 'post', $post->id);
3113     // format the post body
3114     $options = new stdClass();
3115     $options->para = true;
3116     $formattedtext = format_text($post->message, $post->messageformat, $options, $course->id);
3118     $output = '<table border="0" cellpadding="3" cellspacing="0" class="forumpost">';
3120     $output .= '<tr class="header"><td width="35" valign="top" class="picture left">';
3121     $output .= $OUTPUT->user_picture($userfrom, array('courseid'=>$course->id));
3122     $output .= '</td>';
3124     if ($post->parent) {
3125         $output .= '<td class="topic">';
3126     } else {
3127         $output .= '<td class="topic starter">';
3128     }
3129     $output .= '<div class="subject">'.format_string($post->subject).'</div>';
3131     $fullname = fullname($userfrom, $viewfullnames);
3132     $by = new stdClass();
3133     $by->name = '<a href="'.$CFG->wwwroot.'/user/view.php?id='.$userfrom->id.'&amp;course='.$course->id.'">'.$fullname.'</a>';
3134     $by->date = userdate($post->modified, '', $userto->timezone);
3135     $output .= '<div class="author">'.get_string('bynameondate', 'forum', $by).'</div>';
3137     $output .= '</td></tr>';
3139     $output .= '<tr><td class="left side" valign="top">';
3141     if (isset($userfrom->groups)) {
3142         $groups = $userfrom->groups[$forum->id];
3143     } else {
3144         $groups = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid);
3145     }
3147     if ($groups) {
3148         $output .= print_group_picture($groups, $course->id, false, true, true);
3149     } else {
3150         $output .= '&nbsp;';
3151     }
3153     $output .= '</td><td class="content">';
3155     $attachments = forum_print_attachments($post, $cm, 'html');
3156     if ($attachments !== '') {
3157         $output .= '<div class="attachments">';
3158         $output .= $attachments;
3159         $output .= '</div>';
3160     }
3162     $output .= $formattedtext;
3164 // Commands
3165     $commands = array();
3167     if ($post->parent) {
3168         $commands[] = '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.
3169                       $post->discussion.'&amp;parent='.$post->parent.'">'.get_string('parent', 'forum').'</a>';
3170     }
3172     if ($reply) {
3173         $commands[] = '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/post.php?reply='.$post->id.'">'.
3174                       get_string('reply', 'forum').'</a>';
3175     }
3177     $output .= '<div class="commands">';
3178     $output .= implode(' | ', $commands);
3179     $output .= '</div>';
3181 // Context link to post if required
3182     if ($link) {
3183         $output .= '<div class="link">';
3184         $output .= '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'#p'.$post->id.'">'.
3185                      get_string('postincontext', 'forum').'</a>';
3186         $output .= '</div>';
3187     }
3189     if ($footer) {
3190         $output .= '<div class="footer">'.$footer.'</div>';
3191     }
3192     $output .= '</td></tr></table>'."\n\n";
3194     return $output;
3197 /**
3198  * Print a forum post
3199  *
3200  * @global object
3201  * @global object
3202  * @uses FORUM_MODE_THREADED
3203  * @uses PORTFOLIO_FORMAT_PLAINHTML
3204  * @uses PORTFOLIO_FORMAT_FILE
3205  * @uses PORTFOLIO_FORMAT_RICHHTML
3206  * @uses PORTFOLIO_ADD_TEXT_LINK
3207  * @uses CONTEXT_MODULE
3208  * @param object $post The post to print.
3209  * @param object $discussion
3210  * @param object $forum
3211  * @param object $cm
3212  * @param object $course
3213  * @param boolean $ownpost Whether this post belongs to the current user.
3214  * @param boolean $reply Whether to print a 'reply' link at the bottom of the message.
3215  * @param boolean $link Just print a shortened version of the post as a link to the full post.
3216  * @param string $footer Extra stuff to print after the message.
3217  * @param string $highlight Space-separated list of terms to highlight.
3218  * @param int $post_read true, false or -99. If we already know whether this user
3219  *          has read this post, pass that in, otherwise, pass in -99, and this
3220  *          function will work it out.
3221  * @param boolean $dummyifcantsee When forum_user_can_see_post says that
3222  *          the current user can't see this post, if this argument is true
3223  *          (the default) then print a dummy 'you can't see this post' post.
3224  *          If false, don't output anything at all.
3225  * @param bool|null $istracked
3226  * @return void
3227  */
3228 function forum_print_post($post, $discussion, $forum, &$cm, $course, $ownpost=false, $reply=false, $link=false,
3229                           $footer="", $highlight="", $postisread=null, $dummyifcantsee=true, $istracked=null, $return=false) {
3230     global $USER, $CFG, $OUTPUT;
3232     require_once($CFG->libdir . '/filelib.php');
3234     // String cache
3235     static $str;
3237     $modcontext = context_module::instance($cm->id);
3239     $post->course = $course->id;
3240     $post->forum  = $forum->id;
3241     $post->message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php', $modcontext->id, 'mod_forum', 'post', $post->id);
3242     if (!empty($CFG->enableplagiarism)) {
3243         require_once($CFG->libdir.'/plagiarismlib.php');
3244         $post->message .= plagiarism_get_links(array('userid' => $post->userid,
3245             'content' => $post->message,
3246             'cmid' => $cm->id,
3247             'course' => $post->course,
3248             'forum' => $post->forum));
3249     }
3251     // caching
3252     if (!isset($cm->cache)) {
3253         $cm->cache = new stdClass;
3254     }
3256     if (!isset($cm->cache->caps)) {
3257         $cm->cache->caps = array();
3258         $cm->cache->caps['mod/forum:viewdiscussion']   = has_capability('mod/forum:viewdiscussion', $modcontext);
3259         $cm->cache->caps['moodle/site:viewfullnames']  = has_capability('moodle/site:viewfullnames', $modcontext);
3260         $cm->cache->caps['mod/forum:editanypost']      = has_capability('mod/forum:editanypost', $modcontext);
3261         $cm->cache->caps['mod/forum:splitdiscussions'] = has_capability('mod/forum:splitdiscussions', $modcontext);
3262         $cm->cache->caps['mod/forum:deleteownpost']    = has_capability('mod/forum:deleteownpost', $modcontext);
3263         $cm->cache->caps['mod/forum:deleteanypost']    = has_capability('mod/forum:deleteanypost', $modcontext);
3264         $cm->cache->caps['mod/forum:viewanyrating']    = has_capability('mod/forum:viewanyrating', $modcontext);
3265         $cm->cache->caps['mod/forum:exportpost']       = has_capability('mod/forum:exportpost', $modcontext);
3266         $cm->cache->caps['mod/forum:exportownpost']    = has_capability('mod/forum:exportownpost', $modcontext);
3267     }
3269     if (!isset($cm->uservisible)) {
3270         $cm->uservisible = coursemodule_visible_for_user($cm);
3271     }
3273     if ($istracked && is_null($postisread)) {
3274         $postisread = forum_tp_is_post_read($USER->id, $post);
3275     }
3277     if (!forum_user_can_see_post($forum, $discussion, $post, NULL, $cm)) {
3278         $output = '';
3279         if (!$dummyifcantsee) {
3280             if ($return) {
3281                 return $output;
3282             }
3283             echo $output;
3284             return;
3285         }
3286         $output .= html_writer::tag('a', '', array('id'=>'p'.$post->id));
3287         $output .= html_writer::start_tag('div', array('class'=>'forumpost clearfix',
3288                                                        'role' => 'region',
3289                                                        'aria-label' => get_string('hiddenforumpost', 'forum')));
3290         $output .= html_writer::start_tag('div', array('class'=>'row header'));
3291         $output .= html_writer::tag('div', '', array('class'=>'left picture')); // Picture
3292         if ($post->parent) {
3293             $output .= html_writer::start_tag('div', array('class'=>'topic'));
3294         } else {
3295             $output .= html_writer::start_tag('div', array('class'=>'topic starter'));
3296         }
3297         $output .= html_writer::tag('div', get_string('forumsubjecthidden','forum'), array('class' => 'subject',
3298                                                                                            'role' => 'header')); // Subject.
3299         $output .= html_writer::tag('div', get_string('forumauthorhidden', 'forum'), array('class' => 'author',
3300                                                                                            'role' => 'header')); // Author.
3301         $output .= html_writer::end_tag('div');
3302         $output .= html_writer::end_tag('div'); // row
3303         $output .= html_writer::start_tag('div', array('class'=>'row'));
3304         $output .= html_writer::tag('div', '&nbsp;', array('class'=>'left side')); // Groups
3305         $output .= html_writer::tag('div', get_string('forumbodyhidden','forum'), array('class'=>'content')); // Content
3306         $output .= html_writer::end_tag('div'); // row
3307         $output .= html_writer::end_tag('div'); // forumpost
3309         if ($return) {
3310             return $output;
3311         }
3312         echo $output;
3313         return;
3314     }
3316     if (empty($str)) {
3317         $str = new stdClass;
3318         $str->edit         = get_string('edit', 'forum');
3319         $str->delete       = get_string('delete', 'forum');
3320         $str->reply        = get_string('reply', 'forum');
3321         $str->parent       = get_string('parent', 'forum');
3322         $str->pruneheading = get_string('pruneheading', 'forum');
3323         $str->prune        = get_string('prune', 'forum');
3324         $str->displaymode     = get_user_preferences('forum_displaymode', $CFG->forum_displaymode);
3325         $str->markread     = get_string('markread', 'forum');
3326         $str->markunread   = get_string('markunread', 'forum');
3327     }
3329     $discussionlink = new moodle_url('/mod/forum/discuss.php', array('d'=>$post->discussion));
3331     // Build an object that represents the posting user
3332     $postuser = new stdClass;
3333     $postuserfields = explode(',', user_picture::fields());
3334     $postuser = username_load_fields_from_object($postuser, $post, null, $postuserfields);
3335     $postuser->id = $post->userid;
3336     $postuser->fullname    = fullname($postuser, $cm->cache->caps['moodle/site:viewfullnames']);
3337     $postuser->profilelink = new moodle_url('/user/view.php', array('id'=>$post->userid, 'course'=>$course->id));
3339     // Prepare the groups the posting user belongs to
3340     if (isset($cm->cache->usersgroups)) {
3341         $groups = array();
3342         if (isset($cm->cache->usersgroups[$post->userid])) {
3343             foreach ($cm->cache->usersgroups[$post->userid] as $gid) {
3344                 $groups[$gid] = $cm->cache->groups[$gid];
3345             }
3346         }
3347     } else {
3348         $groups = groups_get_all_groups($course->id, $post->userid, $cm->groupingid);
3349     }
3351     // Prepare the attachements for the post, files then images
3352     list($attachments, $attachedimages) = forum_print_attachments($post, $cm, 'separateimages');
3354     // Determine if we need to shorten this post
3355     $shortenpost = ($link && (strlen(strip_tags($post->message)) > $CFG->forum_longpost));
3358     // Prepare an array of commands
3359     $commands = array();
3361     // SPECIAL CASE: The front page can display a news item post to non-logged in users.
3362     // Don't display the mark read / unread controls in this case.
3363     if ($istracked && $CFG->forum_usermarksread && isloggedin()) {
3364         $url = new moodle_url($discussionlink, array('postid'=>$post->id, 'mark'=>'unread'));
3365         $text = $str->markunread;
3366         if (!$postisread) {
3367             $url->param('mark', 'read');
3368             $text = $str->markread;
3369         }
3370         if ($str->displaymode == FORUM_MODE_THREADED) {
3371             $url->param('parent', $post->parent);
3372         } else {
3373             $url->set_anchor('p'.$post->id);
3374         }
3375         $commands[] = array('url'=>$url, 'text'=>$text);
3376     }
3378     // Zoom in to the parent specifically
3379     if ($post->parent) {
3380         $url = new moodle_url($discussionlink);
3381         if ($str->displaymode == FORUM_MODE_THREADED) {
3382             $url->param('parent', $post->parent);
3383         } else {
3384             $url->set_anchor('p'.$post->parent);
3385         }
3386         $commands[] = array('url'=>$url, 'text'=>$str->parent);
3387     }
3389     // Hack for allow to edit news posts those are not displayed yet until they are displayed
3390     $age = time() - $post->created;
3391     if (!$post->parent && $forum->type == 'news' && $discussion->timestart > time()) {
3392         $age = 0;
3393     }
3395     if ($forum->type == 'single' and $discussion->firstpost == $post->id) {
3396         if (has_capability('moodle/course:manageactivities', $modcontext)) {
3397             // The first post in single simple is the forum description.
3398             $commands[] = array('url'=>new moodle_url('/course/modedit.php', array('update'=>$cm->id, 'sesskey'=>sesskey(), 'return'=>1)), 'text'=>$str->edit);
3399         }
3400     } else if (($ownpost && $age < $CFG->maxeditingtime) || $cm->cache->caps['mod/forum:editanypost']) {
3401         $commands[] = array('url'=>new moodle_url('/mod/forum/post.php', array('edit'=>$post->id)), 'text'=>$str->edit);
3402     }
3404     if ($cm->cache->caps['mod/forum:splitdiscussions'] && $post->parent && $forum->type != 'single') {
3405         $commands[] = array('url'=>new moodle_url('/mod/forum/post.php', array('prune'=>$post->id)), 'text'=>$str->prune, 'title'=>$str->pruneheading);
3406     }
3408     if ($forum->type == 'single' and $discussion->firstpost == $post->id) {
3409         // Do not allow deleting of first post in single simple type.
3410     } else if (($ownpost && $age < $CFG->maxeditingtime && $cm->cache->caps['mod/forum:deleteownpost']) || $cm->cache->caps['mod/forum:deleteanypost']) {
3411         $commands[] = array('url'=>new moodle_url('/mod/forum/post.php', array('delete'=>$post->id)), 'text'=>$str->delete);
3412     }
3414     if ($reply) {
3415         $commands[] = array('url'=>new moodle_url('/mod/forum/post.php#mformforum', array('reply'=>$post->id)), 'text'=>$str->reply);
3416     }
3418     if ($CFG->enableportfolios && ($cm->cache->caps['mod/forum:exportpost'] || ($ownpost && $cm->cache->caps['mod/forum:exportownpost']))) {
3419         $p = array('postid' => $post->id);
3420         require_once($CFG->libdir.'/portfoliolib.php');
3421         $button = new portfolio_add_button();
3422         $button->set_callback_options('forum_portfolio_caller', array('postid' => $post->id), 'mod_forum');
3423         if (empty($attachments)) {
3424             $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML);
3425         } else {
3426             $button->set_formats(PORTFOLIO_FORMAT_RICHHTML);
3427         }
3429         $porfoliohtml = $button->to_html(PORTFOLIO_ADD_TEXT_LINK);
3430         if (!empty($porfoliohtml)) {
3431             $commands[] = $porfoliohtml;
3432         }
3433     }
3434     // Finished building commands
3437     // Begin output
3439     $output  = '';
3441     if ($istracked) {
3442         if ($postisread) {
3443             $forumpostclass = ' read';
3444         } else {
3445             $forumpostclass = ' unread';
3446             $output .= html_writer::tag('a', '', array('name'=>'unread'));
3447         }
3448     } else {
3449         // ignore trackign status if not tracked or tracked param missing
3450         $forumpostclass = '';
3451     }
3453     $topicclass = '';
3454     if (empty($post->parent)) {
3455         $topicclass = ' firstpost starter';
3456     }
3458     $postbyuser = new stdClass;
3459     $postbyuser->post = $post->subject;
3460     $postbyuser->user = $postuser->fullname;
3461     $discussionbyuser = get_string('postbyuser', 'forum', $postbyuser);
3462     $output .= html_writer::tag('a', '', array('id'=>'p'.$post->id));
3463     $output .= html_writer::start_tag('div', array('class'=>'forumpost clearfix'.$forumpostclass.$topicclass,
3464                                                    'role' => 'region',
3465                                                    'aria-label' => $discussionbyuser));
3466     $output .= html_writer::start_tag('div', array('class'=>'row header clearfix'));
3467     $output .= html_writer::start_tag('div', array('class'=>'left picture'));
3468     $output .= $OUTPUT->user_picture($postuser, array('courseid'=>$course->id));
3469     $output .= html_writer::end_tag('div');
3472     $output .= html_writer::start_tag('div', array('class'=>'topic'.$topicclass));
3474     $postsubject = $post->subject;
3475     if (empty($post->subjectnoformat)) {
3476         $postsubject = format_string($postsubject);
3477     }
3478     $output .= html_writer::tag('div', $postsubject, array('class'=>'subject',
3479                                                            'role' => 'heading',
3480                                                            'aria-level' => '2'));
3482     $by = new stdClass();
3483     $by->name = html_writer::link($postuser->profilelink, $postuser->fullname);
3484     $by->date = userdate($post->modified);
3485     $output .= html_writer::tag('div', get_string('bynameondate', 'forum', $by), array('class'=>'author',
3486                                               &n